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 6650d995e5 Support trace span attribute group by
6650d995e5 is described below
commit 6650d995e527e32e65cd845a9ea06db5e4587a76
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 22:12:57 2026 +0800
Support trace span attribute group by
---
.../service/impl/EntityTraceQueryServiceImpl.java | 90 +++++++++++++++++-
.../impl/EntityTraceQueryServiceImplTest.java | 77 +++++++++++++++
web-next/app/trace/manage/page.test.tsx | 31 +++++++
web-next/app/trace/manage/trace-manage-page.tsx | 103 +++++++++++++++------
4 files changed, 269 insertions(+), 32 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 eb3b4195bd..37fb8d810c 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
@@ -551,6 +551,7 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
Long maxDurationNanos = durationMillisToNanos(maxDurationMs);
if (!StringUtils.hasText(traceId) &&
!resourceFilters.requiresRowFallback()
&& attributeFilters.isEmpty()
+ && !isTraceAttributeGroupBy(normalizedGroupBy)
&& traceQueryRepository.supportsTraceGroupByRows()) {
List<Map<String, Object>> rows =
StringUtils.hasText(normalizedSpanScope)
? traceQueryRepository.queryTraceGroupByRows(
@@ -593,10 +594,15 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
result.put("groups",
rows.stream().map(this::toTraceGroupResult).toList());
return result;
}
- 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,
attributeFilter);
- result.put("groups", buildTraceGroupResults(traces.getContent(),
normalizedGroupBy, resolvedLimit, orderBy, resolvedMinCount));
+ List<TraceAggregate> traces =
aggregateTraceRows(queryRowsForList(traceId, start, end,
queryScope.serviceName(),
+ queryScope.serviceNamespace(), queryScope.environment(),
operationName, minDurationNanos,
+ maxDurationNanos, pushedResourceFilters,
hideInternal)).stream()
+ .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,
attributeFilters))
+ .toList();
+ result.put("groups", buildTraceAggregateGroupResults(traces,
normalizedGroupBy, resolvedLimit, orderBy, resolvedMinCount));
return result;
}
@@ -627,6 +633,24 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
.toList();
}
+ private List<Map<String, Object>>
buildTraceAggregateGroupResults(List<TraceAggregate> traces, String groupBy,
+ int
limit, String orderBy, long minCount) {
+ if (CollectionUtils.isEmpty(traces)) {
+ return List.of();
+ }
+ Map<String, List<TraceAggregate>> grouped = new LinkedHashMap<>();
+ for (TraceAggregate trace : traces) {
+ String value = defaultText(resolveTraceAggregateGroupValue(trace,
groupBy), "unknown");
+ grouped.computeIfAbsent(value, ignored -> new
ArrayList<>()).add(trace);
+ }
+ return grouped.entrySet().stream()
+ .map(entry -> toTraceAggregateGroupResult(entry.getKey(),
entry.getValue()))
+ .filter(group -> ((Long) group.get("traceCount")) >= minCount)
+ .sorted(resolveTraceGroupComparator(orderBy))
+ .limit(limit)
+ .toList();
+ }
+
private Comparator<Map<String, Object>> resolveTraceGroupComparator(String
orderBy) {
String normalized = StringUtils.trimWhitespace(orderBy);
if ("error-count-desc".equalsIgnoreCase(normalized)) {
@@ -670,6 +694,24 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
return group;
}
+ private Map<String, Object> toTraceAggregateGroupResult(String value,
List<TraceAggregate> traces) {
+ Map<String, Object> group = new LinkedHashMap<>();
+ List<Long> durations = traces.stream()
+ .map(TraceAggregate::getDurationNanos)
+ .filter(Objects::nonNull)
+ .filter(duration -> duration >= 0)
+ .sorted()
+ .toList();
+ group.put("value", value);
+ group.put("traceCount", (long) traces.size());
+ group.put("errorTraceCount", traces.stream().filter(trace ->
isErrorStatus(trace.getStatus())).count());
+ group.put("latencyAvgMs", durations.isEmpty() ? 0.0d
+ :
durations.stream().mapToDouble(Long::doubleValue).average().orElse(0.0d) /
1_000_000.0d);
+ group.put("latencyP95Ms", durations.isEmpty() ? 0.0d
+ : durations.get(Math.min(durations.size() - 1, (int)
Math.ceil(durations.size() * 0.95d) - 1)) / 1_000_000.0d);
+ return group;
+ }
+
private String resolveTraceListGroupValue(TraceListItemDto trace, String
groupBy) {
if (trace == null || !StringUtils.hasText(groupBy)) {
return null;
@@ -692,6 +734,38 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
return resourceAttributes.get(groupBy);
}
+ private String resolveTraceAggregateGroupValue(TraceAggregate trace,
String groupBy) {
+ if (trace == null || !StringUtils.hasText(groupBy)) {
+ return null;
+ }
+ Map<String, String> resourceAttributes = trace.getResourceAttributes()
== null
+ ? Collections.emptyMap()
+ : trace.getResourceAttributes();
+ if ("service.name".equals(groupBy)) {
+ return defaultText(trace.getServiceName(),
resourceAttributes.get("service.name"));
+ }
+ if ("operation.name".equals(groupBy)) {
+ return trace.getRootSpanName();
+ }
+ if ("status".equals(groupBy)) {
+ return isErrorStatus(trace.getStatus()) ? "ERROR" : "OK";
+ }
+ if (groupBy.startsWith("resource:")) {
+ return
resourceAttributes.get(groupBy.substring("resource:".length()));
+ }
+ if (groupBy.startsWith("attribute:")) {
+ String key = groupBy.substring("attribute:".length());
+ return trace.spans.stream()
+ .map(TraceSpanNodeDto::getSpanAttributes)
+ .filter(attributes -> !CollectionUtils.isEmpty(attributes))
+ .map(attributes -> attributes.get(key))
+ .filter(StringUtils::hasText)
+ .findFirst()
+ .orElse(null);
+ }
+ return resourceAttributes.get(groupBy);
+ }
+
private String normalizeTraceGroupBy(String groupBy) {
if (!StringUtils.hasText(groupBy)) {
return null;
@@ -711,9 +785,17 @@ public class EntityTraceQueryServiceImpl implements
EntityTraceQueryService {
String key = normalized.substring("resource:".length());
return isSafeResourceFilterKey(key) ? "resource:" + key : null;
}
+ if (normalized.startsWith("attribute:")) {
+ String key = normalized.substring("attribute:".length());
+ return isSafeResourceFilterKey(key) ? "attribute:" + key : null;
+ }
return isSafeResourceFilterKey(normalized) ? normalized : null;
}
+ private boolean isTraceAttributeGroupBy(String groupBy) {
+ return StringUtils.hasText(groupBy) &&
groupBy.startsWith("attribute:");
+ }
+
private TraceOverviewDto toTraceOverview(Map<String, Object> row) {
if (CollectionUtils.isEmpty(row)) {
return 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 7129c681de..c02d55340f 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
@@ -243,6 +243,83 @@ class EntityTraceQueryServiceImplTest {
assertEquals(Set.of("commerce"),
filterCaptor.getValue().get("k8s.namespace.name"));
}
+ @Test
+ void getTraceGroupByStatsGroupsBySpanAttributesWithRowFallback() {
+ long now = System.currentTimeMillis();
+ long start = now - 120_000;
+ long end = now;
+ Map<String, Object> checkoutRoot = traceRow("trace-checkout",
"span-root-1", null, "GET /checkout",
+ "checkout-service", "STATUS_CODE_OK", now - 10_000,
20_000_000L,
+ Map.of("service.name", "checkout-service"));
+ checkoutRoot.put("span_attributes", Map.of("span.kind", "server"));
+ Map<String, Object> checkoutChild = traceRow("trace-checkout",
"span-child-1", "span-root-1", "GET /checkout/{id}",
+ "checkout-service", "STATUS_CODE_OK", now - 9_000, 5_000_000L,
+ Map.of("service.name", "checkout-service"));
+ checkoutChild.put("span_attributes", Map.of("http.route",
"/checkout/{id}"));
+ Map<String, Object> inventoryRoot = traceRow("trace-inventory",
"span-root-2", null, "GET /inventory",
+ "checkout-service", "STATUS_CODE_ERROR", now - 8_000,
30_000_000L,
+ Map.of("service.name", "checkout-service"));
+ inventoryRoot.put("span_attributes", Map.of("http.route",
"/inventory"));
+ Map<String, Object> unknownRoot = traceRow("trace-unknown",
"span-root-3", null, "GET /unknown",
+ "checkout-service", "STATUS_CODE_OK", now - 7_000, 10_000_000L,
+ Map.of("service.name", "checkout-service"));
+ unknownRoot.put("span_attributes", Map.of("span.kind", "server"));
+ 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(checkoutRoot, checkoutChild,
inventoryRoot, unknownRoot));
+
+ Map<String, Object> result =
entityTraceQueryService.getTraceGroupByStats(
+ null,
+ start,
+ end,
+ null,
+ false,
+ "checkout-service",
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ "attribute:http.route",
+ 10,
+ "trace-count-desc",
+ 1,
+ false,
+ "all");
+
+ assertEquals("attribute:http.route", result.get("groupBy"));
+ List<Map<String, Object>> groups = (List<Map<String, Object>>)
result.get("groups");
+ assertEquals(3, groups.size());
+ assertEquals("/checkout/{id}", groups.get(0).get("value"));
+ assertEquals(1L, groups.get(0).get("traceCount"));
+ assertEquals(0L, groups.get(0).get("errorTraceCount"));
+ assertEquals("/inventory", groups.get(1).get("value"));
+ assertEquals(1L, groups.get(1).get("traceCount"));
+ assertEquals(1L, groups.get(1).get("errorTraceCount"));
+ assertEquals("unknown", groups.get(2).get("value"));
+ assertEquals(1L, groups.get(2).get("traceCount"));
+ 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(),
org.mockito.ArgumentMatchers.<Map<String, Set<String>>>any(),
+ eq(false));
+ verify(traceQueryRepository, never()).queryTraceGroupByRows(
+ 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.any(),
+ org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.anyLong(),
+ org.mockito.ArgumentMatchers.anyInt());
+ }
+
@Test
void queryTraceListAndDetailRespectEntityBinding() {
long now = System.currentTimeMillis();
diff --git a/web-next/app/trace/manage/page.test.tsx
b/web-next/app/trace/manage/page.test.tsx
index 725e4a8763..b1030f9fa2 100644
--- a/web-next/app/trace/manage/page.test.tsx
+++ b/web-next/app/trace/manage/page.test.tsx
@@ -809,6 +809,33 @@ describe('trace manage page', () => {
expect(String(mockState.replace.mock.calls[0]?.[0])).toContain('groupBy=resource%3Aservice.version');
}, 15000);
+ it('filters trace attribute group result values back into the attribute
filter route', async () => {
+ mockState.searchParams = new
URLSearchParams('view=list&groupBy=attribute:http.route');
+ interactionContainer = document.createElement('div');
+ document.body.appendChild(interactionContainer);
+ interactionRoot = createRoot(interactionContainer);
+
+ await act(async () => {
+ renderInteractiveTraceManagePage();
+ await Promise.resolve();
+ });
+
+ const attributeValueAction = interactionContainer.querySelector(
+
'[data-trace-manage-group-filter-action="attribute:http.route"][data-trace-manage-group-filter-value="1.2.3"]'
+ ) as HTMLButtonElement | null;
+ expect(attributeValueAction).toBeTruthy();
+
expect(attributeValueAction?.getAttribute('data-trace-manage-group-filter-owner')).toBe('hertzbeat-ui-button');
+ mockState.replace.mockClear();
+
+ await act(async () => {
+ attributeValueAction?.click();
+ await Promise.resolve();
+ });
+
+
expect(String(mockState.replace.mock.calls[0]?.[0])).toContain('attributeFilter=http.route%3D1.2.3');
+
expect(String(mockState.replace.mock.calls[0]?.[0])).toContain('groupBy=attribute%3Ahttp.route');
+ }, 15000);
+
it('saves and restores the current trace explorer query view from shared UI
controls', async () => {
window.localStorage.removeItem('hertzbeat.trace-manage.saved-query-views');
mockState.searchParams = new URLSearchParams(
@@ -1767,11 +1794,13 @@ describe('trace manage page', () => {
expect(source).toContain('selectedResourceAttributeRows');
expect(source).toContain('buildTraceResourceFilterExpression');
expect(source).toContain('buildTraceSpanAttributeFilterExpression');
+ expect(source).toContain('buildTraceSpanAttributeGroupBy');
expect(source).toContain('mergeTraceResourceFilterExpression');
expect(source).toContain('applyTraceResourceFilter');
expect(source).toContain('replaceTraceResourceFilter');
expect(source).toContain('applyTraceSpanAttributeFilter');
expect(source).toContain('replaceTraceSpanAttributeFilter');
+ expect(source).toContain('applyTraceSpanAttributeGroupBy');
expect(source).toContain('buildTraceResourceGroupBy');
expect(source).toContain('applyTraceResourceGroupBy');
expect(source).toContain('data-trace-manage-drawer-span-attributes="span-attributes"');
@@ -1780,6 +1809,8 @@ describe('trace manage page', () => {
expect(source).toContain('data-trace-manage-drawer-span-attribute-filter-action-owner="hertzbeat-ui-button"');
expect(source).toContain('data-trace-manage-drawer-span-attribute-replace-action="true"');
expect(source).toContain('data-trace-manage-drawer-span-attribute-replace-action-owner="hertzbeat-ui-button"');
+
expect(source).toContain('data-trace-manage-drawer-span-attribute-group-action="true"');
+
expect(source).toContain('data-trace-manage-drawer-span-attribute-group-action-owner="hertzbeat-ui-button"');
expect(source).toContain('attributeFilter:
mergeTraceResourceFilterExpression(draft.attributeFilter, expression)');
expect(source).toContain('attributeFilter: expression');
expect(source).toContain("heading={t('trace.manage.drawer.attributes.span.title')}");
diff --git a/web-next/app/trace/manage/trace-manage-page.tsx
b/web-next/app/trace/manage/trace-manage-page.tsx
index 85f3ea1178..25c32ae4fd 100644
--- a/web-next/app/trace/manage/trace-manage-page.tsx
+++ b/web-next/app/trace/manage/trace-manage-page.tsx
@@ -510,6 +510,14 @@ function buildTraceSpanAttributeFilterExpression(name:
React.ReactNode, value: R
return buildTraceResourceFilterExpression(name, value);
}
+function buildTraceSpanAttributeGroupBy(name: React.ReactNode) {
+ const key = String(name ?? '').trim();
+ if (!isSafeTraceResourceFilterKey(key)) {
+ return null;
+ }
+ return `attribute:${key}`;
+}
+
function buildTraceResourceGroupBy(name: React.ReactNode) {
const key = String(name ?? '').trim();
if (!isSafeTraceResourceFilterKey(key)) {
@@ -538,6 +546,11 @@ function buildTraceGroupResultFilter(groupBy: string,
value: string) {
if (!isSafeTraceResourceFilterKey(key)) return null;
return { kind: 'resource' as const, expression:
`${key}=${normalizedValue}` };
}
+ if (normalizedGroupBy.startsWith('attribute:')) {
+ const key = normalizedGroupBy.slice('attribute:'.length);
+ if (!isSafeTraceResourceFilterKey(key)) return null;
+ return { kind: 'attribute' as const, expression:
`${key}=${normalizedValue}` };
+ }
if (!isSafeTraceResourceFilterKey(normalizedGroupBy)) {
return null;
}
@@ -751,6 +764,7 @@ function TraceWaterfallDrawer({
onApplyOperationFilter,
onApplySpanAttributeFilter,
onReplaceSpanAttributeFilter,
+ onApplySpanAttributeGroupBy,
onApplyResourceFilter,
onReplaceResourceFilter,
onApplyResourceGroupBy
@@ -766,6 +780,7 @@ function TraceWaterfallDrawer({
onApplyOperationFilter: (operationName: string) => void;
onApplySpanAttributeFilter: (name: string, value: string) => void;
onReplaceSpanAttributeFilter: (name: string, value: string) => void;
+ onApplySpanAttributeGroupBy: (name: string) => void;
onApplyResourceFilter: (name: string, value: string) => void;
onReplaceResourceFilter: (name: string, value: string) => void;
onApplyResourceGroupBy: (name: string) => void;
@@ -1202,38 +1217,56 @@ function TraceWaterfallDrawer({
title: row.title,
copy: row.copy,
meta: row.meta,
- action: buildTraceSpanAttributeFilterExpression(row.title,
row.copy) ? (
+ action: buildTraceSpanAttributeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeGroupBy(row.title) ? (
<HzActionGroup
data-trace-manage-drawer-span-attribute-action-group="filter-group"
data-trace-manage-drawer-span-attribute-action-group-owner="hertzbeat-ui-action-group"
layout="end-wrap"
>
- <HzButton
-
data-trace-manage-drawer-span-attribute-filter-action="true"
-
data-trace-manage-drawer-span-attribute-filter-action-owner="hertzbeat-ui-button"
-
data-trace-manage-drawer-span-attribute-filter-name={row.title}
-
data-trace-manage-drawer-span-attribute-filter-value={row.copy}
- size="sm"
- intent="secondary"
- onClick={() => onApplySpanAttributeFilter(row.title,
row.copy)}
-
aria-label={t('trace.manage.drawer.attributes.filter-action.aria', { name:
row.title, value: row.copy })}
- >
- <HzButtonIcon icon={Filter}
data-trace-manage-drawer-span-attribute-filter-action-icon="filter"
data-trace-manage-drawer-span-attribute-filter-action-icon-owner="hertzbeat-ui-button-icon"
/>
- {t('trace.manage.drawer.attributes.filter-action')}
- </HzButton>
- <HzButton
-
data-trace-manage-drawer-span-attribute-replace-action="true"
-
data-trace-manage-drawer-span-attribute-replace-action-owner="hertzbeat-ui-button"
-
data-trace-manage-drawer-span-attribute-filter-name={row.title}
-
data-trace-manage-drawer-span-attribute-filter-value={row.copy}
- size="sm"
- intent="secondary"
- onClick={() =>
onReplaceSpanAttributeFilter(row.title, row.copy)}
-
aria-label={t('trace.manage.drawer.attributes.replace-action.aria', { name:
row.title, value: row.copy })}
- >
- <HzButtonIcon icon={Replace}
data-trace-manage-drawer-span-attribute-replace-action-icon="replace"
data-trace-manage-drawer-span-attribute-replace-action-icon-owner="hertzbeat-ui-button-icon"
/>
- {t('trace.manage.drawer.attributes.replace-action')}
- </HzButton>
+ {buildTraceSpanAttributeFilterExpression(row.title,
row.copy) ? (
+ <>
+ <HzButton
+
data-trace-manage-drawer-span-attribute-filter-action="true"
+
data-trace-manage-drawer-span-attribute-filter-action-owner="hertzbeat-ui-button"
+
data-trace-manage-drawer-span-attribute-filter-name={row.title}
+
data-trace-manage-drawer-span-attribute-filter-value={row.copy}
+ size="sm"
+ intent="secondary"
+ onClick={() =>
onApplySpanAttributeFilter(row.title, row.copy)}
+
aria-label={t('trace.manage.drawer.attributes.filter-action.aria', { name:
row.title, value: row.copy })}
+ >
+ <HzButtonIcon icon={Filter}
data-trace-manage-drawer-span-attribute-filter-action-icon="filter"
data-trace-manage-drawer-span-attribute-filter-action-icon-owner="hertzbeat-ui-button-icon"
/>
+
{t('trace.manage.drawer.attributes.filter-action')}
+ </HzButton>
+ <HzButton
+
data-trace-manage-drawer-span-attribute-replace-action="true"
+
data-trace-manage-drawer-span-attribute-replace-action-owner="hertzbeat-ui-button"
+
data-trace-manage-drawer-span-attribute-filter-name={row.title}
+
data-trace-manage-drawer-span-attribute-filter-value={row.copy}
+ size="sm"
+ intent="secondary"
+ onClick={() =>
onReplaceSpanAttributeFilter(row.title, row.copy)}
+
aria-label={t('trace.manage.drawer.attributes.replace-action.aria', { name:
row.title, value: row.copy })}
+ >
+ <HzButtonIcon icon={Replace}
data-trace-manage-drawer-span-attribute-replace-action-icon="replace"
data-trace-manage-drawer-span-attribute-replace-action-icon-owner="hertzbeat-ui-button-icon"
/>
+
{t('trace.manage.drawer.attributes.replace-action')}
+ </HzButton>
+ </>
+ ) : null}
+ {buildTraceSpanAttributeGroupBy(row.title) ? (
+ <HzButton
+
data-trace-manage-drawer-span-attribute-group-action="true"
+
data-trace-manage-drawer-span-attribute-group-action-owner="hertzbeat-ui-button"
+
data-trace-manage-drawer-span-attribute-group-name={row.title}
+ size="sm"
+ intent="secondary"
+ onClick={() =>
onApplySpanAttributeGroupBy(row.title)}
+
aria-label={t('trace.manage.drawer.attributes.group-action.aria', { name:
row.title })}
+ >
+ <HzButtonIcon icon={BarChart3}
data-trace-manage-drawer-span-attribute-group-action-icon="group"
data-trace-manage-drawer-span-attribute-group-action-icon-owner="hertzbeat-ui-button-icon"
/>
+ {t('trace.manage.drawer.attributes.group-action')}
+ </HzButton>
+ ) : null}
</HzActionGroup>
) : null
}))}
@@ -1821,6 +1854,17 @@ function TraceExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft]);
+ const applyTraceSpanAttributeGroupBy = useCallback((name: string) => {
+ const groupBy = buildTraceSpanAttributeGroupBy(name);
+ if (!groupBy) return;
+ const nextQuery: TraceQueryState = {
+ ...draft,
+ groupBy
+ };
+ setDraft(nextQuery);
+ applyQuery(nextQuery);
+ }, [applyQuery, draft, setDraft]);
+
const applyTraceResourceGroupBy = useCallback((name: string) => {
const groupBy = buildTraceResourceGroupBy(name);
if (!groupBy) return;
@@ -1873,7 +1917,9 @@ function TraceExplorer({
? { operationName: filter.value }
: filter.kind === 'status'
? { errorOnly: filter.errorOnly }
- : { resourceFilter:
mergeTraceResourceFilterExpression(draft.resourceFilter, filter.expression) })
+ : filter.kind === 'attribute'
+ ? { attributeFilter:
mergeTraceResourceFilterExpression(draft.attributeFilter, filter.expression) }
+ : { resourceFilter:
mergeTraceResourceFilterExpression(draft.resourceFilter, filter.expression) })
};
setDraft(nextQuery);
applyQuery(nextQuery);
@@ -3355,6 +3401,7 @@ function TraceExplorer({
onApplyOperationFilter={operationName =>
applyTraceQuickFilter('operationName', operationName)}
onApplySpanAttributeFilter={applyTraceSpanAttributeFilter}
onReplaceSpanAttributeFilter={replaceTraceSpanAttributeFilter}
+ onApplySpanAttributeGroupBy={applyTraceSpanAttributeGroupBy}
onApplyResourceFilter={applyTraceResourceFilter}
onReplaceResourceFilter={replaceTraceResourceFilter}
onApplyResourceGroupBy={applyTraceResourceGroupBy}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]