This is an automated email from the ASF dual-hosted git repository.

zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/2.0.0 by this push:
     new 1060b5a914 Preserve metric route context in signal handoffs
1060b5a914 is described below

commit 1060b5a91401d3a7ccb10adcefcf37a06032c8fa
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 19:22:24 2026 +0800

    Preserve metric route context in signal handoffs
---
 web-next/lib/signal-dashboards.test.ts | 23 +++++++++-
 web-next/lib/signal-dashboards.ts      | 80 ++++++++++++++++++++++++++++++----
 2 files changed, 93 insertions(+), 10 deletions(-)

diff --git a/web-next/lib/signal-dashboards.test.ts 
b/web-next/lib/signal-dashboards.test.ts
index 82900ff971..410257aeed 100644
--- a/web-next/lib/signal-dashboards.test.ts
+++ b/web-next/lib/signal-dashboards.test.ts
@@ -1828,7 +1828,7 @@ describe('signal dashboards API client', () => {
         { id: 'metrics-panel', signal: 'metrics', title: 'Metrics', 
visualization: 'time-series', route: '/ingestion/otlp/metrics?query=cpu' },
         { id: 'db-metrics-panel', signal: 'metrics', title: 'DB Metrics', 
visualization: 'time-series', route: 
'/ingestion/otlp/metrics?query=signoz_db_latency_count&serviceName=checkout&groupBy=db.system'
 },
         { id: 'external-metrics-panel', signal: 'metrics', title: 'External 
Metrics', visualization: 'time-series', route: 
'/ingestion/otlp/metrics?query=signoz_external_call_latency_count&serviceName=checkout&groupBy=external.service.address'
 },
-        { id: 'operation-metrics-panel', signal: 'metrics', title: 'Service 
key operations', visualization: 'time-series', route: 
'/ingestion/otlp/metrics?query=http.server.duration&serviceName=checkout&serviceNamespace=payments&groupBy=operation'
 }
+        { id: 'operation-metrics-panel', signal: 'metrics', title: 'Service 
key operations', visualization: 'time-series', route: 
'/ingestion/otlp/metrics?query=http.server.duration&serviceName=checkout&serviceNamespace=payments&environment=prod&entityId=4200&entityType=service&entityName=Checkout+API&source=otlp&collector=collector-a&template=spring-boot&groupBy=operation'
 }
       ])
     });
     const logsDescriptor = 
buildSignalDashboardPanelRuntimeRenderDescriptor(logsPlan, {
@@ -2061,7 +2061,7 @@ describe('signal dashboards API client', () => {
           serviceNamespace: 'payments',
           operationName: 'POST /checkout',
           relatedSignal: 'traces',
-          relatedHandoffHref: 
'/trace/manage?view=list&spanScope=all&serviceName=checkout&serviceNamespace=payments&operationName=POST+%2Fcheckout&returnTo=%2Fdashboard%3Fstart%3D1000%26end%3D3000&start=1000&end=3000'
+          relatedHandoffHref: 
'/trace/manage?view=list&spanScope=all&serviceName=checkout&serviceNamespace=payments&operationName=POST+%2Fcheckout&environment=prod&entityId=4200&entityType=service&entityName=Checkout+API&source=otlp&collector=collector-a&template=spring-boot&returnTo=%2Fdashboard%3Fstart%3D1000%26end%3D3000&start=1000&end=3000'
         })
       ]
     });
@@ -2176,6 +2176,21 @@ describe('signal dashboards API client', () => {
         expect.objectContaining({ name: 'external.service.address', value: 
'payments.internal' })
       ]
     }));
+    expect(syncTooltip.rows[6]).toEqual(expect.objectContaining({
+      operationName: 'POST /checkout',
+      breakoutAttributes: expect.arrayContaining([
+        expect.objectContaining({ name: 'service.name', value: 'checkout' }),
+        expect.objectContaining({ name: 'service.namespace', value: 'payments' 
}),
+        expect.objectContaining({ name: 'deployment.environment.name', value: 
'prod' }),
+        expect.objectContaining({ name: 'hertzbeat.entity_id', value: '4200' 
}),
+        expect.objectContaining({ name: 'hertzbeat.entity_type', value: 
'service' }),
+        expect.objectContaining({ name: 'hertzbeat.entity_name', value: 
'Checkout API' }),
+        expect.objectContaining({ name: 'hertzbeat.source', value: 'otlp' }),
+        expect.objectContaining({ name: 'hertzbeat.collector', value: 
'collector-a' }),
+        expect.objectContaining({ name: 'hertzbeat.template', value: 
'spring-boot' }),
+        expect.objectContaining({ name: 'operation', value: 'POST /checkout' })
+      ])
+    }));
     
