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 744685644c Support trace resource text filters
744685644c is described below
commit 744685644c9874f7f96fdfecc99dd59c7d470a26
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 21:34:34 2026 +0800
Support trace resource text filters
---
.../service/impl/EntityTraceQueryServiceImpl.java | 166 +++++++++++++++++++--
.../impl/EntityTraceQueryServiceImplTest.java | 69 +++++++++
2 files changed, 223 insertions(+), 12 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 f4417bb4bc..2684d038eb 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
@@ -89,6 +89,10 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
private static final long TRACE_GROUP_BY_MAX_MIN_COUNT = 1_000_000L;
private static final long DEFAULT_LOOKBACK_MILLIS =
Duration.ofHours(24).toMillis();
private static final long ACTIVE_TRACE_WINDOW_MILLIS =
Duration.ofMinutes(15).toMillis();
+ private static final String RESOURCE_FILTER_CONTAINS_PREFIX =
"__hz_contains__:";
+ private static final String RESOURCE_FILTER_NOT_CONTAINS_PREFIX =
"__hz_not_contains__:";
+ private static final String RESOURCE_FILTER_EXISTS_VALUE = "__hz_exists__";
+ private static final String RESOURCE_FILTER_NOT_EXISTS_VALUE =
"__hz_not_exists__";
private static final BigInteger LONG_MAX_VALUE =
BigInteger.valueOf(Long.MAX_VALUE);
private static final BigInteger LONG_MIN_VALUE =
BigInteger.valueOf(Long.MIN_VALUE);
private static final BigDecimal LONG_MAX_DECIMAL =
BigDecimal.valueOf(Long.MAX_VALUE);
@@ -99,6 +103,12 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
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 Pattern RESOURCE_FILTER_TEXT_OPERATOR_PATTERN =
Pattern.compile(
+
"^\\s*([A-Za-z0-9._:-]+)\\s+(NOT\\s+CONTAINS|CONTAINS)\\s+(.+)\\s*$",
+ Pattern.CASE_INSENSITIVE);
+ private static final Pattern RESOURCE_FILTER_PRESENCE_OPERATOR_PATTERN =
Pattern.compile(
+ "^\\s*([A-Za-z0-9._:-]+)\\s+(NOT\\s+EXISTS|EXISTS)\\s*$",
+ Pattern.CASE_INSENSITIVE);
private static final Set<String> WORKSPACE_RESOURCE_KEYS = Set.of(
OtlpCorrelationEnricher.WORKSPACE_ID_ATTRIBUTE,
AuthTokenScopes.CLAIM_WORKSPACE_ID,
@@ -237,13 +247,13 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
TraceQueryScope queryScope = resolveTraceQueryScope(entityContext,
identityValues, serviceName, serviceNamespace, environment);
ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
identityValues, parseResourceFilters(resourceFilter));
- Map<String, Set<String>> pushedResourceFilters =
mergeResourceFilters(identityValues, resourceFilters.include());
+ Map<String, Set<String>> pushedResourceFilters =
mergeResourceFilters(identityValues, resourceFilters.pushableInclude());
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) && !resourceFilters.hasExclusions()
+ if (!StringUtils.hasText(traceId) &&
!resourceFilters.requiresRowFallback()
&& traceQueryRepository.supportsTraceListRows()) {
List<Map<String, Object>> rows =
StringUtils.hasText(normalizedSpanScope)
? traceQueryRepository.queryTraceListRows(
@@ -371,11 +381,11 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
TraceQueryScope queryScope = resolveTraceQueryScope(entityContext,
identityValues, serviceName, serviceNamespace, environment);
ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
identityValues, parseResourceFilters(resourceFilter));
- Map<String, Set<String>> pushedResourceFilters =
mergeResourceFilters(identityValues, resourceFilters.include());
+ Map<String, Set<String>> pushedResourceFilters =
mergeResourceFilters(identityValues, resourceFilters.pushableInclude());
Long minDurationNanos = durationMillisToNanos(minDurationMs);
Long maxDurationNanos = durationMillisToNanos(maxDurationMs);
String normalizedSpanScope = normalizeSpanScope(spanScope);
- if (StringUtils.hasText(traceId) && !resourceFilters.hasExclusions()
+ if (StringUtils.hasText(traceId) &&
!resourceFilters.requiresRowFallback()
&& traceQueryRepository.supportsTraceIdOverviewRows()) {
Map<String, Object> row = StringUtils.hasText(normalizedSpanScope)
? traceQueryRepository.queryTraceIdOverviewRows(
@@ -414,7 +424,7 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
return overview;
}
}
- if (!StringUtils.hasText(traceId) && !resourceFilters.hasExclusions()
+ if (!StringUtils.hasText(traceId) &&
!resourceFilters.requiresRowFallback()
&& traceQueryRepository.supportsTraceOverviewRows()) {
Map<String, Object> row = StringUtils.hasText(normalizedSpanScope)
? traceQueryRepository.queryTraceOverviewRows(
@@ -498,10 +508,10 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
TraceQueryScope queryScope = resolveTraceQueryScope(entityContext,
identityValues, serviceName, serviceNamespace, environment);
ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
identityValues, parseResourceFilters(resourceFilter));
- Map<String, Set<String>> pushedResourceFilters =
mergeResourceFilters(identityValues, resourceFilters.include());
+ Map<String, Set<String>> pushedResourceFilters =
mergeResourceFilters(identityValues, resourceFilters.pushableInclude());
Long minDurationNanos = durationMillisToNanos(minDurationMs);
Long maxDurationNanos = durationMillisToNanos(maxDurationMs);
- if (!StringUtils.hasText(traceId) && !resourceFilters.hasExclusions()
+ if (!StringUtils.hasText(traceId) &&
!resourceFilters.requiresRowFallback()
&& traceQueryRepository.supportsTraceGroupByRows()) {
List<Map<String, Object>> rows =
StringUtils.hasText(normalizedSpanScope)
? traceQueryRepository.queryTraceGroupByRows(
@@ -1060,12 +1070,10 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
}
for (Map.Entry<String, Set<String>> entry :
resourceFilters.entrySet()) {
String actual =
trimText(resolveCanonicalValue(trace.getResourceAttributes(), entry.getKey(),
trace.getServiceName()));
- if (!StringUtils.hasText(actual)) {
- return false;
- }
+ boolean keyExists = resourceKeyExists(trace, entry.getKey());
boolean matched = entry.getValue().stream()
.filter(StringUtils::hasText)
- .anyMatch(expected -> actual.equalsIgnoreCase(expected));
+ .anyMatch(expected -> matchesResourceFilterValue(actual,
expected, keyExists));
if (!matched) {
return false;
}
@@ -1087,7 +1095,7 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
}
boolean excluded = entry.getValue().stream()
.filter(StringUtils::hasText)
- .anyMatch(expected -> actual.equalsIgnoreCase(expected));
+ .anyMatch(expected ->
matchesExactResourceFilterValue(actual, expected));
if (excluded) {
return false;
}
@@ -1095,6 +1103,36 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
return true;
}
+ private boolean matchesResourceFilterValue(String actualValue, String
expectedValue, boolean keyExists) {
+ if (isExistsResourceFilterValue(expectedValue)) {
+ return keyExists;
+ }
+ if (isNotExistsResourceFilterValue(expectedValue)) {
+ return !keyExists;
+ }
+ if (isContainsResourceFilterValue(expectedValue)) {
+ return matchesContainedResourceFilterValue(actualValue,
+
expectedValue.substring(RESOURCE_FILTER_CONTAINS_PREFIX.length()));
+ }
+ if (isNotContainsResourceFilterValue(expectedValue)) {
+ return !matchesContainedResourceFilterValue(actualValue,
+
expectedValue.substring(RESOURCE_FILTER_NOT_CONTAINS_PREFIX.length()));
+ }
+ return matchesExactResourceFilterValue(actualValue, expectedValue);
+ }
+
+ private boolean matchesExactResourceFilterValue(String actualValue, String
expectedValue) {
+ return StringUtils.hasText(actualValue) &&
StringUtils.hasText(expectedValue)
+ && actualValue.equalsIgnoreCase(expectedValue);
+ }
+
+ private boolean matchesContainedResourceFilterValue(String actualValue,
String expectedValue) {
+ if (!StringUtils.hasText(actualValue) ||
!StringUtils.hasText(expectedValue)) {
+ return false;
+ }
+ return
actualValue.toLowerCase(Locale.ROOT).contains(expectedValue.toLowerCase(Locale.ROOT));
+ }
+
private ResourceFilterSet parseResourceFilters(String resourceFilter) {
if (!StringUtils.hasText(resourceFilter)) {
return ResourceFilterSet.empty();
@@ -1109,6 +1147,12 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
if (appendResourceFilterListValues(includeFilters, excludeFilters,
trimmedClause)) {
continue;
}
+ if (appendResourceFilterTextValue(includeFilters, trimmedClause)) {
+ continue;
+ }
+ if (appendResourceFilterPresenceValue(includeFilters,
trimmedClause)) {
+ continue;
+ }
if (appendResourceFilterNotEqualsValue(excludeFilters,
trimmedClause)) {
continue;
}
@@ -1126,6 +1170,41 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
return new ResourceFilterSet(includeFilters, excludeFilters);
}
+ private boolean appendResourceFilterTextValue(Map<String, Set<String>>
includeFilters, String clause) {
+ Matcher matcher =
RESOURCE_FILTER_TEXT_OPERATOR_PATTERN.matcher(clause);
+ if (!matcher.matches()) {
+ return false;
+ }
+ String key = trimText(matcher.group(1));
+ String operator = trimText(matcher.group(2));
+ String value = stripResourceFilterQuotes(trimText(matcher.group(3)));
+ if (!isSafeResourceFilterKey(key) || !StringUtils.hasText(operator) ||
!StringUtils.hasText(value)) {
+ return false;
+ }
+ String prefix = operator.replaceAll("\\s+", " ").equalsIgnoreCase("not
contains")
+ ? RESOURCE_FILTER_NOT_CONTAINS_PREFIX
+ : RESOURCE_FILTER_CONTAINS_PREFIX;
+ includeFilters.computeIfAbsent(key, ignored -> new
LinkedHashSet<>()).add(prefix + value);
+ return true;
+ }
+
+ private boolean appendResourceFilterPresenceValue(Map<String, Set<String>>
includeFilters, String clause) {
+ Matcher matcher =
RESOURCE_FILTER_PRESENCE_OPERATOR_PATTERN.matcher(clause);
+ if (!matcher.matches()) {
+ return false;
+ }
+ String key = trimText(matcher.group(1));
+ String operator = trimText(matcher.group(2));
+ if (!isSafeResourceFilterKey(key) || !StringUtils.hasText(operator)) {
+ return false;
+ }
+ String value = operator.replaceAll("\\s+", " ").equalsIgnoreCase("not
exists")
+ ? RESOURCE_FILTER_NOT_EXISTS_VALUE
+ : RESOURCE_FILTER_EXISTS_VALUE;
+ includeFilters.computeIfAbsent(key, ignored -> new
LinkedHashSet<>()).add(value);
+ return true;
+ }
+
private boolean appendResourceFilterListValues(Map<String, Set<String>>
includeFilters,
Map<String, Set<String>>
excludeFilters,
String clause) {
@@ -1331,6 +1410,39 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
return resourceAttributes.get(key);
}
+ private boolean resourceKeyExists(TraceAggregate trace, String key) {
+ if (trace == null || !StringUtils.hasText(key)) {
+ return false;
+ }
+ if ("service.name".equals(key) &&
StringUtils.hasText(trace.getServiceName())) {
+ return true;
+ }
+ return trace.getResourceAttributes().containsKey(key);
+ }
+
+ private static boolean isComplexResourceFilterValue(String value) {
+ return isContainsResourceFilterValue(value)
+ || isNotContainsResourceFilterValue(value)
+ || isExistsResourceFilterValue(value)
+ || isNotExistsResourceFilterValue(value);
+ }
+
+ private static boolean isContainsResourceFilterValue(String value) {
+ return value != null &&
value.startsWith(RESOURCE_FILTER_CONTAINS_PREFIX);
+ }
+
+ private static boolean isNotContainsResourceFilterValue(String value) {
+ return value != null &&
value.startsWith(RESOURCE_FILTER_NOT_CONTAINS_PREFIX);
+ }
+
+ private static boolean isExistsResourceFilterValue(String value) {
+ return RESOURCE_FILTER_EXISTS_VALUE.equals(value);
+ }
+
+ private static boolean isNotExistsResourceFilterValue(String value) {
+ return RESOURCE_FILTER_NOT_EXISTS_VALUE.equals(value);
+ }
+
private Map<String, Set<String>>
canonicalIdentityValues(ObservedEntityContext entityContext) {
if (entityContext == null) {
return Collections.emptyMap();
@@ -1702,6 +1814,36 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
private boolean hasExclusions() {
return !exclude.isEmpty();
}
+
+ private boolean requiresRowFallback() {
+ return hasExclusions() ||
containsComplexResourceFilterValue(include);
+ }
+
+ private Map<String, Set<String>> pushableInclude() {
+ if (include.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<String, Set<String>> pushable = new LinkedHashMap<>();
+ include.forEach((key, values) -> {
+ Set<String> exactValues = new LinkedHashSet<>();
+ values.stream()
+ .filter(value -> !isComplexResourceFilterValue(value))
+ .forEach(exactValues::add);
+ if (!exactValues.isEmpty()) {
+ pushable.put(key, exactValues);
+ }
+ });
+ return pushable;
+ }
+
+ private boolean containsComplexResourceFilterValue(Map<String,
Set<String>> resourceFilters) {
+ if (resourceFilters.isEmpty()) {
+ return false;
+ }
+ return resourceFilters.values().stream()
+ .flatMap(Set::stream)
+
.anyMatch(EntityTraceQueryServiceImpl::isComplexResourceFilterValue);
+ }
}
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 752d9d4d83..7e8f4cfe07 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
@@ -619,6 +619,75 @@ class EntityTraceQueryServiceImplTest {
org.mockito.ArgumentMatchers.anyInt());
}
+ @Test
+ void
queryTraceListAppliesContainsAndPresenceResourceFiltersWithRowFallback() {
+ 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",
+ "cloud.region", "us-east-1")),
+ traceRow("trace-no-version", "span-root-2", null, "GET
/checkout", "checkout-service", "STATUS_CODE_OK",
+ now - 9_000, 2_000_000L,
+ Map.of("service.name", "checkout-service",
+ "host.name", "checkout-2",
+ "cloud.region", "us-east-1")),
+ traceRow("trace-env", "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.3",
+ "host.name", "checkout-3",
+ "deployment.environment.name", "prod",
+ "cloud.region", "us-east-1")),
+ traceRow("trace-west", "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", "1.2.3",
+ "host.name", "checkout-west",
+ "cloud.region", "us-west-2")),
+ traceRow("trace-inventory", "span-root-5", null, "GET
/inventory", "checkout-service", "STATUS_CODE_OK",
+ now - 6_000, 2_000_000L,
+ Map.of("service.name", "checkout-service",
+ "service.version", "1.2.3",
+ "host.name", "inventory-1",
+ "cloud.region", "us-east-1"))
+ ));
+
+ var page = entityTraceQueryService.queryTraceList(null, start, end,
null,
+ false, "checkout-service", null, null,
+ "service.version=1.2.3 and host.name CONTAINS checkout "
+ + "and deployment.environment.name NOT EXISTS and
cloud.region NOT CONTAINS west",
+ 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(Map.of("service.version", Set.of("1.2.3")),
pushedFilterCaptor.getValue());
+ 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]