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 b5e4ca5142 Support metrics friendly label filters
b5e4ca5142 is described below

commit b5e4ca5142793e7424f68049a5a6873bbbe9c636
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 21:39:48 2026 +0800

    Support metrics friendly label filters
---
 .../impl/OtlpIngestionWorkspaceServiceImpl.java    | 208 ++++++++++++++++++++-
 .../OtlpIngestionWorkspaceServiceImplTest.java     |  55 ++++++
 2 files changed, 262 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 fc62fa3962..5996245fe1 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
@@ -94,6 +94,15 @@ public class OtlpIngestionWorkspaceServiceImpl implements 
OtlpIngestionWorkspace
     private static final Pattern METRICS_FILTER_MATCHER = Pattern.compile(
             
"\\s*([A-Za-z_:][A-Za-z0-9_.:-]*)\\s*(=~|!~|!=|=)\\s*(?:\"((?:\\\\.|[^\"\\\\])*)\"|'((?:\\\\.|[^'\\\\])*)'|([^,\\s]+))\\s*"
     );
+    private static final Pattern METRICS_FILTER_LIST_OPERATOR_PATTERN = 
Pattern.compile(
+            
"\\s*([A-Za-z_:][A-Za-z0-9_.:-]*)\\s+(NOT\\s+IN|IN)\\s*(\\(.+\\))\\s*",
+            Pattern.CASE_INSENSITIVE);
+    private static final Pattern METRICS_FILTER_TEXT_OPERATOR_PATTERN = 
Pattern.compile(
+            
"\\s*([A-Za-z_:][A-Za-z0-9_.:-]*)\\s+(NOT\\s+CONTAINS|CONTAINS)\\s+(.+)\\s*",
+            Pattern.CASE_INSENSITIVE);
+    private static final Pattern METRICS_FILTER_PRESENCE_OPERATOR_PATTERN = 
Pattern.compile(
+            "\\s*([A-Za-z_:][A-Za-z0-9_.:-]*)\\s+(NOT\\s+EXISTS|EXISTS)\\s*",
+            Pattern.CASE_INSENSITIVE);
     private static final Pattern SIMPLE_METRIC_NAME = 
Pattern.compile("[A-Za-z_:][A-Za-z0-9_:.-]*");
     private static final int DEFAULT_RECENT_SERVICE_LIMIT = 6;
     private static final int DEFAULT_RECENT_UNBOUND_CANDIDATE_LIMIT = 6;
@@ -1865,11 +1874,16 @@ public class OtlpIngestionWorkspaceServiceImpl 
implements OtlpIngestionWorkspace
             return List.of();
         }
         List<String> matchers = new ArrayList<>();
