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 faf056464b Add operation context to related metrics
faf056464b is described below
commit faf056464b6b0b6e890c8a374f78662e21e5d336
Author: Logic <[email protected]>
AuthorDate: Mon Jun 8 10:55:03 2026 +0800
Add operation context to related metrics
---
.../dto/metrics/OtlpRelatedMetricsDto.java | 2 +
.../controller/OtlpIngestionController.java | 3 +-
.../service/OtlpIngestionWorkspaceService.java | 3 +-
.../impl/OtlpIngestionWorkspaceServiceImpl.java | 40 ++++++++++++++++----
.../controller/OtlpIngestionControllerTest.java | 7 +++-
.../OtlpIngestionWorkspaceServiceImplTest.java | 43 ++++++++++++++++++++++
web-next/app/log/manage/log-manage-page.tsx | 13 +++++--
web-next/app/log/manage/page.test.tsx | 11 +++++-
.../app/trace/manage/trace-manage-client.test.tsx | 18 ++++++++-
web-next/app/trace/manage/trace-manage-page.tsx | 3 +-
web-next/lib/log-manage/view-model.test.ts | 27 ++++++++++++++
web-next/lib/log-manage/view-model.ts | 7 ++++
web-next/lib/signal-route-context.ts | 1 +
web-next/lib/trace-manage/view-model.test.ts | 2 +
web-next/lib/trace-manage/view-model.ts | 2 +
web-next/lib/types.ts | 1 +
16 files changed, 164 insertions(+), 19 deletions(-)
diff --git
a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/metrics/OtlpRelatedMetricsDto.java
b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/metrics/OtlpRelatedMetricsDto.java
index b76bbfe0f3..081af7b232 100644
---
a/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/metrics/OtlpRelatedMetricsDto.java
+++
b/hertzbeat-common-core/src/main/java/org/apache/hertzbeat/common/observability/dto/metrics/OtlpRelatedMetricsDto.java
@@ -35,6 +35,8 @@ public class OtlpRelatedMetricsDto {
private String filter;
+ private String operationName;
+
private String source;
private int candidateCount;
diff --git
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionController.java
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionController.java
index eee0b2b9a4..e61671f9c8 100644
---
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionController.java
+++
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionController.java
@@ -107,8 +107,9 @@ public class OtlpIngestionController {
@RequestParam(value = "serviceNamespace", required = false) String
serviceNamespace,
@RequestParam(value = "environment", required = false) String
environment,
@RequestParam(value = "filter", required = false) String filter,
+ @RequestParam(value = "operationName", required = false) String
operationName,
@RequestParam(value = "limit", required = false) String limit) {
return
ResponseEntity.ok(Message.success(otlpIngestionWorkspaceService.getRelatedMetrics(
- entityId, entityType, start, end, serviceName,
serviceNamespace, environment, filter, limit)));
+ entityId, entityType, start, end, serviceName,
serviceNamespace, environment, filter, operationName, limit)));
}
}
diff --git
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/OtlpIngestionWorkspaceService.java
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/OtlpIngestionWorkspaceService.java
index f541f227c0..fc2eaf37d9 100644
---
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/OtlpIngestionWorkspaceService.java
+++
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/OtlpIngestionWorkspaceService.java
@@ -41,5 +41,6 @@ public interface OtlpIngestionWorkspaceService {
String temporalAggregation, String
step, String limit);
OtlpRelatedMetricsDto getRelatedMetrics(Long entityId, String entityType,
Long start, Long end, String serviceName,
- String serviceNamespace, String
environment, String filter, String limit);
+ String serviceNamespace, String
environment, String filter,
+ String operationName, String
limit);
}
diff --git
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java
index 72a3b7ea36..0ef6bf930c 100644
---
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java
+++
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImpl.java
@@ -778,7 +778,7 @@ public class OtlpIngestionWorkspaceServiceImpl implements
OtlpIngestionWorkspace
@Override
public OtlpRelatedMetricsDto getRelatedMetrics(Long entityId, String
entityType, Long start, Long end, String serviceName,
String serviceNamespace,
String environment,
- String filter, String
limit) {
+ String filter, String
operationName, String limit) {
long resolvedEnd = end == null || end <= 0 ?
System.currentTimeMillis() : end;
long resolvedStart = start == null || start <= 0 || start >=
resolvedEnd
? resolvedEnd - DEFAULT_CONSOLE_LOOKBACK_MILLIS
@@ -788,13 +788,15 @@ public class OtlpIngestionWorkspaceServiceImpl implements
OtlpIngestionWorkspace
entityId, entityType, resolvedStart, resolvedEnd, serviceName,
serviceNamespace, environment
);
String normalizedFilter = trimToNull(filter);
+ String normalizedOperationName = trimToNull(operationName);
List<OtlpRelatedMetricsDto.ResourceMatcher> resourceMatchers =
parseRelatedMetricResourceMatchers(normalizedFilter);
List<OtlpRelatedMetricsDto.Candidate> candidates =
buildRelatedMetricCandidates(
- context, resourceMatchers, resolvedLimit
+ context, resourceMatchers, normalizedOperationName,
resolvedLimit
);
return new OtlpRelatedMetricsDto(
context,
normalizedFilter,
+ normalizedOperationName,
"backend-related-metrics",
candidates.size(),
resourceMatchers,
@@ -805,12 +807,13 @@ public class OtlpIngestionWorkspaceServiceImpl implements
OtlpIngestionWorkspace
public OtlpRelatedMetricsDto getRelatedMetrics(Long entityId, Long start,
Long end, String serviceName,
String serviceNamespace,
String environment,
String filter, String
limit) {
- return getRelatedMetrics(entityId, null, start, end, serviceName,
serviceNamespace, environment, filter, limit);
+ return getRelatedMetrics(entityId, null, start, end, serviceName,
serviceNamespace, environment, filter, null, limit);
}
private List<OtlpRelatedMetricsDto.Candidate> buildRelatedMetricCandidates(
OtlpMetricsConsoleDto.Context context,
List<OtlpRelatedMetricsDto.ResourceMatcher> resourceMatchers,
+ String operationName,
int limit) {
LinkedHashMap<String, OtlpRelatedMetricsDto.Candidate> candidates =
new LinkedHashMap<>();
Map<String, String> resourceMatch =
resourceMatcherValueMap(resourceMatchers);
@@ -837,11 +840,15 @@ public class OtlpIngestionWorkspaceServiceImpl implements
OtlpIngestionWorkspace
addRelatedMetricCandidate(
candidates,
normalizePromqlMetricName(metricName),
- "service",
+ StringUtils.hasText(operationName) ? "operation" :
"service",
relatedMetricFamily(metricName),
- "service-context",
- serviceContextLabels(context),
- serviceContextResourceMatch(context)
+ StringUtils.hasText(operationName) ? "operation-context" :
"service-context",
+ StringUtils.hasText(operationName)
+ ? operationContextLabels(context, operationName)
+ : serviceContextLabels(context),
+ StringUtils.hasText(operationName)
+ ? operationContextResourceMatch(context,
operationName)
+ : serviceContextResourceMatch(context)
);
if (candidates.size() >= limit) {
return List.copyOf(candidates.values());
@@ -951,6 +958,15 @@ public class OtlpIngestionWorkspaceServiceImpl implements
OtlpIngestionWorkspace
return List.copyOf(labels);
}
+ private List<String> operationContextLabels(OtlpMetricsConsoleDto.Context
context, String operationName) {
+ List<String> labels = new ArrayList<>(serviceContextLabels(context));
+ if (StringUtils.hasText(operationName)) {
+ labels.add("operation_name");
+ labels.add("http_route");
+ }
+ return List.copyOf(new LinkedHashSet<>(labels));
+ }
+
private Map<String, String>
serviceContextResourceMatch(OtlpMetricsConsoleDto.Context context) {
if (context == null || !StringUtils.hasText(context.getServiceName()))
{
return Map.of();
@@ -972,6 +988,16 @@ public class OtlpIngestionWorkspaceServiceImpl implements
OtlpIngestionWorkspace
return values;
}
+ private Map<String, String>
operationContextResourceMatch(OtlpMetricsConsoleDto.Context context, String
operationName) {
+ LinkedHashMap<String, String> values = new
LinkedHashMap<>(serviceContextResourceMatch(context));
+ String normalizedOperationName = trimToNull(operationName);
+ if (StringUtils.hasText(normalizedOperationName)) {
+ values.put("operation_name", normalizedOperationName);
+ values.put("http_route", normalizedOperationName);
+ }
+ return values;
+ }
+
private String relatedMetricFamily(String metricName) {
String normalized = trimToNull(metricName);
if (!StringUtils.hasText(normalized)) {
diff --git
a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java
b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java
index 05a3a8064f..ecf3505c0a 100644
---
a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java
+++
b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/controller/OtlpIngestionControllerTest.java
@@ -266,6 +266,7 @@ class OtlpIngestionControllerTest {
new OtlpMetricsConsoleDto.Context(42L, "service", "Checkout
API", "checkout", "commerce", "prod",
1000L, 2000L),
"k8s.pod.name=\"checkout-7d9\"",
+ "POST /checkout",
"backend-related-metrics",
1,
List.of(new
OtlpRelatedMetricsDto.ResourceMatcher("k8s_pod_name", "=", "checkout-7d9")),
@@ -279,7 +280,7 @@ class OtlpIngestionControllerTest {
))
);
when(otlpIngestionWorkspaceService.getRelatedMetrics(42L, "service",
1000L, 2000L, "checkout", "commerce", "prod",
- "k8s.pod.name=\"checkout-7d9\"", "8")).thenReturn(related);
+ "k8s.pod.name=\"checkout-7d9\"", "POST /checkout",
"8")).thenReturn(related);
mockMvc.perform(get("/api/ingestion/otlp/metrics/related")
.param("entityId", "42")
@@ -290,11 +291,13 @@ class OtlpIngestionControllerTest {
.param("serviceNamespace", "commerce")
.param("environment", "prod")
.param("filter", "k8s.pod.name=\"checkout-7d9\"")
+ .param("operationName", "POST /checkout")
.param("limit", "8"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.context.entityId").value(42))
.andExpect(jsonPath("$.data.context.entityType").value("service"))
+ .andExpect(jsonPath("$.data.operationName").value("POST
/checkout"))
.andExpect(jsonPath("$.data.source").value("backend-related-metrics"))
.andExpect(jsonPath("$.data.resourceMatchers[0].label").value("k8s_pod_name"))
.andExpect(jsonPath("$.data.candidates[0].query").value("container.cpu.usage"))
@@ -302,6 +305,6 @@ class OtlpIngestionControllerTest {
verify(otlpIngestionWorkspaceService)
.getRelatedMetrics(42L, "service", 1000L, 2000L, "checkout",
"commerce", "prod",
- "k8s.pod.name=\"checkout-7d9\"", "8");
+ "k8s.pod.name=\"checkout-7d9\"", "POST /checkout",
"8");
}
}
diff --git
a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java
b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java
index d65f8c2368..eaa33fe3ff 100644
---
a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java
+++
b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/ingestion/service/impl/OtlpIngestionWorkspaceServiceImplTest.java
@@ -986,6 +986,7 @@ class OtlpIngestionWorkspaceServiceImplTest {
"commerce",
"prod",
"k8s.pod.name=\"checkout-7d9\" and host.name=\"node-a\"",
+ null,
"8"
);
@@ -1010,6 +1011,48 @@ class OtlpIngestionWorkspaceServiceImplTest {
assertEquals(related.getCandidates().size(),
related.getCandidateCount());
}
+ @Test
+ void relatedMetricsReturnsOperationScopedServiceCandidates() {
+ observabilitySignalIntakeGateway.recordOtlpMetricIntake(
+ Map.of(
+ "service.name", "checkout",
+ "service.namespace", "commerce",
+ "deployment.environment.name", "prod"
+ ),
+ 2_000L,
+ "http.server.duration",
+ "histogram",
+ "ms",
+ 14.0,
+ Map.of()
+ );
+
when(workspaceQueryGateway.findEntityById(42L)).thenReturn(java.util.Optional.empty());
+
when(workspaceQueryGateway.findIdentitiesByEntityId(42L)).thenReturn(List.of());
+
+ OtlpRelatedMetricsDto related =
otlpIngestionWorkspaceService.getRelatedMetrics(
+ 42L,
+ "service",
+ 1_000L,
+ 2_000L,
+ "checkout",
+ "commerce",
+ "prod",
+ "k8s.pod.name=\"checkout-7d9\"",
+ "POST /checkout",
+ "8"
+ );
+
+ assertEquals("POST /checkout", related.getOperationName());
+ assertTrue(related.getCandidates().stream().anyMatch(candidate ->
+ "http_server_duration".equals(candidate.getQuery())
+ && "operation".equals(candidate.getSource())
+ && "latency".equals(candidate.getFamily())
+ && "operation-context".equals(candidate.getReason())
+ &&
candidate.getMatchedLabels().contains("operation_name")
+ && "POST
/checkout".equals(candidate.getResourceMatch().get("operation_name"))
+ && "POST
/checkout".equals(candidate.getResourceMatch().get("http_route"))));
+ }
+
@Test
void metricsConsoleAppliesFilterWhenQueryIsExplicitMetricName() {
String expectedQuery =
groupedMetricPromql("__name__=\"http_server_request_duration_count\", "
diff --git a/web-next/app/log/manage/log-manage-page.tsx
b/web-next/app/log/manage/log-manage-page.tsx
index 7b985942b2..298b49affa 100644
--- a/web-next/app/log/manage/log-manage-page.tsx
+++ b/web-next/app/log/manage/log-manage-page.tsx
@@ -635,7 +635,7 @@ function buildLogMetricsPreviewApiUrl(metricsHref: string |
null | undefined, qu
return `/ingestion/otlp/metrics/console?${params.toString()}`;
}
-function buildLogMetricsRelatedApiUrl(metricsHref: string | null | undefined) {
+function buildLogMetricsRelatedApiUrl(metricsHref: string | null | undefined,
operationName?: string | null) {
if (!metricsHref) return null;
const href = new URL(metricsHref, 'http://localhost');
const sourceParams = href.searchParams;
@@ -651,11 +651,16 @@ function buildLogMetricsRelatedApiUrl(metricsHref: string
| null | undefined) {
'entityName',
'serviceName',
'serviceNamespace',
- 'environment'
+ 'environment',
+ 'operationName'
].forEach(key => {
const value = sourceParams.get(key)?.trim();
if (value) params.set(key, value);
});
+ const operationNameFallback = operationName?.trim();
+ if (!params.get('operationName') && operationNameFallback) {
+ params.set('operationName', operationNameFallback);
+ }
if (!params.get('serviceName') && !params.get('entityId')) return null;
params.set('limit', '8');
return `/ingestion/otlp/metrics/related?${params.toString()}`;
@@ -1677,8 +1682,8 @@ function LogManageExplorer({
[detailHandoffLinks.metricsHref, detailLog]
);
const detailMetricsRelatedUrl = useMemo(
- () => buildLogMetricsRelatedApiUrl(detailLog ?
detailHandoffLinks.metricsHref : null),
- [detailHandoffLinks.metricsHref, detailLog]
+ () => buildLogMetricsRelatedApiUrl(detailLog ?
detailHandoffLinks.metricsHref : null, routeContext.operationName),
+ [detailHandoffLinks.metricsHref, detailLog, routeContext.operationName]
);
const detailContextRows = useMemo(
() => buildLogDetailContextRows(detailContextState.data, detailLog, t),
diff --git a/web-next/app/log/manage/page.test.tsx
b/web-next/app/log/manage/page.test.tsx
index 10e84c1e3e..373aa28e19 100644
--- a/web-next/app/log/manage/page.test.tsx
+++ b/web-next/app/log/manage/page.test.tsx
@@ -556,7 +556,8 @@ function buildLogManageRouteState(): LogManageRouteState {
tz: mockState.searchParams.get('tz') || undefined,
source: mockState.searchParams.get('source') || undefined,
traceId: mockState.searchParams.get('traceId') || undefined,
- spanId: mockState.searchParams.get('spanId') || undefined
+ spanId: mockState.searchParams.get('spanId') || undefined,
+ operationName: mockState.searchParams.get('operationName') || undefined
},
shouldCleanUrl: Boolean(mockState.searchParams.get('returnLabel') ||
mockState.searchParams.get('returnTo')?.includes('returnLabel='))
};
@@ -2767,7 +2768,7 @@ describe('log manage page', () => {
});
it('uses backend related metrics candidates before static fallback targets
when the broad metrics query fails', async () => {
- mockState.searchParams = new
URLSearchParams('view=table&search=timeout&severityText=ERROR');
+ mockState.searchParams = new
URLSearchParams('view=table&search=timeout&severityText=ERROR&operationName=POST%20%2Fcheckout');
apiMessageGet.mockImplementation((path: string) => {
if (path.startsWith('/ingestion/otlp/metrics/related')) {
return Promise.resolve({
@@ -2866,6 +2867,10 @@ describe('log manage page', () => {
'k8s.node.name': 'node-a',
'k8s.container.name': 'checkout',
'host.name': 'node-a'
+ },
+ attributes: {
+ ...mockState.renderData.list.content[0].attributes,
+ 'http.route': 'POST /checkout'
}
}
]
@@ -2896,7 +2901,9 @@ describe('log manage page', () => {
const relatedCalls = apiMessageGet.mock.calls.filter(call =>
String(call[0]).startsWith('/ingestion/otlp/metrics/related'));
expect(relatedCalls).toHaveLength(1);
const relatedHref = decodeURIComponent(String(relatedCalls[0]?.[0] || ''));
+ const relatedParams = new URL(String(relatedCalls[0]?.[0] || ''),
'http://localhost').searchParams;
expect(relatedHref).toContain('serviceName=checkout');
+ expect(relatedParams.get('operationName')).toBe('POST /checkout');
expect(relatedHref).toContain('k8s.pod.name="checkout-7d9"');
expect(relatedHref).toContain('host.name="node-a"');
const metricsPreviewCalls = apiMessageGet.mock.calls.filter(call =>
String(call[0]).startsWith('/ingestion/otlp/metrics/console'));
diff --git a/web-next/app/trace/manage/trace-manage-client.test.tsx
b/web-next/app/trace/manage/trace-manage-client.test.tsx
index 15078e74d0..d741513c32 100644
--- a/web-next/app/trace/manage/trace-manage-client.test.tsx
+++ b/web-next/app/trace/manage/trace-manage-client.test.tsx
@@ -194,7 +194,8 @@ describe('TraceManagePage client loading', () => {
])
.mockResolvedValueOnce({
source: 'backend-related-metrics',
- candidateCount: 2,
+ operationName: 'POST /checkout',
+ candidateCount: 3,
candidates: [
{
query: 'container.cpu.usage',
@@ -211,6 +212,14 @@ describe('TraceManagePage client loading', () => {
reason: 'resource-filter',
matchedLabels: ['host_name'],
resourceMatch: { host_name: 'node-a' }
+ },
+ {
+ query: 'http.server.duration',
+ source: 'operation',
+ family: 'latency',
+ reason: 'operation-context',
+ matchedLabels: ['operation_name'],
+ resourceMatch: { operation_name: 'POST /checkout', http_route:
'POST /checkout' }
}
]
})
@@ -275,8 +284,10 @@ describe('TraceManagePage client loading', () => {
const relatedMetricsCall = apiMessageGet.mock.calls.find(call =>
String(call[0]).startsWith('/ingestion/otlp/metrics/related'));
expect(relatedMetricsCall).toBeTruthy();
const relatedMetricsHref =
decodeURIComponent(String(relatedMetricsCall?.[0] || ''));
+ const relatedMetricsParams = new URL(String(relatedMetricsCall?.[0] ||
''), 'http://localhost').searchParams;
expect(relatedMetricsHref).toContain('serviceName=checkout');
expect(relatedMetricsHref).toContain('serviceNamespace=payments');
+ expect(relatedMetricsParams.get('operationName')).toBe('POST /checkout');
expect(relatedMetricsHref).toContain('k8s.pod.name="checkout-7d9"');
expect(relatedMetricsHref).toContain('host.name="node-a"');
const relatedLogsCall = apiMessageGet.mock.calls.find(call =>
String(call[0]).startsWith('/logs/list'));
@@ -309,6 +320,7 @@ describe('TraceManagePage client loading', () => {
expect(relatedMetrics?.textContent).toContain('Related metrics');
expect(relatedMetrics?.textContent).toContain('container.cpu.usage');
expect(relatedMetrics?.textContent).toContain('system.cpu.utilization');
+ expect(relatedMetrics?.textContent).toContain('http.server.duration');
const metricAction =
relatedMetrics?.querySelector('[data-trace-manage-drawer-related-metric-query="container.cpu.usage"]')
as HTMLAnchorElement | null;
expect(metricAction?.getAttribute('href')).toContain('/ingestion/otlp/metrics?');
expect(metricAction?.getAttribute('href')).toContain('query=container.cpu.usage');
@@ -322,6 +334,10 @@ describe('TraceManagePage client loading', () => {
expect(metricActionParams.get('relatedMetricReason')).toBe('resource-filter');
expect(metricActionParams.get('relatedMetricMatchedLabels')).toBe('k8s_pod_name');
expect(metricActionParams.get('relatedMetricResourceMatch')).toBe('{"k8s_pod_name":"checkout-7d9"}');
+ const operationMetricAction =
relatedMetrics?.querySelector('[data-trace-manage-drawer-related-metric-query="http.server.duration"]')
as HTMLAnchorElement | null;
+
expect(operationMetricAction?.getAttribute('data-trace-manage-drawer-related-metric-source')).toBe('operation');
+
expect(operationMetricAction?.getAttribute('data-trace-manage-drawer-related-metric-reason')).toBe('operation-context');
+ expect(new URL(operationMetricAction?.getAttribute('href') || '',
'http://localhost').searchParams.get('operationName')).toBe('POST /checkout');
});
it('narrows trace table rows by service from the row cell', async () => {
diff --git a/web-next/app/trace/manage/trace-manage-page.tsx
b/web-next/app/trace/manage/trace-manage-page.tsx
index b8893f151e..ad5abb5d30 100644
--- a/web-next/app/trace/manage/trace-manage-page.tsx
+++ b/web-next/app/trace/manage/trace-manage-page.tsx
@@ -235,7 +235,8 @@ function buildTraceRelatedMetricsApiUrl(metricsHref: string
| null | undefined)
'entityName',
'serviceName',
'serviceNamespace',
- 'environment'
+ 'environment',
+ 'operationName'
].forEach(key => {
const value = sourceParams.get(key)?.trim();
if (value) params.set(key, value);
diff --git a/web-next/lib/log-manage/view-model.test.ts
b/web-next/lib/log-manage/view-model.test.ts
index cb8cf65115..0c7b4747c1 100644
--- a/web-next/lib/log-manage/view-model.test.ts
+++ b/web-next/lib/log-manage/view-model.test.ts
@@ -722,6 +722,33 @@ describe('log view model', () => {
expect(metricsParams.get('template')).toBe('hertzbeat-self');
});
+ it('keeps selected log operation context in metrics handoffs', () => {
+ const result = buildLogHandoffLinks(
+ {
+ traceId: 'trace-operation',
+ spanId: 'span-operation',
+ timeUnixNano: 1_710_000_000_000_000_000,
+ resource: {
+ 'service.name': 'checkout',
+ 'service.namespace': 'payments'
+ },
+ attributes: {
+ 'http.route': 'POST /checkout'
+ }
+ } as any,
+ {
+ entityId: '7',
+ entityType: 'service',
+ entityName: 'Checkout API'
+ }
+ );
+
+ const metricsParams = new URL(result.metricsHref,
'https://example.com').searchParams;
+ expect(metricsParams.get('operationName')).toBe('POST /checkout');
+ expect(metricsParams.get('serviceName')).toBe('checkout');
+ expect(metricsParams.get('entityType')).toBe('service');
+ });
+
it('can override trace and metrics return paths with the current log
workspace route', () => {
const currentLogReturnTo =
`/log/manage?traceId=trace-1&spanId=span-1&view=stream&start=1709999100000&end=1710000060000&returnTo=%2Foverview&returnLabel=${encodeURIComponent(t('menu.log.manage'))}`;
diff --git a/web-next/lib/log-manage/view-model.ts
b/web-next/lib/log-manage/view-model.ts
index 048223db6a..d3c6949aaa 100644
--- a/web-next/lib/log-manage/view-model.ts
+++ b/web-next/lib/log-manage/view-model.ts
@@ -705,6 +705,12 @@ export function buildLogHandoffLinks(
);
const traceId = firstText(selectedLog?.traceId, routeContext.traceId);
const spanId = firstText(selectedLog?.spanId, routeContext.spanId);
+ const operationName = firstText(
+ readAttribute(selectedLog?.attributes, 'operation.name'),
+ readAttribute(selectedLog?.attributes, 'span.name'),
+ readAttribute(selectedLog?.attributes, 'http.route'),
+ routeContext.operationName
+ );
const metricsFilter = buildLogMetricsResourceFilter(selectedLog);
const signalDraft = options?.alertDraft;
const signalContext: SignalRouteContext = {
@@ -750,6 +756,7 @@ export function buildLogHandoffLinks(
if (spanId) metricsParams.set('spanId', spanId);
if (serviceName) metricsParams.set('serviceName', serviceName);
if (serviceNamespace) metricsParams.set('serviceNamespace',
serviceNamespace);
+ if (operationName) metricsParams.set('operationName', operationName);
if (metricsFilter) metricsParams.set('filter', metricsFilter);
appendSignalRouteContext(metricsParams, metricsContext);
diff --git a/web-next/lib/signal-route-context.ts
b/web-next/lib/signal-route-context.ts
index d6fe9ce2b3..4f6b1f1ce5 100644
--- a/web-next/lib/signal-route-context.ts
+++ b/web-next/lib/signal-route-context.ts
@@ -32,6 +32,7 @@ export const SIGNAL_ROUTE_CONTEXT_PARAM_KEYS = [
'monitorInstance',
'traceId',
'spanId',
+ 'operationName',
'source',
'collector',
'template',
diff --git a/web-next/lib/trace-manage/view-model.test.ts
b/web-next/lib/trace-manage/view-model.test.ts
index f25462e8f4..436999721a 100644
--- a/web-next/lib/trace-manage/view-model.test.ts
+++ b/web-next/lib/trace-manage/view-model.test.ts
@@ -745,6 +745,7 @@ describe('trace view model', () => {
} as any,
{
spanId: 'span-db',
+ spanName: 'POST /checkout',
serviceName: 'checkout',
resourceAttributes: {
'deployment.environment.name': 'prod-east',
@@ -778,6 +779,7 @@ describe('trace view model', () => {
expect(metricsParams.get('entityName')).toBe('Checkout DB');
expect(metricsParams.get('collector')).toBe('collector-b');
expect(metricsParams.get('template')).toBe('postgres');
+ expect(metricsParams.get('operationName')).toBe('POST /checkout');
const entityHref = new URL(result.entityHref, 'https://example.com');
expect(entityHref.pathname).toBe('/entities/43');
diff --git a/web-next/lib/trace-manage/view-model.ts
b/web-next/lib/trace-manage/view-model.ts
index 9678d5f3c5..d78980e959 100644
--- a/web-next/lib/trace-manage/view-model.ts
+++ b/web-next/lib/trace-manage/view-model.ts
@@ -848,6 +848,7 @@ export function buildTraceHandoffLinks(
routeContext.template,
readTraceSignalAttribute(detail, selectedSpan, 'template',
'hertzbeat.template', 'hertzbeat_template', 'hertzbeat.monitor_template',
'hertzbeat_monitor_template')
);
+ const operationName = firstText(selectedSpan?.spanName,
detail?.rootSpanName, routeContext.operationName);
const metricsFilter = buildTraceMetricsResourceFilter(detail, selectedSpan);
const routeStart = readEpochMillisRouteParam(routeContext.start);
const routeEnd = readEpochMillisRouteParam(routeContext.end);
@@ -898,6 +899,7 @@ export function buildTraceHandoffLinks(
if (spanId) metricsParams.set('spanId', spanId);
if (serviceName) metricsParams.set('serviceName', serviceName);
if (serviceNamespace) metricsParams.set('serviceNamespace',
serviceNamespace);
+ if (operationName) metricsParams.set('operationName', operationName);
if (metricsFilter) metricsParams.set('filter', metricsFilter);
appendSignalRouteContext(metricsParams, metricsContext);
diff --git a/web-next/lib/types.ts b/web-next/lib/types.ts
index bda55f6916..a04f05c94d 100644
--- a/web-next/lib/types.ts
+++ b/web-next/lib/types.ts
@@ -375,6 +375,7 @@ export interface OtlpMetricsConsole {
export interface OtlpRelatedMetrics {
context?: OtlpMetricsConsole['context'];
filter?: string | null;
+ operationName?: string | null;
source?: string | null;
candidateCount?: number;
resourceMatchers?: Array<{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]