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 527f248106 Preserve resource related metric suggestions
527f248106 is described below

commit 527f248106854cd5a2f9a63ba17a9ba75f91cc23
Author: Logic <[email protected]>
AuthorDate: Sun Jun 14 13:03:27 2026 +0800

    Preserve resource related metric suggestions
    
    Keep PromQL-proven related metric candidates while retaining pod and host 
resource-filter suggestions for cross-signal drilldowns.
    
    Validation: ./mvnw -pl hertzbeat-observability -am 
'-Dtest=OtlpIngestionWorkspaceServiceImplTest#relatedMetricsPrefersPromqlAvailableCandidatesWhenExecutorExists+relatedMetricsKeepsResourceSuggestionsWhenPromqlFindsServiceCandidates'
 test -DskipITs -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false; 
script/dev/run-three-signal-live-proof.sh; in-app Browser smoke for 
/ingestion/otlp/metrics, /log/manage, and /trace/manage.
---
 .../impl/OtlpIngestionWorkspaceServiceImpl.java    | 35 +++++++++-
 .../OtlpIngestionWorkspaceServiceImplTest.java     | 79 ++++++++++++++++++++++
 2 files changed, 113 insertions(+), 1 deletion(-)

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 b51d31b2eb..9b5a176be3 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
@@ -944,12 +944,45 @@ public class OtlpIngestionWorkspaceServiceImpl implements 
OtlpIngestionWorkspace
             List<OtlpRelatedMetricsDto.Candidate> availableCandidates =
                     
filterPromqlAvailableRelatedMetricCandidates(rawCandidates, start, end, limit);
             if (!availableCandidates.isEmpty()) {
-                return availableCandidates;
+                return 
mergeAvailableRelatedMetricCandidates(availableCandidates, rawCandidates, 
limit);
             }
         }
         return rawCandidates.stream().limit(limit).toList();
     }
 
+    private List<OtlpRelatedMetricsDto.Candidate> 
mergeAvailableRelatedMetricCandidates(
+            List<OtlpRelatedMetricsDto.Candidate> availableCandidates,
+            List<OtlpRelatedMetricsDto.Candidate> rawCandidates,
+            int limit) {
+        if (limit <= 0) {
+            return List.of();
+        }
+        LinkedHashMap<String, OtlpRelatedMetricsDto.Candidate> merged = new 
LinkedHashMap<>();
+        for (OtlpRelatedMetricsDto.Candidate candidate : availableCandidates) {
+            putRelatedMetricCandidate(merged, candidate, limit);
+        }
+        for (OtlpRelatedMetricsDto.Candidate candidate : rawCandidates) {
+            if (!"resource-filter".equals(candidate.getReason())) {
+                continue;
+            }
+            putRelatedMetricCandidate(merged, candidate, limit);
+        }
+        return merged.values().stream().limit(limit).toList();
+    }
+
+    private void putRelatedMetricCandidate(LinkedHashMap<String, 
OtlpRelatedMetricsDto.Candidate> candidates,
+                                           OtlpRelatedMetricsDto.Candidate 
candidate,
+                                           int limit) {
+        if (candidate == null || candidates.size() >= limit) {
+            return;
+        }
+        String query = trimToNull(candidate.getQuery());
+        if (!StringUtils.hasText(query)) {
+            return;
+        }
+        candidates.putIfAbsent(candidate.getSource() + ":" + 
normalizePromqlMetricName(query), candidate);
+    }
+
     private List<String> 
relatedMetricCandidateNames(OtlpMetricsConsoleDto.Context context, long start, 
long end) {
         List<String> candidates = new ArrayList<>();
         if (metricQueryRepository.hasPromqlExecutor()) {
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 51b3c365c8..e9e6be6f97 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
@@ -1406,6 +1406,85 @@ class OtlpIngestionWorkspaceServiceImplTest {
         );
     }
 
+    @Test
+    void 
relatedMetricsKeepsResourceSuggestionsWhenPromqlFindsServiceCandidates() {
+        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());
+        when(metricQueryRepository.hasPromqlExecutor()).thenReturn(true);
+        when(metricQueryRepository.queryPromqlRange(
+                anyString(),
+                anyString(),
+                anyLong(),
+                anyLong(),
+                anyString()
+        )).thenAnswer(invocation -> {
+            String refId = invocation.getArgument(0);
+            String query = invocation.getArgument(1);
+            if ("otlp-related-metrics-inventory".equals(refId)) {
+                return promqlSuccess(new 
DatasourceQueryData("otlp-related-metrics-inventory", 200, null, List.of()));
+            }
+            if (query.contains("__name__=\"http_server_duration\"")) {
+                return promqlSuccess(new DatasourceQueryData(
+                        "otlp-related-metrics",
+                        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_duration"),
+                                        Map.of()
+                                ),
+                                Collections.singletonList(new Object[] 
{2_000L, 14.0})
+                        ))
+                ));
+            }
+            return promqlSuccess(new 
DatasourceQueryData("otlp-related-metrics", 200, null, List.of()));
+        });
+
+        OtlpRelatedMetricsDto related = 
otlpIngestionWorkspaceService.getRelatedMetrics(
+                42L,
+                "service",
+                1_000L,
+                2_000L,
+                "checkout",
+                "commerce",
+                "prod",
+                "k8s.pod.name=\"checkout-7d9\" and host.name=\"node-a\"",
+                null,
+                "8"
+        );
+
+        assertTrue(related.getCandidates().stream().anyMatch(candidate ->
+                "http_server_duration".equals(candidate.getQuery())
+                        && "promql-series".equals(candidate.getReason())));
+        assertTrue(related.getCandidates().stream().anyMatch(candidate ->
+                "container.cpu.usage".equals(candidate.getQuery())
+                        && "pod".equals(candidate.getSource())
+                        && "resource-filter".equals(candidate.getReason())
+                        && 
candidate.getMatchedLabels().contains("k8s_pod_name")));
+        assertTrue(related.getCandidates().stream().anyMatch(candidate ->
+                "system.cpu.utilization".equals(candidate.getQuery())
+                        && "host".equals(candidate.getSource())
+                        && "resource-filter".equals(candidate.getReason())
+                        && 
candidate.getMatchedLabels().contains("host_name")));
+    }
+
     @Test
     void relatedMetricsDiscoversCandidateNamesFromPromqlSeriesInventory() {
         observabilitySignalIntakeGateway.recordOtlpMetricIntake(


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

Reply via email to