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]