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 9d6bf8fed4 Add operation filters to metric log handoffs
9d6bf8fed4 is described below
commit 9d6bf8fed4ed0e077ecbda85c7ddf15abe492f55
Author: Logic <[email protected]>
AuthorDate: Wed Jun 10 09:11:20 2026 +0800
Add operation filters to metric log handoffs
---
web-next/lib/otlp-metrics/view-model.test.ts | 43 ++++++++++++++++++++++++++++
web-next/lib/otlp-metrics/view-model.ts | 30 +++++++++++++++++++
2 files changed, 73 insertions(+)
diff --git a/web-next/lib/otlp-metrics/view-model.test.ts
b/web-next/lib/otlp-metrics/view-model.test.ts
index 91448091ef..62d130848e 100644
--- a/web-next/lib/otlp-metrics/view-model.test.ts
+++ b/web-next/lib/otlp-metrics/view-model.test.ts
@@ -1109,6 +1109,7 @@ describe('otlp metrics view model', () => {
expect(logParams.get('traceId')).toBe('trace-series-42');
expect(logParams.get('spanId')).toBe('span-series-42');
expect(logParams.get('operationName')).toBe('/inventory/{id}');
+ expect(logParams.get('attributeFilter')).toBeNull();
expect(logParams.get('collector')).toBe('collector-b');
expect(logParams.get('template')).toBe('fastapi');
@@ -1141,6 +1142,48 @@ describe('otlp metrics view model', () => {
expect(alertRulesHref.searchParams.get('alertQuery')).toContain('operationName=/inventory/{id}');
});
+ it('adds an executable log attribute filter for operation-level metric
handoffs', () => {
+ const result = buildMetricsHandoffLinks(
+ {
+ context: {
+ serviceName: 'checkout',
+ serviceNamespace: 'payments',
+ environment: 'prod',
+ start: 1000,
+ end: 2000
+ }
+ } as any,
+ {
+ query: 'http_server_duration_milliseconds_count',
+ serviceName: 'checkout',
+ serviceNamespace: 'payments',
+ environment: 'prod'
+ },
+ { source: 'otlp' },
+ {
+ key: 'http-server-duration-checkout',
+ name: 'http.server.duration',
+ labels: {
+ __name__: 'http.server.duration',
+ 'service.name': 'checkout',
+ 'service.namespace': 'payments',
+ 'deployment.environment.name': 'prod',
+ http_route: '/checkout/:id'
+ },
+ points: [[2000, 18]],
+ latestValue: 18
+ }
+ );
+
+ const logParams = new URL(result.logsHref,
'https://example.com').searchParams;
+ expect(logParams.get('traceId')).toBeNull();
+ expect(logParams.get('spanId')).toBeNull();
+ expect(logParams.get('serviceName')).toBe('checkout');
+ expect(logParams.get('serviceNamespace')).toBe('payments');
+ expect(logParams.get('operationName')).toBe('/checkout/:id');
+
expect(logParams.get('attributeFilter')).toBe('http.route="/checkout/:id"');
+ });
+
it('opens trace-linked metric logs as history records without an extra text
search filter', () => {
const result = buildMetricsHandoffLinks(
{
diff --git a/web-next/lib/otlp-metrics/view-model.ts
b/web-next/lib/otlp-metrics/view-model.ts
index f1d03d7d84..17c44ce0c5 100644
--- a/web-next/lib/otlp-metrics/view-model.ts
+++ b/web-next/lib/otlp-metrics/view-model.ts
@@ -184,6 +184,17 @@ function readSeriesLabel(series: OtlpMetricSeriesView |
null | undefined, ...key
return firstText(...keys.map(key => series.labels[key]));
}
+function escapeLogAttributeFilterValue(value: string) {
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+}
+
+function buildLogAttributeFilterExpression(name: string, value: string |
undefined) {
+ const trimmedName = name.trim();
+ const trimmedValue = value?.trim();
+ if (!/^[A-Za-z0-9_.:-]+$/.test(trimmedName) || !trimmedValue || trimmedValue
=== '-') return undefined;
+ return `${trimmedName}="${escapeLogAttributeFilterValue(trimmedValue)}"`;
+}
+
function buildMetricSeriesSignalContext(series: OtlpMetricSeriesView | null |
undefined): SignalRouteContext {
if (!series) return {};
return {
@@ -201,6 +212,18 @@ function buildMetricSeriesSignalContext(series:
OtlpMetricSeriesView | null | un
};
}
+function buildMetricsLogsOperationAttributeFilter(
+ selectedSeries: OtlpMetricSeriesView | null | undefined,
+ operationName: string | undefined,
+ traceId: string | undefined,
+ spanId: string | undefined
+) {
+ if (traceId || spanId) return undefined;
+ const httpRoute = readSeriesLabel(selectedSeries, 'http.route',
'http_route');
+ if (httpRoute) return buildLogAttributeFilterExpression('http.route',
httpRoute);
+ return buildLogAttributeFilterExpression('span.name', operationName);
+}
+
export function buildConsoleFacts(
data: OtlpMetricsConsole,
t: Translator,
@@ -1005,6 +1028,13 @@ export function buildMetricsHandoffLinks(
if (traceId) logParams.set('traceId', traceId);
if (spanId) logParams.set('spanId', spanId);
appendSignalRouteContext(logParams, signalContext);
+ const logOperationAttributeFilter = buildMetricsLogsOperationAttributeFilter(
+ selectedSeries,
+ operationName,
+ traceId,
+ spanId
+ );
+ if (logOperationAttributeFilter) logParams.set('attributeFilter',
logOperationAttributeFilter);
const traceParams = new URLSearchParams();
if (traceId) traceParams.set('traceId', traceId);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]