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 1d3d4e3c8f Filter metrics console by operation context
1d3d4e3c8f is described below

commit 1d3d4e3c8fb91b0d1a9fc98d02321d725a4cb8e8
Author: Logic <[email protected]>
AuthorDate: Wed Jun 10 08:23:11 2026 +0800

    Filter metrics console by operation context
---
 .../controller/OtlpIngestionController.java        |   5 +-
 .../service/OtlpIngestionWorkspaceService.java     |   3 +-
 .../impl/OtlpIngestionWorkspaceServiceImpl.java    | 213 ++++++++++++++++-----
 .../controller/OtlpIngestionControllerTest.java    |   7 +-
 .../OtlpIngestionWorkspaceServiceImplTest.java     |  86 +++++++++
 .../app/ingestion/otlp/metrics/route-state.test.ts |   3 +-
 web-next/app/ingestion/otlp/metrics/route-state.ts |   1 +
 web-next/lib/otlp-metrics/controller.test.ts       |  12 +-
 web-next/lib/otlp-metrics/controller.ts            |   1 +
 9 files changed, 271 insertions(+), 60 deletions(-)

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 bfc2f34217..fa1cdb9b39 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
@@ -91,10 +91,11 @@ public class OtlpIngestionController {
             @RequestParam(value = "aggregation", required = false) String 
aggregation,
             @RequestParam(value = "temporalAggregation", required = false) 
String temporalAggregation,
             @RequestParam(value = "step", required = false) String step,
-            @RequestParam(value = "limit", required = false) String limit) {
+            @RequestParam(value = "limit", required = false) String limit,
+            @RequestParam(value = "operationName", required = false) String 
operationName) {
         return 
ResponseEntity.ok(Message.success(otlpIngestionWorkspaceService.getMetricsConsole(
                 entityId, entityType, start, end, serviceName, 
serviceNamespace, environment, query, filter, groupBy, aggregation,
-                temporalAggregation, step, limit)));
+                temporalAggregation, step, limit, operationName)));
     }
 
     @GetMapping("/metrics/inventory")
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 37ea8d7491..6066eb9531 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
@@ -39,7 +39,8 @@ public interface OtlpIngestionWorkspaceService {
     OtlpMetricsConsoleDto getMetricsConsole(Long entityId, String entityType, 
Long start, Long end, String serviceName,
                                             String serviceNamespace, String 
environment, String query,
                                             String filter, String groupBy, 
String aggregation,
-                                            String temporalAggregation, String 
step, String limit);
+                                            String temporalAggregation, String 
step, String limit,
+                                            String operationName);
 
     OtlpMetricsInventoryDto getMetricsInventory(Long entityId, String 
entityType, Long start, Long end,
                                                 String serviceName, String 
serviceNamespace, String environment,
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 99ba05223e..94214dd978 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
@@ -681,11 +681,20 @@ public class OtlpIngestionWorkspaceServiceImpl implements 
OtlpIngestionWorkspace
         );
     }
 
-    @Override
     public OtlpMetricsConsoleDto getMetricsConsole(Long entityId, String 
entityType, Long start, Long end,
                                                    String serviceName, String 
serviceNamespace, String environment,
                                                    String query, String 
filter, String groupBy, String aggregation,
                                                    String temporalAggregation, 
String step, String limit) {
+        return getMetricsConsole(entityId, entityType, start, end, 
serviceName, serviceNamespace, environment, query,
+                filter, groupBy, aggregation, temporalAggregation, step, 
limit, null);
+    }
+
+    @Override
+    public OtlpMetricsConsoleDto getMetricsConsole(Long entityId, String 
entityType, Long start, Long end,
+                                                   String serviceName, String 
serviceNamespace, String environment,
+                                                   String query, String 
filter, String groupBy, String aggregation,
+                                                   String temporalAggregation, 
String step, String limit,
+                                                   String operationName) {
         long resolvedEnd = end == null || end <= 0 ? 
System.currentTimeMillis() : end;
         long resolvedStart = start == null || start <= 0 || start >= 
resolvedEnd
                 ? resolvedEnd - DEFAULT_CONSOLE_LOOKBACK_MILLIS
@@ -701,6 +710,8 @@ public class OtlpIngestionWorkspaceServiceImpl implements 
OtlpIngestionWorkspace
                 entityId, entityType, resolvedStart, resolvedEnd, serviceName, 
serviceNamespace, environment
         );
         String resolvedQuery = trimToNull(query);
