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]

Reply via email to