From 4c080dfd5fdd17cb0e01ddcbb95e6900172e138e Mon Sep 17 00:00:00 2001 From: ByteBaker <42913098+ByteBaker@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:39:52 +0530 Subject: [PATCH] fix: alert graph edges not rendering due to timestamp equality (#10347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### **User description** ## Problem Fixes openobserve/o2-enterprise#1229 Alert graphs were not rendering edges (arrows) when nodes had identical timestamps, causing the topology graph to show nodes without connections. ## Root Cause The edge creation logic used strict timestamp inequality (`<`, `>`), which failed when alerts fired at the exact same microsecond. This commonly occurs with: - High-frequency alerting - Bulk alert ingestion - Simulated/test data with fixed timestamps - Clock resolution limitations ## Solution Added timestamp tiebreaker logic using node index (insertion order) for all three edge creation scenarios: 1. **Same-service temporal edges**: Connect to most recent previous node, use index as tiebreaker 2. **Cross-service temporal edges**: Connect to chronologically closest node, use index as tiebreaker 3. **Service dependency edges**: Connect related services, use index as tiebreaker ## Changes - `src/service/alerts/incidents.rs`: Updated 3 edge creation locations with timestamp + index comparisons ## Impact - ✅ Edges now created even when alerts have identical timestamps - ✅ Deterministic behavior based on insertion order - ✅ Graph visualization shows proper alert flow with arrows - ✅ Root cause analysis now possible ## Testing - Verified code compiles successfully - Manual testing recommended with alerts at same timestamp ___ ### **PR Type** Bug fix ___ ### **Description** - Add tiebreaker for equal timestamps - Ensure deterministic topology edge creation - Fix missing incident graph arrows ___ ### Diagram Walkthrough ```mermaid flowchart LR A["New alert node added"] --> B["Find candidate prior/next nodes"] B --> C["Compare (first_fired_at, node_index)"] C --> D["Create temporal/dependency edges deterministically"] ```

File Walkthrough

Relevant files
Bug fix
incidents.rs
Deterministic edge creation for equal timestamps                 

src/service/alerts/incidents.rs
  • Add index-based tiebreaker for same-service edges
  • Allow dependency edges when timestamps equal
  • Refine cross-service temporal matching with index
  • Update ordering keys to include node index
+17/-5   
___ --- src/service/alerts/incidents.rs | 22 ++++++-- web/src/plugins/traces/ServiceGraph.vue | 74 ++++++++----------------- 2 files changed, 40 insertions(+), 56 deletions(-) diff --git a/src/service/alerts/incidents.rs b/src/service/alerts/incidents.rs index b0d3c18db6..4c5537a6d9 100644 --- a/src/service/alerts/incidents.rs +++ b/src/service/alerts/incidents.rs @@ -729,6 +729,7 @@ pub async fn enrich_with_topology( let current_node_first_fired = topology.nodes[current_node_index].first_fired_at; // Find the most recent previous node from same service + // Use node index as tiebreaker when timestamps are equal (handles simultaneous alerts) let most_recent_previous = topology .nodes .iter() @@ -736,9 +737,11 @@ pub async fn enrich_with_topology( .filter(|(idx, node)| { *idx != current_node_index && node.service_name == service_name - && node.first_fired_at < current_node_first_fired + && (node.first_fired_at < current_node_first_fired + || (node.first_fired_at == current_node_first_fired + && *idx < current_node_index)) }) - .max_by_key(|(_, node)| node.first_fired_at); + .max_by_key(|(idx, node)| (node.first_fired_at, *idx)); if let Some((prev_idx, _)) = most_recent_previous { // Check if edge already exists @@ -814,7 +817,11 @@ pub async fn enrich_with_topology( let to_node = &topology.nodes[to_idx]; // Only add if from happened before to (chronological) - if from_node.first_fired_at < to_node.first_fired_at { + // Use node index as tiebreaker when timestamps are equal (handles simultaneous + // alerts) + if from_node.first_fired_at < to_node.first_fired_at + || (from_node.first_fired_at == to_node.first_fired_at && from_idx < to_idx) + { // Check if edge already exists let edge_exists = topology.edges.iter().any(|e| { e.from_node_index == from_idx @@ -843,17 +850,22 @@ pub async fn enrich_with_topology( let from_node = &topology.nodes[from_idx]; // Find most recent node in to_service that fired after from_node + // Use node index as tiebreaker when timestamps are equal (handles simultaneous + // alerts) let most_recent_to = to_indices .iter() .filter_map(|&to_idx| { let to_node = &topology.nodes[to_idx]; - if to_node.first_fired_at > from_node.first_fired_at { + if to_node.first_fired_at > from_node.first_fired_at + || (to_node.first_fired_at == from_node.first_fired_at + && to_idx > from_idx) + { Some((to_idx, to_node.first_fired_at)) } else { None } }) - .min_by_key(|(_, fired_at)| *fired_at); // Closest in time (earliest after from_node) + .min_by_key(|(to_idx, fired_at)| (*fired_at, *to_idx)); // Closest in time, then by index if let Some((to_idx, _)) = most_recent_to { // Check if edge already exists diff --git a/web/src/plugins/traces/ServiceGraph.vue b/web/src/plugins/traces/ServiceGraph.vue index b3d7038f7e..3391947d4a 100644 --- a/web/src/plugins/traces/ServiceGraph.vue +++ b/web/src/plugins/traces/ServiceGraph.vue @@ -88,6 +88,7 @@ ]" dense no-caps + class="view-toggle-buttons" @update:model-value="setVisualizationType" /> @@ -144,58 +145,9 @@
No Service Graph Data
-
- Possible causes: +
+ Try querying a longer duration
-
-
-
- Queried time range has no traces, try selecting a longer duration. -
-
- -
-
- Maybe your traces have only internal spans (span_kind = 1)? -
-
- Make sure O2_SERVICE_GRAPH_EXCLUDE_INTERNAL_SPANS=true -
-
to process internal spans for service edges.
-
-
-
@@ -804,4 +756,24 @@ code { font-weight: 600; color: #333; } + +.view-toggle-buttons { + :deep(.q-btn) { + padding-left: 1rem; + padding-right: 1rem; + } + + :deep(.q-btn-item) { + padding-left: 1rem; + padding-right: 1rem; + } + + :deep(.q-btn__content) { + gap: 0.5rem; + } + + :deep(.q-separator) { + margin: 0.25rem 0; + } +}