-        for (String rawClause : normalized.split("(?i)\\s+and\\s+|\\s*,\\s*")) 
{
+        for (String rawClause : splitMetricsFilterClauses(normalized)) {
             String clause = trimToNull(rawClause);
             if (!StringUtils.hasText(clause)) {
                 continue;
             }
+            String parsedMatcher = parseMetricsFriendlyFilterMatcher(clause, 
excludedLabels);
+            if (StringUtils.hasText(parsedMatcher)) {
+                matchers.add(parsedMatcher);
+                continue;
+            }
             Matcher matcher = METRICS_FILTER_MATCHER.matcher(clause);
             if (!matcher.matches()) {
                 continue;
@@ -1890,6 +1904,198 @@ public class OtlpIngestionWorkspaceServiceImpl 
implements OtlpIngestionWorkspace
         return matchers;
     }
 
+    private String parseMetricsFriendlyFilterMatcher(String clause, 
Set<String> excludedLabels) {
+        String listMatcher = parseMetricsListFilterMatcher(clause, 
excludedLabels);
+        if (StringUtils.hasText(listMatcher)) {
+            return listMatcher;
+        }
+        String textMatcher = parseMetricsTextFilterMatcher(clause, 
excludedLabels);
+        if (StringUtils.hasText(textMatcher)) {
+            return textMatcher;
+        }
+        return parseMetricsPresenceFilterMatcher(clause, excludedLabels);
+    }
+
+    private String parseMetricsListFilterMatcher(String clause, Set<String> 
excludedLabels) {
+        Matcher matcher = METRICS_FILTER_LIST_OPERATOR_PATTERN.matcher(clause);
+        if (!matcher.matches()) {
+            return null;
+        }
+        String labelName = normalizePromqlLabelName(matcher.group(1));
+        if (!isPromqlLabelName(labelName) || 
excludedLabels.contains(labelName)) {
+            return null;
+        }
+        String valueList = trimToNull(matcher.group(3));
+        if (!StringUtils.hasText(valueList) || valueList.length() < 2
+                || !valueList.startsWith("(") || !valueList.endsWith(")")) {
+            return null;
+        }
+        List<String> values = 
splitMetricsFilterListValues(valueList.substring(1, valueList.length() - 
1)).stream()
+                .map(value -> stripMetricsFilterQuotes(trimToNull(value)))
+                .filter(StringUtils::hasText)
+                .toList();
+        if (values.isEmpty()) {
+            return null;
+        }
+        String regex = "^(?:" + values.stream()
+                .map(this::escapePromqlRegexValue)
+                .collect(java.util.stream.Collectors.joining("|")) + ")$";
+        String operator = trimToNull(matcher.group(2));
+        String promqlOperator = operator != null && 
operator.replaceAll("\\s+", " ").equalsIgnoreCase("not in")
+                ? "!~"
+                : "=~";
+        return labelName + promqlOperator + "\"" + 
escapePromqlLabelValue(regex) + "\"";
+    }
+
+    private String parseMetricsTextFilterMatcher(String clause, Set<String> 
excludedLabels) {
+        Matcher matcher = METRICS_FILTER_TEXT_OPERATOR_PATTERN.matcher(clause);
+        if (!matcher.matches()) {
+            return null;
+        }
+        String labelName = normalizePromqlLabelName(matcher.group(1));
+        if (!isPromqlLabelName(labelName) || 
excludedLabels.contains(labelName)) {
+            return null;
+        }
+        String value = stripMetricsFilterQuotes(trimToNull(matcher.group(3)));
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        String regex = ".*" + escapePromqlRegexValue(value) + ".*";
+        String operator = trimToNull(matcher.group(2));
+        String promqlOperator = operator != null && 
operator.replaceAll("\\s+", " ").equalsIgnoreCase("not contains")
+                ? "!~"
+                : "=~";
+        return labelName + promqlOperator + "\"" + 
escapePromqlLabelValue(regex) + "\"";
+    }
+
+    private String parseMetricsPresenceFilterMatcher(String clause, 
Set<String> excludedLabels) {
+        Matcher matcher = 
METRICS_FILTER_PRESENCE_OPERATOR_PATTERN.matcher(clause);
+        if (!matcher.matches()) {
+            return null;
+        }
+        String labelName = normalizePromqlLabelName(matcher.group(1));
+        if (!isPromqlLabelName(labelName) || 
excludedLabels.contains(labelName)) {
+            return null;
+        }
+        String operator = trimToNull(matcher.group(2));
+        String promqlOperator = operator != null && 
operator.replaceAll("\\s+", " ").equalsIgnoreCase("not exists")
+                ? "!~"
+                : "=~";
+        return labelName + promqlOperator + "\".+\"";
+    }
+
+    private List<String> splitMetricsFilterClauses(String filter) {
+        List<String> clauses = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+        int depth = 0;
+        char quote = 0;
+        for (int index = 0; index < filter.length(); index++) {
+            char character = filter.charAt(index);
+            if (quote != 0) {
+                current.append(character);
+                if (character == quote) {
+                    quote = 0;
+                }
+                continue;
+            }
+            if (character == '\'' || character == '"') {
+                quote = character;
+                current.append(character);
+                continue;
+            }
+            if (character == '(') {
+                depth++;
+                current.append(character);
+                continue;
+            }
+            if (character == ')') {
+                depth = Math.max(0, depth - 1);
+                current.append(character);
+                continue;
+            }
+            if (depth == 0 && character == ',') {
+                addMetricsFilterClause(clauses, current);
+                continue;
+            }
+            if (depth == 0 && isMetricsFilterAndDelimiter(filter, index)) {
+                addMetricsFilterClause(clauses, current);
+                index += 4;
+                continue;
+            }
+            current.append(character);
+        }
+        addMetricsFilterClause(clauses, current);
+        return clauses;
+    }
+
+    private List<String> splitMetricsFilterListValues(String values) {
+        List<String> result = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+        char quote = 0;
+        for (int index = 0; index < values.length(); index++) {
+            char character = values.charAt(index);
+            if (quote != 0) {
+                current.append(character);
+                if (character == quote) {
+                    quote = 0;
+                }
+                continue;
+            }
+            if (character == '\'' || character == '"') {
+                quote = character;
+                current.append(character);
+                continue;
+            }
+            if (character == ',') {
+                addMetricsFilterClause(result, current);
+                continue;
+            }
+            current.append(character);
+        }
+        addMetricsFilterClause(result, current);
+        return result;
+    }
+
+    private void addMetricsFilterClause(List<String> clauses, StringBuilder 
current) {
+        String clause = trimToNull(current.toString());
+        if (StringUtils.hasText(clause)) {
+            clauses.add(clause);
+        }
+        current.setLength(0);
+    }
+
+    private boolean isMetricsFilterAndDelimiter(String value, int index) {
+        return index + 5 <= value.length() && value.regionMatches(true, index, 
" and ", 0, 5);
+    }
+
+    private String stripMetricsFilterQuotes(String value) {
+        if (value == null || value.length() < 2) {
+            return value;
+        }
+        char first = value.charAt(0);
+        char last = value.charAt(value.length() - 1);
+        if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) {
+            return trimToNull(value.substring(1, value.length() - 1));
+        }
+        return value;
+    }
+
+    private String escapePromqlRegexValue(String value) {
+        String normalized = trimToNull(value);
+        if (!StringUtils.hasText(normalized)) {
+            return "";
+        }
+        StringBuilder escaped = new StringBuilder();
+        for (int index = 0; index < normalized.length(); index++) {
+            char character = normalized.charAt(index);
+            if ("\\.^$|?*+()[]{}".indexOf(character) >= 0) {
+                escaped.append('\\');
+            }
+            escaped.append(character);
+        }
+        return escaped.toString();
+    }
+
     private String normalizePromqlLabelName(String label) {
         String normalized = trimToNull(label);
         if (!StringUtils.hasText(normalized)) {
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 341eaaa7e0..b7eeb2be2b 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
@@ -821,6 +821,61 @@ class OtlpIngestionWorkspaceServiceImplTest {
         );
     }
 
+    @Test
+    void metricsConsoleTranslatesFriendlyLabelOperatorsToPromqlMatchers() {
+        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("span.kind", "server", "http.route", "/checkout/{id}")
+        );
+        String expectedQuery = 
groupedMetricPromql("__name__=\"http_server_request_duration_count\", "
+                + "service_name=\"checkout\", service_namespace=\"commerce\", 
deployment_environment_name=\"prod\", "
+                + "span_kind=~\"^(?:server|consumer)$\", 
http_route=~\".*checkout.*\", "
+                + "host_name!~\".*canary.*\", 
cloud_region!~\"^(?:us-west-1|us-west-2)$\", "
+                + "k8s_pod_name=~\".+\", service_instance_id!~\".+\"");
+        DatasourceQueryData emptyQueryData = new 
DatasourceQueryData("otlp-metrics-console", 200, null, List.of());
+        when(metricQueryRepository.hasPromqlExecutor()).thenReturn(true);
+        when(metricQueryRepository.queryPromqlRange(
+                eq("otlp-metrics-console"),
+                anyString(),
+                anyLong(),
+                anyLong(),
+                anyString()
+        )).thenAnswer(invocation -> promqlSuccess(emptyQueryData));
+
+        OtlpMetricsConsoleDto console = 
otlpIngestionWorkspaceService.getMetricsConsole(
+                null,
+                1_000L,
+                2_000L,
+                "checkout",
+                "commerce",
+                "prod",
+                null,
+                "span.kind IN ('server', \"consumer\") and http.route CONTAINS 
checkout "
+                        + "and host.name NOT CONTAINS canary and cloud.region 
NOT IN ('us-west-1', 'us-west-2') "
+                        + "and k8s.pod.name EXISTS and service.instance.id NOT 
EXISTS",
+                null,
+                null
+        );
+
+        assertEquals(expectedQuery, console.getQuery());
+        verify(metricQueryRepository).queryPromqlRange(
+                eq("otlp-metrics-console"),
+                eq(expectedQuery),
+                anyLong(),
+                anyLong(),
+                anyString()
+        );
+    }
+
     @Test
     void metricsConsoleAppliesRequestedEntityIdentityAsPromqlResourceMatcher() 
{
         observabilitySignalIntakeGateway.recordOtlpMetricIntake(


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

Reply via email to