expect(buildSignalDashboardRuntimeEvidenceSourceHandoff('/log/manage?view=table',
 syncTooltip.rows[0], {
       timeRange: { start: '1000', end: '3000' },
       returnTo: '/dashboard?start=1000&end=3000'
@@ -2188,6 +2203,10 @@ describe('signal dashboards API client', () => {
       timeRange: { start: '1000', end: '3000' },
       returnTo: '/dashboard?start=1000&end=3000'
     
})).toBe('/trace/manage?serviceName=payments&traceId=trace-1&spanId=span-root&serviceNamespace=payments&environment=prod&entityId=4200&entityType=service&entityName=Checkout+API&source=otlp&collector=collector-a&template=spring-boot&returnTo=%2Fdashboard%3Fstart%3D1000%26end%3D3000&start=1000&end=3000');
+    
expect(buildSignalDashboardRuntimeEvidenceSourceHandoff('/trace/manage?view=list',
 syncTooltip.rows[6], {
+      timeRange: { start: '1000', end: '3000' },
+      returnTo: '/dashboard?start=1000&end=3000'
+    
})).toBe('/trace/manage?view=list&serviceName=checkout&serviceNamespace=payments&environment=prod&entityId=4200&entityType=service&entityName=Checkout+API&source=otlp&collector=collector-a&template=spring-boot&returnTo=%2Fdashboard%3Fstart%3D1000%26end%3D3000&start=1000&end=3000');
     
expect(buildSignalDashboardRuntimeEvidenceSourceHandoff('/ingestion/otlp/metrics?query=cpu.usage',
 syncTooltip.rows[3], {
       timeRange: { start: '1000', end: '3000' },
       returnTo: '/dashboard?start=1000&end=3000'
diff --git a/web-next/lib/signal-dashboards.ts 
b/web-next/lib/signal-dashboards.ts
index 2f7ecf1b60..64f7d5ba3f 100644
--- a/web-next/lib/signal-dashboards.ts
+++ b/web-next/lib/signal-dashboards.ts
@@ -232,6 +232,7 @@ export type SignalDashboardPanelRuntimeRenderDescriptor = {
   signal: string;
   visualization: string;
   state: SignalDashboardPanelExecutionResult['state'];
+  primaryUrl?: string;
   kind: SignalDashboardPanelRuntimeSummary['kind'];
   mode: SignalDashboardPanelRuntimePreview['mode'];
   itemCount: number;
@@ -3312,6 +3313,7 @@ export function 
buildSignalDashboardPanelRuntimeRenderDescriptor(
     signal: summary.signal,
     visualization: summary.visualization,
     state: summary.state,
+    primaryUrl: result?.primaryUrl || plan.primaryUrl,
     kind: summary.kind,
     mode: preview.mode,
     itemCount: summary.itemCount,
@@ -3465,6 +3467,53 @@ function metricHertzBeatHandoffContext(labels: 
Record<string, string>) {
   };
 }
 
+function metricRouteHandoffContext(route: string | undefined) {
+  const normalizedRoute = route?.trim();
+  const emptyContext = {
+    serviceName: '',
+    serviceNamespace: '',
+    environment: '',
+    entityId: '',
+    entityType: '',
+    entityName: '',
+    source: '',
+    collector: '',
+    template: ''
+  };
+  if (!normalizedRoute) return emptyContext;
+  try {
+    const params = new URL(normalizedRoute, 
'http://hertzbeat.local').searchParams;
+    return {
+      serviceName: syncTooltipIdentifier(params.get('serviceName') || 
undefined),
+      serviceNamespace: syncTooltipIdentifier(params.get('serviceNamespace') 
|| undefined),
+      environment: syncTooltipIdentifier(params.get('environment') || 
undefined),
+      entityId: syncTooltipIdentifier(params.get('entityId') || undefined),
+      entityType: syncTooltipIdentifier(params.get('entityType') || undefined),
+      entityName: syncTooltipIdentifier(params.get('entityName') || undefined),
+      source: syncTooltipIdentifier(params.get('source') || undefined),
+      collector: syncTooltipIdentifier(params.get('collector') || undefined),
+      template: syncTooltipIdentifier(params.get('template') || undefined)
+    };
+  } catch {
+    return emptyContext;
+  }
+}
+
+function metricRouteBreakoutLabels(route: string | undefined): Record<string, 
string> {
+  const context = metricRouteHandoffContext(route);
+  return Object.fromEntries([
+    ['service.name', context.serviceName],
+    ['service.namespace', context.serviceNamespace],
+    ['deployment.environment.name', context.environment],
+    ['hertzbeat.entity_id', context.entityId],
+    ['hertzbeat.entity_type', context.entityType],
+    ['hertzbeat.entity_name', context.entityName],
+    ['hertzbeat.source', context.source],
+    ['hertzbeat.collector', context.collector],
+    ['hertzbeat.template', context.template]
+  ].filter((entry): entry is [string, string] => Boolean(entry[1])));
+}
+
 const METRIC_BREAKOUT_ATTRIBUTE_PRIORITY = [
   'service.name',
   'service.namespace',
@@ -3486,17 +3535,21 @@ const METRIC_BREAKOUT_ATTRIBUTE_PRIORITY = [
   'net.peer.name'
 ];
 
-function metricBreakoutAttributes(labels: Record<string, string>): 
SignalDashboardRuntimeBreakoutAttribute[] {
+function metricBreakoutAttributes(labels: Record<string, string>, route: 
string | undefined): SignalDashboardRuntimeBreakoutAttribute[] {
+  const mergedLabels = {
+    ...metricRouteBreakoutLabels(route),
+    ...labels
+  };
   const seen = new Set<string>();
   const orderedNames = [
     ...METRIC_BREAKOUT_ATTRIBUTE_PRIORITY,
-    ...Object.keys(labels).sort()
+    ...Object.keys(mergedLabels).sort()
   ];
   return orderedNames.flatMap(name => {
     const attributeName = normalizeBreakoutAttributeName(name);
     if (!attributeName || attributeName === '__name__' || 
seen.has(attributeName)) return [];
     seen.add(attributeName);
-    const value = syncTooltipIdentifier(labels[name]);
+    const value = syncTooltipIdentifier(mergedLabels[name]);
     if (!value) return [];
     return [{
       key: `${attributeName}:${value}`,
@@ -3508,14 +3561,25 @@ function metricBreakoutAttributes(labels: 
Record<string, string>): SignalDashboa
 
 function syncTooltipMetricRelatedHandoff(
   labels: Record<string, string>,
+  route: string | undefined,
   options: SignalDashboardRuntimeSyncTooltipOptions = {}
 ) {
-  const serviceName = metricLabelValue(labels, ['service.name', 'serviceName', 
'service_name', 'service']);
-  const serviceNamespace = metricLabelValue(labels, ['service.namespace', 
'serviceNamespace', 'service_namespace']);
+  const routeContext = metricRouteHandoffContext(route);
+  const serviceName = metricLabelValue(labels, ['service.name', 'serviceName', 
'service_name', 'service']) || routeContext.serviceName;
+  const serviceNamespace = metricLabelValue(labels, ['service.namespace', 
'serviceNamespace', 'service_namespace']) || routeContext.serviceNamespace;
   const dbSystem = metricLabelValue(labels, ['db.system', 'dbSystem', 
'db_system']);
   const externalAddress = metricLabelValue(labels, 
['external.service.address', 'externalServiceAddress', 
'external_service_address', 'server.address', 'net.peer.name']);
   const operationName = metricOperationName(labels);
-  const hertzBeatContext = metricHertzBeatHandoffContext(labels);
+  const labelContext = metricHertzBeatHandoffContext(labels);
+  const hertzBeatContext = {
+    environment: labelContext.environment || routeContext.environment,
+    entityId: labelContext.entityId || routeContext.entityId,
+    entityType: labelContext.entityType || routeContext.entityType,
+    entityName: labelContext.entityName || routeContext.entityName,
+    source: labelContext.source || routeContext.source,
+    collector: labelContext.collector || routeContext.collector,
+    template: labelContext.template || routeContext.template
+  };
   const resourceFilter = dbSystem
     ? metricResourceFilter('db.system', dbSystem)
     : externalAddress ? metricResourceFilter('external.service.address', 
externalAddress) : '';
@@ -3720,8 +3784,8 @@ export function buildSignalDashboardRuntimeSyncTooltip(
           label: series.label,
           value: String(point.value),
           meta: String(point.timestamp),
-          breakoutAttributes: metricBreakoutAttributes(series.labels),
-          ...syncTooltipMetricRelatedHandoff(series.labels, options)
+          breakoutAttributes: metricBreakoutAttributes(series.labels, 
renderer.primaryUrl),
+          ...syncTooltipMetricRelatedHandoff(series.labels, 
renderer.primaryUrl, options)
         }));
     }) || [];
     const tableRows = renderer.tableRows


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to