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 fed439e5d8 Support trace negative resource filters
fed439e5d8 is described below

commit fed439e5d8a28055c00ef3ee026d7239f98893d4
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 20:49:13 2026 +0800

    Support trace negative resource filters
---
 .../service/impl/EntityTraceQueryServiceImpl.java  | 157 ++++++++++++++++-----
 .../impl/EntityTraceQueryServiceImplTest.java      |  63 +++++++++
 2 files changed, 185 insertions(+), 35 deletions(-)

diff --git 
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
 
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
index 3f4bbf36bc..f4417bb4bc 100644
--- 
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
+++ 
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImpl.java
@@ -37,6 +37,8 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.hertzbeat.common.entity.manager.EntityIdentity;
@@ -91,12 +93,20 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
     private static final BigInteger LONG_MIN_VALUE = 
BigInteger.valueOf(Long.MIN_VALUE);
     private static final BigDecimal LONG_MAX_DECIMAL = 
BigDecimal.valueOf(Long.MAX_VALUE);
     private static final BigDecimal LONG_MIN_DECIMAL = 
BigDecimal.valueOf(Long.MIN_VALUE);
+    private static final Pattern RESOURCE_FILTER_LIST_OPERATOR_PATTERN = 
Pattern.compile(
+            "^\\s*([A-Za-z0-9._:-]+)\\s+(NOT\\s+IN|IN)\\s*(\\(.+\\))\\s*$",
+            Pattern.CASE_INSENSITIVE);
+    private static final Pattern RESOURCE_FILTER_NOT_EQUALS_PATTERN = 
Pattern.compile(
+            "^\\s*([A-Za-z0-9._:-]+)\\s*!=\\s*(.+?)\\s*$",
+            Pattern.CASE_INSENSITIVE);
     private static final Set<String> WORKSPACE_RESOURCE_KEYS = Set.of(
             OtlpCorrelationEnricher.WORKSPACE_ID_ATTRIBUTE,
             AuthTokenScopes.CLAIM_WORKSPACE_ID,
             "workspace.id"
     );
     private static final Set<String> ENTITY_SCOPE_RESOURCE_KEYS = Set.of(
+            OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_ID,
+            OtlpResourceSemanticAttributes.HERTZBEAT_ENTITY_TYPE,
             "service.name",
             "service.namespace",
             "deployment.environment.name"
@@ -225,15 +235,16 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
         ObservedEntityContext entityContext = entityId == null ? null : 
loadEntityContext(entityId);
         Map<String, Set<String>> identityValues = 
canonicalIdentityValues(entityContext);
         TraceQueryScope queryScope = resolveTraceQueryScope(entityContext, 
identityValues, serviceName, serviceNamespace, environment);
-        Map<String, Set<String>> resourceFilters = 
removeEntityScopeResourceFilters(
+        ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
                 identityValues, parseResourceFilters(resourceFilter));
-        Map<String, Set<String>> pushedResourceFilters = 
mergeResourceFilters(identityValues, resourceFilters);
+        Map<String, Set<String>> pushedResourceFilters = 
mergeResourceFilters(identityValues, resourceFilters.include());
         PageRequest pageRequest = 
PageRequest.of(normalizeTraceListPageIndex(pageIndex), 
normalizeTraceListPageSize(pageSize));
         int repositoryOffset = 
Math.toIntExact(Math.min(pageRequest.getOffset(), Integer.MAX_VALUE));
         Long minDurationNanos = durationMillisToNanos(minDurationMs);
         Long maxDurationNanos = durationMillisToNanos(maxDurationMs);
         String normalizedSpanScope = normalizeSpanScope(spanScope);
-        if (!StringUtils.hasText(traceId) && 
traceQueryRepository.supportsTraceListRows()) {
+        if (!StringUtils.hasText(traceId) && !resourceFilters.hasExclusions()
+                && traceQueryRepository.supportsTraceListRows()) {
             List<Map<String, Object>> rows = 
StringUtils.hasText(normalizedSpanScope)
                     ? traceQueryRepository.queryTraceListRows(
                             start,
@@ -358,13 +369,14 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
         ObservedEntityContext entityContext = entityId == null ? null : 
loadEntityContext(entityId);
         Map<String, Set<String>> identityValues = 
canonicalIdentityValues(entityContext);
         TraceQueryScope queryScope = resolveTraceQueryScope(entityContext, 
identityValues, serviceName, serviceNamespace, environment);
-        Map<String, Set<String>> resourceFilters = 
removeEntityScopeResourceFilters(
+        ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
                 identityValues, parseResourceFilters(resourceFilter));
-        Map<String, Set<String>> pushedResourceFilters = 
mergeResourceFilters(identityValues, resourceFilters);
+        Map<String, Set<String>> pushedResourceFilters = 
mergeResourceFilters(identityValues, resourceFilters.include());
         Long minDurationNanos = durationMillisToNanos(minDurationMs);
         Long maxDurationNanos = durationMillisToNanos(maxDurationMs);
         String normalizedSpanScope = normalizeSpanScope(spanScope);
-        if (StringUtils.hasText(traceId) && 
traceQueryRepository.supportsTraceIdOverviewRows()) {
+        if (StringUtils.hasText(traceId) && !resourceFilters.hasExclusions()
+                && traceQueryRepository.supportsTraceIdOverviewRows()) {
             Map<String, Object> row = StringUtils.hasText(normalizedSpanScope)
                     ? traceQueryRepository.queryTraceIdOverviewRows(
                             traceId,
@@ -402,7 +414,8 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
                 return overview;
             }
         }
-        if (!StringUtils.hasText(traceId) && 
traceQueryRepository.supportsTraceOverviewRows()) {
+        if (!StringUtils.hasText(traceId) && !resourceFilters.hasExclusions()
+                && traceQueryRepository.supportsTraceOverviewRows()) {
             Map<String, Object> row = StringUtils.hasText(normalizedSpanScope)
                     ? traceQueryRepository.queryTraceOverviewRows(
                             start,
@@ -483,12 +496,13 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
         ObservedEntityContext entityContext = entityId == null ? null : 
loadEntityContext(entityId);
         Map<String, Set<String>> identityValues = 
canonicalIdentityValues(entityContext);
         TraceQueryScope queryScope = resolveTraceQueryScope(entityContext, 
identityValues, serviceName, serviceNamespace, environment);
-        Map<String, Set<String>> resourceFilters = 
removeEntityScopeResourceFilters(
+        ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
                 identityValues, parseResourceFilters(resourceFilter));
-        Map<String, Set<String>> pushedResourceFilters = 
mergeResourceFilters(identityValues, resourceFilters);
+        Map<String, Set<String>> pushedResourceFilters = 
mergeResourceFilters(identityValues, resourceFilters.include());
         Long minDurationNanos = durationMillisToNanos(minDurationMs);
         Long maxDurationNanos = durationMillisToNanos(maxDurationMs);
-        if (!StringUtils.hasText(traceId) && 
traceQueryRepository.supportsTraceGroupByRows()) {
+        if (!StringUtils.hasText(traceId) && !resourceFilters.hasExclusions()
+                && traceQueryRepository.supportsTraceGroupByRows()) {
             List<Map<String, Object>> rows = 
StringUtils.hasText(normalizedSpanScope)
                     ? traceQueryRepository.queryTraceGroupByRows(
                             start,
@@ -894,7 +908,7 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
     }
 
     private boolean matchesTraceFilters(TraceAggregate trace, Map<String, 
Set<String>> identityValues,
-                                        Map<String, Set<String>> 
resourceFilters, Long start, Long end,
+                                        ResourceFilterSet resourceFilters, 
Long start, Long end,
                                         String traceId, Boolean errorOnly, 
String serviceName, String serviceNamespace,
                                         String environment, String 
operationName, Long minDurationNanos,
                                         Long maxDurationNanos, Boolean 
hideInternal) {
@@ -1032,7 +1046,12 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
         return false;
     }
 
-    private boolean matchesResourceFilters(TraceAggregate trace, Map<String, 
Set<String>> resourceFilters) {
+    private boolean matchesResourceFilters(TraceAggregate trace, 
ResourceFilterSet resourceFilters) {
+        return matchesIncludedResourceFilters(trace, resourceFilters.include())
+                && matchesExcludedResourceFilters(trace, 
resourceFilters.exclude());
+    }
+
+    private boolean matchesIncludedResourceFilters(TraceAggregate trace, 
Map<String, Set<String>> resourceFilters) {
         if (resourceFilters.isEmpty()) {
             return true;
         }
@@ -1054,17 +1073,43 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
         return true;
     }
 
-    private Map<String, Set<String>> parseResourceFilters(String 
resourceFilter) {
+    private boolean matchesExcludedResourceFilters(TraceAggregate trace, 
Map<String, Set<String>> resourceFilters) {
+        if (resourceFilters.isEmpty()) {
+            return true;
+        }
+        if (trace == null) {
+            return false;
+        }
+        for (Map.Entry<String, Set<String>> entry : 
resourceFilters.entrySet()) {
+            String actual = 
trimText(resolveCanonicalValue(trace.getResourceAttributes(), entry.getKey(), 
trace.getServiceName()));
+            if (!StringUtils.hasText(actual)) {
+                continue;
+            }
+            boolean excluded = entry.getValue().stream()
+                    .filter(StringUtils::hasText)
+                    .anyMatch(expected -> actual.equalsIgnoreCase(expected));
+            if (excluded) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private ResourceFilterSet parseResourceFilters(String resourceFilter) {
         if (!StringUtils.hasText(resourceFilter)) {
-            return Collections.emptyMap();
+            return ResourceFilterSet.empty();
         }
-        Map<String, Set<String>> filters = new LinkedHashMap<>();
+        Map<String, Set<String>> includeFilters = new LinkedHashMap<>();
+        Map<String, Set<String>> excludeFilters = new LinkedHashMap<>();
         for (String clause : splitResourceFilterClauses(resourceFilter)) {
             String trimmedClause = trimText(clause);
             if (!StringUtils.hasText(trimmedClause)) {
                 continue;
             }
-            if (appendResourceFilterInValues(filters, trimmedClause)) {
+            if (appendResourceFilterListValues(includeFilters, excludeFilters, 
trimmedClause)) {
+                continue;
+            }
+            if (appendResourceFilterNotEqualsValue(excludeFilters, 
trimmedClause)) {
                 continue;
             }
             int separatorIndex = resourceFilterSeparatorIndex(trimmedClause);
@@ -1076,38 +1121,49 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
             if (!isSafeResourceFilterKey(key) || !StringUtils.hasText(value)) {
                 continue;
             }
-            filters.computeIfAbsent(key, ignored -> new 
LinkedHashSet<>()).add(value);
+            includeFilters.computeIfAbsent(key, ignored -> new 
LinkedHashSet<>()).add(value);
         }
-        return filters;
+        return new ResourceFilterSet(includeFilters, excludeFilters);
     }
 
-    private boolean appendResourceFilterInValues(Map<String, Set<String>> 
filters, String clause) {
-        int inIndex = resourceFilterInOperatorIndex(clause);
-        if (inIndex <= 0) {
+    private boolean appendResourceFilterListValues(Map<String, Set<String>> 
includeFilters,
+                                                   Map<String, Set<String>> 
excludeFilters,
+                                                   String clause) {
+        Matcher matcher = 
RESOURCE_FILTER_LIST_OPERATOR_PATTERN.matcher(clause);
+        if (!matcher.matches()) {
             return false;
         }
-        String key = trimText(clause.substring(0, inIndex));
-        String valueList = trimText(clause.substring(inIndex + 4));
-        if (!isSafeResourceFilterKey(key) || !StringUtils.hasText(valueList)
-                || !valueList.startsWith("(") || !valueList.endsWith(")")) {
+        String key = trimText(matcher.group(1));
+        String operator = trimText(matcher.group(2));
+        String valueList = trimText(matcher.group(3));
+        if (!isSafeResourceFilterKey(key) || !StringUtils.hasText(operator) || 
!StringUtils.hasText(valueList)
+                || valueList.length() < 2 || !valueList.startsWith("(") || 
!valueList.endsWith(")")) {
             return false;
         }
+        Map<String, Set<String>> target = operator.replaceAll("\\s+", " 
").equalsIgnoreCase("not in")
+                ? excludeFilters
+                : includeFilters;
         for (String value : 
splitResourceFilterListValues(valueList.substring(1, valueList.length() - 1))) {
             String normalizedValue = 
stripResourceFilterQuotes(trimText(value));
             if (StringUtils.hasText(normalizedValue)) {
-                filters.computeIfAbsent(key, ignored -> new 
LinkedHashSet<>()).add(normalizedValue);
+                target.computeIfAbsent(key, ignored -> new 
LinkedHashSet<>()).add(normalizedValue);
             }
         }
-        return filters.containsKey(key);
+        return target.containsKey(key);
     }
 
-    private int resourceFilterInOperatorIndex(String clause) {
-        String normalized = clause == null ? null : 
clause.toLowerCase(Locale.ROOT);
-        if (!StringUtils.hasText(normalized)) {
-            return -1;
+    private boolean appendResourceFilterNotEqualsValue(Map<String, 
Set<String>> excludeFilters, String clause) {
+        Matcher matcher = RESOURCE_FILTER_NOT_EQUALS_PATTERN.matcher(clause);
+        if (!matcher.matches()) {
+            return false;
+        }
+        String key = trimText(matcher.group(1));
+        String value = stripResourceFilterQuotes(trimText(matcher.group(2)));
+        if (!isSafeResourceFilterKey(key) || !StringUtils.hasText(value)) {
+            return false;
         }
-        int index = normalized.indexOf(" in ");
-        return index < 0 ? -1 : index;
+        excludeFilters.computeIfAbsent(key, ignored -> new 
LinkedHashSet<>()).add(value);
+        return true;
     }
 
     private List<String> splitResourceFilterClauses(String resourceFilter) {
@@ -1242,8 +1298,19 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
         return merged;
     }
 
-    private Map<String, Set<String>> 
removeEntityScopeResourceFilters(Map<String, Set<String>> identityValues,
-                                                                      
Map<String, Set<String>> resourceFilters) {
+    private ResourceFilterSet removeEntityScopeResourceFilters(Map<String, 
Set<String>> identityValues,
+                                                               
ResourceFilterSet resourceFilters) {
+        if (resourceFilters == null || resourceFilters.isEmpty()) {
+            return ResourceFilterSet.empty();
+        }
+        return new ResourceFilterSet(
+                removeEntityScopeResourceFilterMap(identityValues, 
resourceFilters.include()),
+                removeEntityScopeResourceFilterMap(identityValues, 
resourceFilters.exclude())
+        );
+    }
+
+    private Map<String, Set<String>> 
removeEntityScopeResourceFilterMap(Map<String, Set<String>> identityValues,
+                                                                        
Map<String, Set<String>> resourceFilters) {
         if (CollectionUtils.isEmpty(identityValues) || 
CollectionUtils.isEmpty(resourceFilters)) {
             return resourceFilters;
         }
@@ -1617,6 +1684,26 @@ public class EntityTraceQueryServiceImpl implements 
EntityTraceQueryService {
         return trimText(Objects.toString(row.get(key), null));
     }
 
+    private record ResourceFilterSet(Map<String, Set<String>> include, 
Map<String, Set<String>> exclude) {
+
+        private static ResourceFilterSet empty() {
+            return new ResourceFilterSet(Collections.emptyMap(), 
Collections.emptyMap());
+        }
+
+        private ResourceFilterSet {
+            include = include == null ? Collections.emptyMap() : include;
+            exclude = exclude == null ? Collections.emptyMap() : exclude;
+        }
+
+        private boolean isEmpty() {
+            return include.isEmpty() && exclude.isEmpty();
+        }
+
+        private boolean hasExclusions() {
+            return !exclude.isEmpty();
+        }
+    }
+
     private record TraceQueryScope(String serviceName, String 
serviceNamespace, String environment) {
     }
 
diff --git 
a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java
 
b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java
index 3e9c143ac4..752d9d4d83 100644
--- 
a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java
+++ 
b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/service/impl/EntityTraceQueryServiceImplTest.java
@@ -556,6 +556,69 @@ class EntityTraceQueryServiceImplTest {
                 org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(), 
eq(true));
     }
 
+    @Test
+    void queryTraceListAppliesNegativeResourceFiltersWithRowFallback() {
+        long now = System.currentTimeMillis();
+        long start = now - 120_000;
+        long end = now;
+        when(traceQueryRepository.queryRecentTraceRows(
+                eq(1500), eq(start), eq(end), eq("checkout-service"), 
org.mockito.ArgumentMatchers.isNull(),
+                org.mockito.ArgumentMatchers.isNull(), 
org.mockito.ArgumentMatchers.isNull(),
+                org.mockito.ArgumentMatchers.isNull(), 
org.mockito.ArgumentMatchers.isNull(),
+                org.mockito.ArgumentMatchers.isNull(), 
org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                eq(false))).thenReturn(List.of(
+                traceRow("trace-stable", "span-root-1", null, "GET /checkout", 
"checkout-service", "STATUS_CODE_OK",
+                        now - 10_000, 2_000_000L,
+                        Map.of("service.name", "checkout-service",
+                                "service.version", "1.2.3",
+                                "host.name", "checkout-1",
+                                "deployment.environment.name", "prod")),
+                traceRow("trace-canary", "span-root-2", null, "GET /checkout", 
"checkout-service", "STATUS_CODE_OK",
+                        now - 9_000, 2_000_000L,
+                        Map.of("service.name", "checkout-service",
+                                "service.version", "1.2.3",
+                                "host.name", "checkout-canary",
+                                "deployment.environment.name", "prod")),
+                traceRow("trace-staging", "span-root-3", null, "GET 
/checkout", "checkout-service", "STATUS_CODE_OK",
+                        now - 8_000, 2_000_000L,
+                        Map.of("service.name", "checkout-service",
+                                "service.version", "1.2.4",
+                                "host.name", "checkout-2",
+                                "deployment.environment.name", "staging")),
+                traceRow("trace-other-version", "span-root-4", null, "GET 
/checkout", "checkout-service",
+                        "STATUS_CODE_OK", now - 7_000, 2_000_000L,
+                        Map.of("service.name", "checkout-service",
+                                "service.version", "2.0.0",
+                                "host.name", "checkout-3",
+                                "deployment.environment.name", "prod"))
+        ));
+
+        var page = entityTraceQueryService.queryTraceList(null, start, end, 
null,
+                false, "checkout-service", null, null,
+                "service.version IN (\"1.2.3\", '1.2.4') and host.name NOT IN 
('checkout-canary') "
+                        + "and deployment.environment.name!=staging",
+                null, null, null, 0, 20, false);
+
+        assertEquals(1, page.getTotalElements());
+        assertEquals("trace-stable", 
page.getContent().getFirst().getTraceId());
+        ArgumentCaptor<Map<String, Set<String>>> pushedFilterCaptor = 
ArgumentCaptor.forClass(Map.class);
+        verify(traceQueryRepository).queryRecentTraceRows(
+                eq(1500), eq(start), eq(end), eq("checkout-service"), 
org.mockito.ArgumentMatchers.isNull(),
+                org.mockito.ArgumentMatchers.isNull(), 
org.mockito.ArgumentMatchers.isNull(),
+                org.mockito.ArgumentMatchers.isNull(), 
org.mockito.ArgumentMatchers.isNull(),
+                org.mockito.ArgumentMatchers.isNull(), 
pushedFilterCaptor.capture(), eq(false));
+        assertEquals(Set.of("1.2.3", "1.2.4"), 
pushedFilterCaptor.getValue().get("service.version"));
+        verify(traceQueryRepository, never()).queryTraceListRows(
+                org.mockito.ArgumentMatchers.any(), 
org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), 
org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), 
org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), 
org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.any(), 
org.mockito.ArgumentMatchers.any(),
+                org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+                org.mockito.ArgumentMatchers.any(), 
org.mockito.ArgumentMatchers.anyInt(),
+                org.mockito.ArgumentMatchers.anyInt());
+    }
+
     @Test
     void traceQueriesPreferEntityIdentityOverConflictingRouteContext() {
         long now = System.currentTimeMillis();


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

Reply via email to