+        String normalizedOperationName = trimToNull(operationName);
+        List<String> resolvedQueries;
         if (!StringUtils.hasText(resolvedQuery)) {
             OtlpMetricsConsoleDto autoResolvedConsole = 
queryDefaultMetricsConsole(
                     context,
@@ -712,17 +723,19 @@ public class OtlpIngestionWorkspaceServiceImpl implements 
OtlpIngestionWorkspace
                     resolvedStart,
                     resolvedEnd,
                     resolvedStep,
-                    resolvedSeriesLimit
+                    resolvedSeriesLimit,
+                    normalizedOperationName
             );
             if (autoResolvedConsole != null) {
                 return autoResolvedConsole;
             }
-            resolvedQuery = buildDefaultMetricsQuery(context, filter, groupBy, 
aggregation, temporalAggregation);
+            resolvedQueries = buildDefaultMetricsQueries(context, filter, 
groupBy, aggregation, temporalAggregation,
+                    normalizedOperationName);
         } else {
-            resolvedQuery = buildMetricsQueryForExplicitMetric(context, 
resolvedQuery, filter, groupBy, aggregation,
-                    temporalAggregation);
+            resolvedQueries = buildMetricsQueriesForExplicitMetric(context, 
resolvedQuery, filter, groupBy, aggregation,
+                    temporalAggregation, normalizedOperationName);
         }
-        if (!StringUtils.hasText(resolvedQuery)) {
+        if (CollectionUtils.isEmpty(resolvedQueries)) {
             return new OtlpMetricsConsoleDto(
                     context,
                     null,
@@ -734,6 +747,7 @@ public class OtlpIngestionWorkspaceServiceImpl implements 
OtlpIngestionWorkspace
                     message("observability.otlp.metrics-console.no-context")
             );
         }
+        resolvedQuery = resolvedQueries.getFirst();
         if (!metricQueryRepository.hasPromqlExecutor()) {
             return new OtlpMetricsConsoleDto(
                     context,
@@ -746,29 +760,54 @@ public class OtlpIngestionWorkspaceServiceImpl implements 
OtlpIngestionWorkspace
                     
message("observability.otlp.metrics-console.promql-unavailable")
             );
         }
-        MetricsQueryExecution execution = 
executeMetricsConsoleQuery(resolvedQuery, resolvedStart, resolvedEnd,
-                resolvedStep, resolvedSeriesLimit);
-        if (execution.errorMessage() != null) {
+        MetricsQueryExecution firstEmptyExecution = null;
+        String firstEmptyQuery = null;
+        String lastErrorMessage = null;
+        for (String candidateQuery : resolvedQueries) {
+            MetricsQueryExecution execution = 
executeMetricsConsoleQuery(candidateQuery, resolvedStart, resolvedEnd,
+                    resolvedStep, resolvedSeriesLimit);
+            if (execution.errorMessage() != null) {
+                lastErrorMessage = execution.errorMessage();
+                continue;
+            }
+            if (execution.stats().getNonEmptySeries() > 0) {
+                return new OtlpMetricsConsoleDto(
+                        context,
+                        candidateQuery,
+                        execution.datasource(),
+                        WarehouseConstants.PROMQL,
+                        execution.results(),
+                        execution.stats(),
+                        deriveMetricsEmptyStateReason(execution.results(), 
execution.stats()),
+                        execution.results() == null ? null : 
trimToNull(execution.results().getMsg())
+                );
+            }
+            if (firstEmptyExecution == null) {
+                firstEmptyExecution = execution;
+                firstEmptyQuery = candidateQuery;
+            }
+        }
+        if (firstEmptyExecution == null) {
             return new OtlpMetricsConsoleDto(
                     context,
                     resolvedQuery,
-                    execution.datasource(),
+                    null,
                     WarehouseConstants.PROMQL,
                     null,
                     new OtlpMetricsConsoleDto.Stats(0, 0, null),
                     "load_failed",
-                    execution.errorMessage()
+                    lastErrorMessage
             );
         }
         return new OtlpMetricsConsoleDto(
                 context,
-                resolvedQuery,
-                execution.datasource(),
+                firstEmptyQuery,
+                firstEmptyExecution.datasource(),
                 WarehouseConstants.PROMQL,
-                execution.results(),
-                execution.stats(),
-                deriveMetricsEmptyStateReason(execution.results(), 
execution.stats()),
-                execution.results() == null ? null : 
trimToNull(execution.results().getMsg())
+                firstEmptyExecution.results(),
+                firstEmptyExecution.stats(),
+                deriveMetricsEmptyStateReason(firstEmptyExecution.results(), 
firstEmptyExecution.stats()),
+                firstEmptyExecution.results() == null ? null : 
trimToNull(firstEmptyExecution.results().getMsg())
         );
     }
 
@@ -1280,6 +1319,10 @@ public class OtlpIngestionWorkspaceServiceImpl 
implements OtlpIngestionWorkspace
         return values;
     }
 
