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 7d11789354 Support trace span attribute filters
7d11789354 is described below
commit 7d11789354e6c193f130103f7054618936097e51
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 21:51:24 2026 +0800
Support trace span attribute filters
---
.../traces/controller/TraceQueryController.java | 10 +-
.../traces/service/EntityTraceQueryService.java | 29 ++++++
.../service/impl/EntityTraceQueryServiceImpl.java | 104 ++++++++++++++++++++-
.../controller/TraceQueryControllerTest.java | 21 +++--
.../impl/EntityTraceQueryServiceImplTest.java | 49 ++++++++++
5 files changed, 195 insertions(+), 18 deletions(-)
diff --git
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryController.java
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryController.java
index 3a18a62e8f..51e15e4ee5 100644
---
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryController.java
+++
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryController.java
@@ -62,6 +62,7 @@ public class TraceQueryController {
@RequestParam(value = "serviceNamespace", required = false) String
serviceNamespace,
@RequestParam(value = "environment", required = false) String
environment,
@RequestParam(value = "resourceFilter", required = false) String
resourceFilter,
+ @RequestParam(value = "attributeFilter", required = false) String
attributeFilter,
@RequestParam(value = "operationName", required = false) String
operationName,
@RequestParam(value = "minDurationMs", required = false) Long
minDurationMs,
@RequestParam(value = "maxDurationMs", required = false) Long
maxDurationMs,
@@ -73,7 +74,7 @@ public class TraceQueryController {
Page<TraceListItemDto> page = entityTraceQueryService.queryTraceList(
entityId, start, end, traceId, errorOnly, serviceName,
serviceNamespace, environment,
scopedResourceFilter, operationName, minDurationMs,
maxDurationMs, pageIndex, pageSize, hideInternal,
- spanScope);
+ spanScope, attributeFilter);
return ResponseEntity.ok(Message.success(page));
}
@@ -90,6 +91,7 @@ public class TraceQueryController {
@RequestParam(value = "serviceNamespace", required = false) String
serviceNamespace,
@RequestParam(value = "environment", required = false) String
environment,
@RequestParam(value = "resourceFilter", required = false) String
resourceFilter,
+ @RequestParam(value = "attributeFilter", required = false) String
attributeFilter,
@RequestParam(value = "operationName", required = false) String
operationName,
@RequestParam(value = "minDurationMs", required = false) Long
minDurationMs,
@RequestParam(value = "maxDurationMs", required = false) Long
maxDurationMs,
@@ -98,7 +100,8 @@ public class TraceQueryController {
String scopedResourceFilter =
mergeEntityContextResourceFilter(entityId, entityType, resourceFilter);
return
ResponseEntity.ok(Message.success(entityTraceQueryService.getTraceOverview(
entityId, start, end, traceId, errorOnly, serviceName,
serviceNamespace, environment,
- scopedResourceFilter, operationName, minDurationMs,
maxDurationMs, hideInternal, spanScope)));
+ scopedResourceFilter, operationName, minDurationMs,
maxDurationMs, hideInternal, spanScope,
+ attributeFilter)));
}
@GetMapping("/stats/group-by")
@@ -114,6 +117,7 @@ public class TraceQueryController {
@RequestParam(value = "serviceNamespace", required = false) String
serviceNamespace,
@RequestParam(value = "environment", required = false) String
environment,
@RequestParam(value = "resourceFilter", required = false) String
resourceFilter,
+ @RequestParam(value = "attributeFilter", required = false) String
attributeFilter,
@RequestParam(value = "operationName", required = false) String
operationName,
@RequestParam(value = "minDurationMs", required = false) Long
minDurationMs,
@RequestParam(value = "maxDurationMs", required = false) Long
maxDurationMs,
@@ -127,7 +131,7 @@ public class TraceQueryController {
return
ResponseEntity.ok(Message.success(entityTraceQueryService.getTraceGroupByStats(
entityId, start, end, traceId, errorOnly, serviceName,
serviceNamespace, environment,
scopedResourceFilter, operationName, minDurationMs,
maxDurationMs, groupBy, limit, orderBy, minCount,
- hideInternal, spanScope)));
+ hideInternal, spanScope, attributeFilter)));
}
@GetMapping("/{traceId}")
diff --git
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/EntityTraceQueryService.java
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/EntityTraceQueryService.java
index 0a5fb0542b..339fea9cc3 100644
---
a/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/EntityTraceQueryService.java
+++
b/hertzbeat-observability/src/main/java/org/apache/hertzbeat/observability/traces/service/EntityTraceQueryService.java
@@ -63,6 +63,16 @@ public interface EntityTraceQueryService {
String resourceFilter,
String operationName, Long minDurationMs,
Long maxDurationMs, int
pageIndex, int pageSize, Boolean hideInternal,
String spanScope) {
+ return queryTraceList(entityId, start, end, traceId, errorOnly,
serviceName, serviceNamespace, environment,
+ resourceFilter, operationName, minDurationMs, maxDurationMs,
pageIndex, pageSize, hideInternal,
+ spanScope, null);
+ }
+
+ default Page<TraceListItemDto> queryTraceList(Long entityId, Long start,
Long end, String traceId, Boolean errorOnly,
+ String serviceName, String
serviceNamespace, String environment,
+ String resourceFilter,
String operationName, Long minDurationMs,
+ Long maxDurationMs, int
pageIndex, int pageSize, Boolean hideInternal,
+ String spanScope, String
attributeFilter) {
return queryTraceList(entityId, start, end, traceId, errorOnly,
serviceName, serviceNamespace, environment,
resourceFilter, operationName, minDurationMs, maxDurationMs,
pageIndex, pageSize, hideInternal);
}
@@ -92,6 +102,14 @@ public interface EntityTraceQueryService {
String serviceName, String
serviceNamespace, String environment,
String resourceFilter, String
operationName, Long minDurationMs, Long maxDurationMs,
Boolean hideInternal, String
spanScope) {
+ return getTraceOverview(entityId, start, end, traceId, errorOnly,
serviceName, serviceNamespace, environment,
+ resourceFilter, operationName, minDurationMs, maxDurationMs,
hideInternal, spanScope, null);
+ }
+
+ default TraceOverviewDto getTraceOverview(Long entityId, Long start, Long
end, String traceId, Boolean errorOnly,
+ String serviceName, String
serviceNamespace, String environment,
+ String resourceFilter, String
operationName, Long minDurationMs, Long maxDurationMs,
+ Boolean hideInternal, String
spanScope, String attributeFilter) {
return getTraceOverview(entityId, start, end, traceId, errorOnly,
serviceName, serviceNamespace, environment,
resourceFilter, operationName, minDurationMs, maxDurationMs,
hideInternal);
}
@@ -107,6 +125,17 @@ public interface EntityTraceQueryService {
String resourceFilter,
String operationName, Long minDurationMs,
Long maxDurationMs,
String groupBy, Integer limit, String orderBy,
Integer minCount, Boolean
hideInternal, String spanScope) {
+ return getTraceGroupByStats(entityId, start, end, traceId, errorOnly,
serviceName, serviceNamespace,
+ environment, resourceFilter, operationName, minDurationMs,
maxDurationMs, groupBy, limit, orderBy,
+ minCount, hideInternal, spanScope, null);
+ }
+
+ default Map<String, Object> getTraceGroupByStats(Long entityId, Long
start, Long end, String traceId, Boolean errorOnly,
+ String serviceName,
String serviceNamespace, String environment,
+ String resourceFilter,
String operationName, Long minDurationMs,
+ Long maxDurationMs,
String groupBy, Integer limit, String orderBy,
+ Integer minCount, Boolean
hideInternal, String spanScope,
+ String attributeFilter) {
return getTraceGroupByStats(entityId, start, end, traceId, errorOnly,
serviceName, serviceNamespace,
environment, resourceFilter, operationName, minDurationMs,
maxDurationMs, groupBy, limit, orderBy,
minCount, hideInternal);
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 2684d038eb..eb3b4195bd 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
@@ -242,11 +242,23 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
String resourceFilter, String
operationName, Long minDurationMs,
Long maxDurationMs, int
pageIndex, int pageSize,
Boolean hideInternal, String
spanScope) {
+ return queryTraceList(entityId, start, end, traceId, errorOnly,
serviceName, serviceNamespace, environment,
+ resourceFilter, operationName, minDurationMs, maxDurationMs,
pageIndex, pageSize, hideInternal,
+ spanScope, null);
+ }
+
+ @Override
+ public Page<TraceListItemDto> queryTraceList(Long entityId, Long start,
Long end, String traceId, Boolean errorOnly,
+ String serviceName, String
serviceNamespace, String environment,
+ String resourceFilter, String
operationName, Long minDurationMs,
+ Long maxDurationMs, int
pageIndex, int pageSize,
+ Boolean hideInternal, String
spanScope, String attributeFilter) {
ObservedEntityContext entityContext = entityId == null ? null :
loadEntityContext(entityId);
Map<String, Set<String>> identityValues =
canonicalIdentityValues(entityContext);
TraceQueryScope queryScope = resolveTraceQueryScope(entityContext,
identityValues, serviceName, serviceNamespace, environment);
ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
identityValues, parseResourceFilters(resourceFilter));
+ ResourceFilterSet attributeFilters =
parseResourceFilters(attributeFilter);
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));
@@ -254,6 +266,7 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
Long maxDurationNanos = durationMillisToNanos(maxDurationMs);
String normalizedSpanScope = normalizeSpanScope(spanScope);
if (!StringUtils.hasText(traceId) &&
!resourceFilters.requiresRowFallback()
+ && attributeFilters.isEmpty()
&& traceQueryRepository.supportsTraceListRows()) {
List<Map<String, Object>> rows =
StringUtils.hasText(normalizedSpanScope)
? traceQueryRepository.queryTraceListRows(
@@ -305,7 +318,7 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
.filter(trace -> matchesSpanScope(trace, normalizedSpanScope))
.filter(trace -> matchesTraceFilters(trace, identityValues,
resourceFilters, start, end, traceId, errorOnly,
queryScope.serviceName(),
queryScope.serviceNamespace(), queryScope.environment(), operationName,
- minDurationNanos, maxDurationNanos, hideInternal))
+ minDurationNanos, maxDurationNanos, hideInternal,
attributeFilters))
.sorted(Comparator.comparing(TraceAggregate::getStartTime,
Comparator.nullsLast(Comparator.reverseOrder())))
.toList();
int safeStart = Math.min(repositoryOffset, filtered.size());
@@ -376,16 +389,27 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
String serviceName, String
serviceNamespace, String environment,
String resourceFilter, String
operationName, Long minDurationMs, Long maxDurationMs,
Boolean hideInternal, String
spanScope) {
+ return getTraceOverview(entityId, start, end, traceId, errorOnly,
serviceName, serviceNamespace, environment,
+ resourceFilter, operationName, minDurationMs, maxDurationMs,
hideInternal, spanScope, null);
+ }
+
+ @Override
+ public TraceOverviewDto getTraceOverview(Long entityId, Long start, Long
end, String traceId, Boolean errorOnly,
+ String serviceName, String
serviceNamespace, String environment,
+ String resourceFilter, String
operationName, Long minDurationMs, Long maxDurationMs,
+ Boolean hideInternal, String
spanScope, String attributeFilter) {
ObservedEntityContext entityContext = entityId == null ? null :
loadEntityContext(entityId);
Map<String, Set<String>> identityValues =
canonicalIdentityValues(entityContext);
TraceQueryScope queryScope = resolveTraceQueryScope(entityContext,
identityValues, serviceName, serviceNamespace, environment);
ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
identityValues, parseResourceFilters(resourceFilter));
+ ResourceFilterSet attributeFilters =
parseResourceFilters(attributeFilter);
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.requiresRowFallback()
+ && attributeFilters.isEmpty()
&& traceQueryRepository.supportsTraceIdOverviewRows()) {
Map<String, Object> row = StringUtils.hasText(normalizedSpanScope)
? traceQueryRepository.queryTraceIdOverviewRows(
@@ -425,6 +449,7 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
}
}
if (!StringUtils.hasText(traceId) &&
!resourceFilters.requiresRowFallback()
+ && attributeFilters.isEmpty()
&& traceQueryRepository.supportsTraceOverviewRows()) {
Map<String, Object> row = StringUtils.hasText(normalizedSpanScope)
? traceQueryRepository.queryTraceOverviewRows(
@@ -464,7 +489,7 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
Page<TraceListItemDto> result = queryTraceList(entityId, start, end,
traceId, errorOnly,
queryScope.serviceName(), queryScope.serviceNamespace(),
queryScope.environment(),
resourceFilter, operationName, minDurationMs, maxDurationMs,
0, TRACE_LIST_SAMPLE_LIMIT, hideInternal,
- normalizedSpanScope);
+ normalizedSpanScope, attributeFilter);
Long latestObservedAt = result.getContent().stream()
.map(TraceListItemDto::getStartTime)
.filter(Objects::nonNull)
@@ -493,6 +518,18 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
Long minDurationMs, Long
maxDurationMs, String groupBy,
Integer limit, String
orderBy, Integer minCount, Boolean hideInternal,
String spanScope) {
+ return getTraceGroupByStats(entityId, start, end, traceId, errorOnly,
serviceName, serviceNamespace,
+ environment, resourceFilter, operationName, minDurationMs,
maxDurationMs, groupBy, limit, orderBy,
+ minCount, hideInternal, spanScope, null);
+ }
+
+ @Override
+ public Map<String, Object> getTraceGroupByStats(Long entityId, Long start,
Long end, String traceId,
+ Boolean errorOnly, String
serviceName, String serviceNamespace,
+ String environment, String
resourceFilter, String operationName,
+ Long minDurationMs, Long
maxDurationMs, String groupBy,
+ Integer limit, String
orderBy, Integer minCount, Boolean hideInternal,
+ String spanScope, String
attributeFilter) {
String normalizedGroupBy = normalizeTraceGroupBy(groupBy);
int resolvedLimit = resolveTraceGroupByLimit(limit);
long resolvedMinCount = resolveTraceGroupByMinCount(minCount);
@@ -508,10 +545,12 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
TraceQueryScope queryScope = resolveTraceQueryScope(entityContext,
identityValues, serviceName, serviceNamespace, environment);
ResourceFilterSet resourceFilters = removeEntityScopeResourceFilters(
identityValues, parseResourceFilters(resourceFilter));
+ ResourceFilterSet attributeFilters =
parseResourceFilters(attributeFilter);
Map<String, Set<String>> pushedResourceFilters =
mergeResourceFilters(identityValues, resourceFilters.pushableInclude());
Long minDurationNanos = durationMillisToNanos(minDurationMs);
Long maxDurationNanos = durationMillisToNanos(maxDurationMs);
if (!StringUtils.hasText(traceId) &&
!resourceFilters.requiresRowFallback()
+ && attributeFilters.isEmpty()
&& traceQueryRepository.supportsTraceGroupByRows()) {
List<Map<String, Object>> rows =
StringUtils.hasText(normalizedSpanScope)
? traceQueryRepository.queryTraceGroupByRows(
@@ -556,7 +595,7 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
}
Page<TraceListItemDto> traces = queryTraceList(entityId, start, end,
traceId, errorOnly, queryScope.serviceName(),
queryScope.serviceNamespace(), queryScope.environment(),
resourceFilter, operationName, minDurationMs, maxDurationMs,
- 0, TRACE_LIST_SAMPLE_LIMIT, hideInternal, normalizedSpanScope);
+ 0, TRACE_LIST_SAMPLE_LIMIT, hideInternal, normalizedSpanScope,
attributeFilter);
result.put("groups", buildTraceGroupResults(traces.getContent(),
normalizedGroupBy, resolvedLimit, orderBy, resolvedMinCount));
return result;
}
@@ -921,7 +960,8 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
ResourceFilterSet resourceFilters,
Long start, Long end,
String traceId, Boolean errorOnly,
String serviceName, String serviceNamespace,
String environment, String
operationName, Long minDurationNanos,
- Long maxDurationNanos, Boolean
hideInternal) {
+ Long maxDurationNanos, Boolean
hideInternal,
+ ResourceFilterSet attributeFilters) {
if (trace == null) {
return false;
}
@@ -964,7 +1004,8 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
if (!identityValues.isEmpty() && !matchesEntity(trace,
identityValues)) {
return false;
}
- return resourceFilters.isEmpty() || matchesResourceFilters(trace,
resourceFilters);
+ return (resourceFilters.isEmpty() || matchesResourceFilters(trace,
resourceFilters))
+ && matchesSpanAttributeFilters(trace, attributeFilters);
}
private String normalizeSpanScope(String spanScope) {
@@ -1061,6 +1102,59 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
&& matchesExcludedResourceFilters(trace,
resourceFilters.exclude());
}
+ private boolean matchesSpanAttributeFilters(TraceAggregate trace,
ResourceFilterSet attributeFilters) {
+ if (attributeFilters == null || attributeFilters.isEmpty()) {
+ return true;
+ }
+ if (trace == null || CollectionUtils.isEmpty(trace.spans)) {
+ return false;
+ }
+ return trace.spans.stream()
+ .anyMatch(span -> matchesFilterMap(span.getSpanAttributes(),
attributeFilters));
+ }
+
+ private boolean matchesFilterMap(Map<String, String> values,
ResourceFilterSet filters) {
+ Map<String, String> source = values == null ? Collections.emptyMap() :
values;
+ return matchesIncludedFilterMap(source, filters.include())
+ && matchesExcludedFilterMap(source, filters.exclude());
+ }
+
+ private boolean matchesIncludedFilterMap(Map<String, String> source,
Map<String, Set<String>> filters) {
+ if (filters.isEmpty()) {
+ return true;
+ }
+ for (Map.Entry<String, Set<String>> entry : filters.entrySet()) {
+ String actual = trimText(source.get(entry.getKey()));
+ boolean keyExists = source.containsKey(entry.getKey());
+ boolean matched = entry.getValue().stream()
+ .filter(StringUtils::hasText)
+ .anyMatch(expected -> matchesResourceFilterValue(actual,
expected, keyExists));
+ if (!matched) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean matchesExcludedFilterMap(Map<String, String> source,
Map<String, Set<String>> filters) {
+ if (filters.isEmpty()) {
+ return true;
+ }
+ for (Map.Entry<String, Set<String>> entry : filters.entrySet()) {
+ String actual = trimText(source.get(entry.getKey()));
+ if (!StringUtils.hasText(actual)) {
+ continue;
+ }
+ boolean excluded = entry.getValue().stream()
+ .filter(StringUtils::hasText)
+ .anyMatch(expected ->
matchesExactResourceFilterValue(actual, expected));
+ if (excluded) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private boolean matchesIncludedResourceFilters(TraceAggregate trace,
Map<String, Set<String>> resourceFilters) {
if (resourceFilters.isEmpty()) {
return true;
diff --git
a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryControllerTest.java
b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryControllerTest.java
index f5ab96e167..4b8f92fbea 100644
---
a/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryControllerTest.java
+++
b/hertzbeat-observability/src/test/java/org/apache/hertzbeat/observability/traces/controller/TraceQueryControllerTest.java
@@ -69,7 +69,7 @@ class TraceQueryControllerTest {
when(entityTraceQueryService.queryTraceList(
1L, 100L, 200L, "trace-1", true, "checkout", "commerce",
"prod",
"service.version=1.2.3 and hertzbeat.entity_id=\"1\" and
hertzbeat.entity_type=\"service\"", "GET /checkout",
- 100L, 500L, 2, 50, true, null))
+ 100L, 500L, 2, 50, true, null, "http.route CONTAINS checkout"))
.thenReturn(new PageImpl<>(List.of(item), PageRequest.of(2,
50), 1));
mockMvc.perform(get("/api/traces/list")
@@ -83,6 +83,7 @@ class TraceQueryControllerTest {
.param("serviceNamespace", "commerce")
.param("environment", "prod")
.param("resourceFilter", "service.version=1.2.3")
+ .param("attributeFilter", "http.route CONTAINS
checkout")
.param("operationName", "GET /checkout")
.param("minDurationMs", "100")
.param("maxDurationMs", "500")
@@ -97,7 +98,7 @@ class TraceQueryControllerTest {
verify(entityTraceQueryService).queryTraceList(
1L, 100L, 200L, "trace-1", true, "checkout", "commerce",
"prod",
"service.version=1.2.3 and hertzbeat.entity_id=\"1\" and
hertzbeat.entity_type=\"service\"", "GET /checkout",
- 100L, 500L, 2, 50, true, null);
+ 100L, 500L, 2, 50, true, null, "http.route CONTAINS checkout");
}
@Test
@@ -116,7 +117,7 @@ class TraceQueryControllerTest {
);
when(entityTraceQueryService.queryTraceList(
null, 100L, 200L, null, false, "checkout", null, "prod",
- null, "POST /checkout", 100L, 500L, 0, 20, null, "entrypoint"))
+ null, "POST /checkout", 100L, 500L, 0, 20, null, "entrypoint",
null))
.thenReturn(new PageImpl<>(List.of(item), PageRequest.of(0,
20), 1));
mockMvc.perform(get("/api/traces/list")
@@ -135,7 +136,7 @@ class TraceQueryControllerTest {
verify(entityTraceQueryService).queryTraceList(
null, 100L, 200L, null, false, "checkout", null, "prod",
- null, "POST /checkout", 100L, 500L, 0, 20, null, "entrypoint");
+ null, "POST /checkout", 100L, 500L, 0, 20, null, "entrypoint",
null);
}
@Test
@@ -143,7 +144,7 @@ class TraceQueryControllerTest {
TraceOverviewDto overview = new TraceOverviewDto(2, 1,
1_710_000_000_000L, true);
when(entityTraceQueryService.getTraceOverview(
9L, 100L, 200L, "trace-9", false, "payments", "core", "stage",
- "service.version=2.0.0 and hertzbeat.entity_id=\"9\"", "POST
/pay", 200L, 900L, true, null))
+ "service.version=2.0.0 and hertzbeat.entity_id=\"9\"", "POST
/pay", 200L, 900L, true, null, null))
.thenReturn(overview);
mockMvc.perform(get("/api/traces/stats/overview")
@@ -168,7 +169,7 @@ class TraceQueryControllerTest {
verify(entityTraceQueryService).getTraceOverview(
9L, 100L, 200L, "trace-9", false, "payments", "core", "stage",
- "service.version=2.0.0 and hertzbeat.entity_id=\"9\"", "POST
/pay", 200L, 900L, true, null);
+ "service.version=2.0.0 and hertzbeat.entity_id=\"9\"", "POST
/pay", 200L, 900L, true, null, null);
}
@Test
@@ -176,7 +177,7 @@ class TraceQueryControllerTest {
TraceOverviewDto overview = new TraceOverviewDto(3, 0,
1_710_000_000_000L, true);
when(entityTraceQueryService.getTraceOverview(
null, 100L, 200L, null, false, "checkout", null, "prod",
- null, "POST /checkout", 100L, 500L, null, "entrypoint"))
+ null, "POST /checkout", 100L, 500L, null, "entrypoint", null))
.thenReturn(overview);
mockMvc.perform(get("/api/traces/stats/overview")
@@ -195,7 +196,7 @@ class TraceQueryControllerTest {
verify(entityTraceQueryService).getTraceOverview(
null, 100L, 200L, null, false, "checkout", null, "prod",
- null, "POST /checkout", 100L, 500L, null, "entrypoint");
+ null, "POST /checkout", 100L, 500L, null, "entrypoint", null);
}
@Test
@@ -213,7 +214,7 @@ class TraceQueryControllerTest {
when(entityTraceQueryService.getTraceGroupByStats(
3L, 100L, 200L, "trace-3", true, "checkout", "commerce",
"prod",
"host.name=checkout-1 and hertzbeat.entity_id=\"3\"", "GET
/checkout", 100L, 500L,
- "resource:service.version", 7, "latency-p95-desc", 5, true,
"entrypoint"))
+ "resource:service.version", 7, "latency-p95-desc", 5, true,
"entrypoint", null))
.thenReturn(result);
mockMvc.perform(get("/api/traces/stats/group-by")
@@ -247,6 +248,6 @@ class TraceQueryControllerTest {
verify(entityTraceQueryService).getTraceGroupByStats(
3L, 100L, 200L, "trace-3", true, "checkout", "commerce",
"prod",
"host.name=checkout-1 and hertzbeat.entity_id=\"3\"", "GET
/checkout", 100L, 500L,
- "resource:service.version", 7, "latency-p95-desc", 5, true,
"entrypoint");
+ "resource:service.version", 7, "latency-p95-desc", 5, true,
"entrypoint", null);
}
}
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 7e8f4cfe07..7129c681de 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
@@ -688,6 +688,55 @@ class EntityTraceQueryServiceImplTest {
org.mockito.ArgumentMatchers.anyInt());
}
+ @Test
+ void queryTraceListAppliesSpanAttributeFiltersWithRowFallback() {
+ long now = System.currentTimeMillis();
+ long start = now - 120_000;
+ long end = now;
+ Map<String, Object> matchingRow = traceRow("trace-checkout",
"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"));
+ matchingRow.put("span_attributes", Map.of("http.route",
"/checkout/{id}", "span.kind", "server"));
+ Map<String, Object> inventoryRow = traceRow("trace-inventory",
"span-root-2", null, "GET /inventory",
+ "checkout-service", "STATUS_CODE_OK", now - 9_000, 2_000_000L,
+ Map.of("service.name", "checkout-service", "service.version",
"1.2.3"));
+ inventoryRow.put("span_attributes", Map.of("http.route", "/inventory",
"span.kind", "server"));
+ Map<String, Object> databaseRow = traceRow("trace-db", "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"));
+ databaseRow.put("span_attributes", Map.of("http.route",
"/checkout/{id}", "db.system", "mysql"));
+ 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(matchingRow, inventoryRow,
databaseRow));
+
+ var page = entityTraceQueryService.queryTraceList(null, start, end,
null,
+ false, "checkout-service", null, null,
+ "service.version=1.2.3", null, null, null, 0, 20, false, null,
+ "http.route CONTAINS checkout and db.system NOT EXISTS");
+
+ assertEquals(1, page.getTotalElements());
+ assertEquals("trace-checkout",
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]