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]

Reply via email to