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]