+    private List<String> metricsOperationLabels(String operationName) {
+        return StringUtils.hasText(trimToNull(operationName)) ? 
List.of("operation_name", "http_route") : List.of();
+    }
+
     private String relatedMetricFamily(String metricName) {
         String normalized = trimToNull(metricName);
         if (!StringUtils.hasText(normalized)) {
@@ -1908,7 +1951,8 @@ public class OtlpIngestionWorkspaceServiceImpl implements 
OtlpIngestionWorkspace
                                                              long 
resolvedStart,
                                                              long resolvedEnd,
                                                              String 
resolvedStep,
-                                                             int 
resolvedSeriesLimit) {
+                                                             int 
resolvedSeriesLimit,
+                                                             String 
operationName) {
         if (!metricQueryRepository.hasPromqlExecutor()) {
             return null;
         }
@@ -1932,32 +1976,30 @@ public class OtlpIngestionWorkspaceServiceImpl 
implements OtlpIngestionWorkspace
         String lastErrorMessage = null;
         for (OtlpMetricsConsoleDto.Context candidateContext : 
candidateContexts) {
             for (String metricName : candidateMetricNames(candidateContext)) {
-                String candidateQuery = 
buildMetricsQueryForMetric(candidateContext, metricName, filter, groupBy,
-                        aggregation, temporalAggregation);
-                if (!StringUtils.hasText(candidateQuery)) {
-                    continue;
-                }
-                MetricsQueryExecution execution = 
executeMetricsConsoleQuery(candidateQuery, resolvedStart,
-                        resolvedEnd, resolvedStep, resolvedSeriesLimit);
-                if (execution.errorMessage() != null) {
-                    lastErrorMessage = execution.errorMessage();
-                    continue;
-                }
-                OtlpMetricsConsoleDto console = new OtlpMetricsConsoleDto(
-                        candidateContext,
-                        candidateQuery,
-                        execution.datasource(),
-                        WarehouseConstants.PROMQL,
-                        execution.results(),
-                        execution.stats(),
-                        deriveMetricsEmptyStateReason(execution.results(), 
execution.stats()),
-                        execution.results() == null ? null : 
trimToNull(execution.results().getMsg())
-                );
-                if (execution.stats().getNonEmptySeries() > 0) {
-                    return console;
-                }
-                if (firstEmptyConsole == null) {
-                    firstEmptyConsole = console;
+                for (String candidateQuery : 
buildMetricsQueriesForMetric(candidateContext, metricName, filter, groupBy,
+                        aggregation, temporalAggregation, operationName)) {
+                    MetricsQueryExecution execution = 
executeMetricsConsoleQuery(candidateQuery, resolvedStart,
+                            resolvedEnd, resolvedStep, resolvedSeriesLimit);
+                    if (execution.errorMessage() != null) {
+                        lastErrorMessage = execution.errorMessage();
+                        continue;
+                    }
+                    OtlpMetricsConsoleDto console = new OtlpMetricsConsoleDto(
+                            candidateContext,
+                            candidateQuery,
+                            execution.datasource(),
+                            WarehouseConstants.PROMQL,
+                            execution.results(),
+                            execution.stats(),
+                            deriveMetricsEmptyStateReason(execution.results(), 
execution.stats()),
+                            execution.results() == null ? null : 
trimToNull(execution.results().getMsg())
+                    );
+                    if (execution.stats().getNonEmptySeries() > 0) {
+                        return console;
+                    }
+                    if (firstEmptyConsole == null) {
+                        firstEmptyConsole = console;
+                    }
                 }
             }
         }
@@ -1984,15 +2026,26 @@ public class OtlpIngestionWorkspaceServiceImpl 
implements OtlpIngestionWorkspace
                                             String groupBy,
                                             String aggregation,
                                             String temporalAggregation) {
+        return buildDefaultMetricsQueries(context, filter, groupBy, 
aggregation, temporalAggregation, null).stream()
+                .findFirst()
+                .orElse(null);
+    }
+
+    private List<String> 
buildDefaultMetricsQueries(OtlpMetricsConsoleDto.Context context,
+                                                    String filter,
+                                                    String groupBy,
+                                                    String aggregation,
+                                                    String temporalAggregation,
+                                                    String operationName) {
         if (context == null || !StringUtils.hasText(context.getServiceName())) 
{
-            return null;
+            return List.of();
         }
         String preferredMetricName = 
candidateMetricNames(context).stream().findFirst().orElse(null);
         if (!StringUtils.hasText(preferredMetricName)) {
-            return null;
+            return List.of();
         }
-        return buildMetricsQueryForMetric(context, preferredMetricName, 
filter, groupBy, aggregation,
-                temporalAggregation);
+        return buildMetricsQueriesForMetric(context, preferredMetricName, 
filter, groupBy, aggregation,
+                temporalAggregation, operationName);
     }
 
     private List<String> candidateMetricNames(OtlpMetricsConsoleDto.Context 
context) {
@@ -2051,6 +2104,44 @@ public class OtlpIngestionWorkspaceServiceImpl 
implements OtlpIngestionWorkspace
                                               String groupBy,
                                               String aggregation,
                                               String temporalAggregation) {
+        return buildMetricsQueriesForMetric(context, metricName, filter, 
groupBy, aggregation, temporalAggregation, null)
+                .stream()
+                .findFirst()
+                .orElse(null);
+    }
+
+    private List<String> 
buildMetricsQueriesForMetric(OtlpMetricsConsoleDto.Context context,
+                                                      String metricName,
+                                                      String filter,
+                                                      String groupBy,
+                                                      String aggregation,
+                                                      String 
temporalAggregation,
+                                                      String operationName) {
+        List<String> operationLabels = metricsOperationLabels(operationName);
+        if (operationLabels.isEmpty()) {
+            String query = buildMetricsQueryForMetric(context, metricName, 
filter, groupBy, aggregation,
+                    temporalAggregation, null, null);
+            return StringUtils.hasText(query) ? List.of(query) : List.of();
+        }
+        List<String> queries = new ArrayList<>();
+        for (String operationLabel : operationLabels) {
+            String query = buildMetricsQueryForMetric(context, metricName, 
filter, groupBy, aggregation,
+                    temporalAggregation, operationLabel, operationName);
+            if (StringUtils.hasText(query)) {
+                queries.add(query);
+            }
+        }
+        return queries;
+    }
+
+    private String buildMetricsQueryForMetric(OtlpMetricsConsoleDto.Context 
context,
+                                              String metricName,
+                                              String filter,
+                                              String groupBy,
+                                              String aggregation,
+                                              String temporalAggregation,
+                                              String operationLabel,
+                                              String operationName) {
         if (context == null || !StringUtils.hasText(context.getServiceName())) 
{
             return null;
         }
@@ -2080,6 +2171,11 @@ public class OtlpIngestionWorkspaceServiceImpl 
implements OtlpIngestionWorkspace
             matchers.add("hertzbeat_entity_type=\"" + 
escapePromqlLabelValue(context.getEntityType()) + "\"");
             scopedLabels.add("hertzbeat_entity_type");
         }
+        String normalizedOperationName = trimToNull(operationName);
+        if (StringUtils.hasText(operationLabel) && 
StringUtils.hasText(normalizedOperationName)) {
+            matchers.add(operationLabel + "=\"" + 
escapePromqlLabelValue(normalizedOperationName) + "\"");
+            scopedLabels.add(operationLabel);
+        }
         matchers.addAll(parseMetricsFilterMatchers(filter, scopedLabels));
         String selector = "{" + String.join(", ", matchers) + "}";
         return normalizeAggregation(aggregation)
@@ -2128,13 +2224,26 @@ public class OtlpIngestionWorkspaceServiceImpl 
implements OtlpIngestionWorkspace
                                                       String groupBy,
                                                       String aggregation,
                                                       String 
temporalAggregation) {
+        return buildMetricsQueriesForExplicitMetric(context, query, filter, 
groupBy, aggregation, temporalAggregation, null)
+                .stream()
+                .findFirst()
+                .orElse(null);
+    }
+
+    private List<String> 
buildMetricsQueriesForExplicitMetric(OtlpMetricsConsoleDto.Context context,
+                                                              String query,
+                                                              String filter,
+                                                              String groupBy,
+                                                              String 
aggregation,
+                                                              String 
temporalAggregation,
+                                                              String 
operationName) {
         String normalizedQuery = trimToNull(query);
         if (!StringUtils.hasText(normalizedQuery) || 
!SIMPLE_METRIC_NAME.matcher(normalizedQuery).matches()) {
-            return query;
+            return StringUtils.hasText(normalizedQuery) ? 
List.of(normalizedQuery) : List.of();
         }
-        String generatedQuery = buildMetricsQueryForMetric(context, 
normalizedQuery, filter, groupBy, aggregation,
-                temporalAggregation);
-        return StringUtils.hasText(generatedQuery) ? generatedQuery : query;
+        List<String> generatedQueries = buildMetricsQueriesForMetric(context, 
normalizedQuery, filter, groupBy, aggregation,
+                temporalAggregation, operationName);
+        return generatedQueries.isEmpty() ? List.of(normalizedQuery) : 
generatedQueries;
     }
 
     private List<String> parseMetricsFilterMatchers(String filter) {
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 61cb651a81..9fd83b2165 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
@@ -232,7 +232,7 @@ class OtlpIngestionControllerTest {
                 null
         );
         when(otlpIngestionWorkspaceService.getMetricsConsole(42L, "service", 
1000L, 2000L, "checkout", "commerce", "prod",
-                null, "span.kind=\"server\"", null, null, "rate", "60", "1"))
+                null, "span.kind=\"server\"", null, null, "rate", "60", "1", 
"POST /checkout"))
                 .thenReturn(console);
 
         mockMvc.perform(get("/api/ingestion/otlp/metrics/console")
@@ -246,7 +246,8 @@ class OtlpIngestionControllerTest {
                         .param("filter", "span.kind=\"server\"")
                         .param("temporalAggregation", "rate")
                         .param("step", "60")
-                        .param("limit", "1"))
+                        .param("limit", "1")
+                        .param("operationName", "POST /checkout"))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.code").value(0))
                 .andExpect(jsonPath("$.data.context.entityId").value(42))
@@ -258,7 +259,7 @@ class OtlpIngestionControllerTest {
 
         verify(otlpIngestionWorkspaceService)
                 .getMetricsConsole(42L, "service", 1000L, 2000L, "checkout", 
"commerce", "prod",
-                        null, "span.kind=\"server\"", null, null, "rate", 
"60", "1");
+                        null, "span.kind=\"server\"", null, null, "rate", 
"60", "1", "POST /checkout");
     }
 
     @Test
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 ddaa9e1d8d..35e0b65fdd 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
@@ -822,6 +822,92 @@ class OtlpIngestionWorkspaceServiceImplTest {
         );
     }
 
+    @Test
+    void metricsConsoleUsesOperationNameWithHttpRouteFallback() {
+        observabilitySignalIntakeGateway.recordOtlpMetricIntake(
+                Map.of(
+                        "service.name", "checkout",
+                        "service.namespace", "commerce",
+                        "deployment.environment.name", "prod"
+                ),
+                2_000L,
+                "http_server_request_duration_count",
+                "sum",
+                "1",
+                14.0,
+                Map.of("http.route", "POST /checkout")
+        );
+        String operationNameQuery = 
groupedMetricPromql("__name__=\"http_server_request_duration_count\", "
+                + "service_name=\"checkout\", service_namespace=\"commerce\", 
deployment_environment_name=\"prod\", "
+                + "operation_name=\"POST /checkout\"");
+        String httpRouteQuery = 
groupedMetricPromql("__name__=\"http_server_request_duration_count\", "
+                + "service_name=\"checkout\", service_namespace=\"commerce\", 
deployment_environment_name=\"prod\", "
+                + "http_route=\"POST /checkout\"");
+        DatasourceQueryData emptyQueryData = new 
DatasourceQueryData("otlp-metrics-console", 200, null, List.of());
+        DatasourceQueryData httpRouteData = new DatasourceQueryData(
+                "otlp-metrics-console",
+                200,
+                null,
+                List.of(new DatasourceQueryData.SchemaData(
+                        new DatasourceQueryData.MetricSchema(
+                                List.of(
+                                        new 
DatasourceQueryData.MetricField("__ts__", "time", null),
+                                        new 
DatasourceQueryData.MetricField("__value__", "number", null)
+                                ),
+                                Map.of("__name__", 
"http_server_request_duration_count"),
+                                Map.of()
+                        ),
+                        Collections.singletonList(new Object[] {2_000L, 14.0})
+                ))
+        );
+        when(metricQueryRepository.hasPromqlExecutor()).thenReturn(true);
+        when(metricQueryRepository.queryPromqlRange(
+                eq("otlp-metrics-console"),
+                anyString(),
+                anyLong(),
+                anyLong(),
+                anyString()
+        )).thenAnswer(invocation -> {
+            String query = invocation.getArgument(1);
+            return promqlSuccess(httpRouteQuery.equals(query) ? httpRouteData 
: emptyQueryData);
+        });
+
+        OtlpMetricsConsoleDto console = 
otlpIngestionWorkspaceService.getMetricsConsole(
+                null,
+                null,
+                1_000L,
+                2_000L,
+                "checkout",
+                "commerce",
+                "prod",
+                "http_server_request_duration_count",
+                null,
+                null,
+                null,
+                null,
+                null,
+                null,
+                "POST /checkout"
+        );
+
+        assertEquals(httpRouteQuery, console.getQuery());
+        assertEquals(1, console.getStats().getNonEmptySeries());
+        verify(metricQueryRepository).queryPromqlRange(
+                eq("otlp-metrics-console"),
+                eq(operationNameQuery),
+                anyLong(),
+                anyLong(),
+                anyString()
+        );
+        verify(metricQueryRepository).queryPromqlRange(
+                eq("otlp-metrics-console"),
+                eq(httpRouteQuery),
+                anyLong(),
+                anyLong(),
+                anyString()
+        );
+    }
+
     @Test
     void metricsConsoleTranslatesFriendlyLabelOperatorsToPromqlMatchers() {
         observabilitySignalIntakeGateway.recordOtlpMetricIntake(
diff --git a/web-next/app/ingestion/otlp/metrics/route-state.test.ts 
b/web-next/app/ingestion/otlp/metrics/route-state.test.ts
index aed2102c54..7c7f0f20a3 100644
--- a/web-next/app/ingestion/otlp/metrics/route-state.test.ts
+++ b/web-next/app/ingestion/otlp/metrics/route-state.test.ts
@@ -30,6 +30,7 @@ describe('otlp metrics route state', () => {
       returnTo: `/log/manage?returnLabel=${localizedReturnLabel}`,
       traceId: 'trace-123',
       spanId: 'span-456',
+      operationName: 'POST /checkout',
       query: 'http_server_duration_milliseconds_count',
       filter: 'service.name="checkout"',
       aggregation: 'sum',
@@ -50,7 +51,7 @@ describe('otlp metrics route state', () => {
     });
 
     expect(route).toBe(
-      
'/ingestion/otlp/metrics?entityId=7&entityType=service&entityName=checkout&returnTo=%2Flog%2Fmanage&traceId=trace-123&spanId=span-456&query=http_server_duration_milliseconds_count&filter=service.name%3D%22checkout%22&aggregation=sum&temporalAggregation=rate&groupBy=service_name&legendFormat=%7B%7Bservice.name%7D%7D+-+p95&formula=A+*+1000&step=60&limit=25&timeRange=last-1h&serviceName=checkout&serviceNamespace=payments&environment=prod&collector=collector-a&template=spring-boot&star
 [...]
+      
'/ingestion/otlp/metrics?entityId=7&entityType=service&entityName=checkout&returnTo=%2Flog%2Fmanage&traceId=trace-123&spanId=span-456&operationName=POST+%2Fcheckout&query=http_server_duration_milliseconds_count&filter=service.name%3D%22checkout%22&aggregation=sum&temporalAggregation=rate&groupBy=service_name&legendFormat=%7B%7Bservice.name%7D%7D+-+p95&formula=A+*+1000&step=60&limit=25&timeRange=last-1h&serviceName=checkout&serviceNamespace=payments&environment=prod&collector=collec
 [...]
     );
     expect(route).not.toContain('returnLabel=');
     expect(route).not.toContain(encodeURIComponent(localizedReturnLabel));
diff --git a/web-next/app/ingestion/otlp/metrics/route-state.ts 
b/web-next/app/ingestion/otlp/metrics/route-state.ts
index 4f9d30028a..a960dc4770 100644
--- a/web-next/app/ingestion/otlp/metrics/route-state.ts
+++ b/web-next/app/ingestion/otlp/metrics/route-state.ts
@@ -28,6 +28,7 @@ const METRICS_ROUTE_KEYS: Array<keyof OtlpMetricsQueryState> 
= [
   'returnTo',
   'traceId',
   'spanId',
+  'operationName',
   'query',
   'series',
   'filter',
diff --git a/web-next/lib/otlp-metrics/controller.test.ts 
b/web-next/lib/otlp-metrics/controller.test.ts
index bd23320b9b..987335a7bd 100644
--- a/web-next/lib/otlp-metrics/controller.test.ts
+++ b/web-next/lib/otlp-metrics/controller.test.ts
@@ -46,11 +46,21 @@ describe('otlp metrics controller', () => {
         limit: '25',
         traceId: 'trace-1',
         spanId: 'span-1',
+        operationName: 'POST /checkout',
         serviceName: 'checkout',
         start: '1000',
         end: '2000'
       })
-    
).toBe('/ingestion/otlp/metrics/console?query=http_server_duration_milliseconds_count&filter=service.name%3D%22checkout%22&aggregation=sum&temporalAggregation=rate&groupBy=service_name&step=60&limit=25&traceId=trace-1&spanId=span-1&serviceName=checkout&start=1000&end=2000');
+    
).toBe('/ingestion/otlp/metrics/console?query=http_server_duration_milliseconds_count&filter=service.name%3D%22checkout%22&aggregation=sum&temporalAggregation=rate&groupBy=service_name&step=60&limit=25&traceId=trace-1&spanId=span-1&operationName=POST+%2Fcheckout&serviceName=checkout&start=1000&end=2000');
+  });
+
+  it('reads operation context from trace and log handoffs into metrics console 
state', () => {
+    const query = queryStateFromParams(new URLSearchParams(
+      
'query=http.server.duration&serviceName=checkout&operationName=POST+%2Fcheckout&traceId=trace-1&spanId=span-1'
+    ));
+
+    expect(query.operationName).toBe('POST /checkout');
+    
expect(buildOtlpMetricsConsoleUrl(query)).toContain('operationName=POST+%2Fcheckout');
   });
 
   it('builds and loads metrics inventory from entity and service context 
only', async () => {
diff --git a/web-next/lib/otlp-metrics/controller.ts 
b/web-next/lib/otlp-metrics/controller.ts
index d3277fe20e..c3f43acc76 100644
--- a/web-next/lib/otlp-metrics/controller.ts
+++ b/web-next/lib/otlp-metrics/controller.ts
@@ -99,6 +99,7 @@ export function queryStateFromParams(params: 
SearchParamReader): OtlpMetricsQuer
     returnTo: stripReturnLabelFromHref(params.get('returnTo')) || undefined,
     traceId: params.get('traceId') || undefined,
     spanId: params.get('spanId') || undefined,
+    operationName: params.get('operationName') || undefined,
     inspector: readMetricsInspectorView(params.get('inspector')),
     warningThreshold: 
readFiniteNumberRouteParam(params.get('warningThreshold')),
     criticalThreshold: 
readFiniteNumberRouteParam(params.get('criticalThreshold')),


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


Reply via email to