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 8b2925951f Add metrics label exclude actions
8b2925951f is described below

commit 8b2925951f7746afd01ad73e2d1e1810cb85192d
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 22:25:16 2026 +0800

    Add metrics label exclude actions
---
 .../ingestion/otlp/metrics/otlp-metrics-page.tsx   | 40 ++++++++++++++++++++++
 web-next/app/ingestion/otlp/metrics/page.test.tsx  | 24 +++++++++++++
 web-next/lib/i18n-runtime-messages.ts              |  6 ++++
 3 files changed, 70 insertions(+)

diff --git a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx 
b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
index 96d4a45313..17108227e4 100644
--- a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
+++ b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
@@ -117,6 +117,13 @@ function buildMetricAttributeFilterExpression(name: 
string, value: string) {
   return `${trimmedName}="${escapeMetricFilterValue(trimmedValue)}"`;
 }
 
+function buildMetricAttributeExcludeFilterExpression(name: string, value: 
string) {
+  const trimmedName = name.trim();
+  const trimmedValue = value.trim();
+  if (!trimmedName || !trimmedValue) return null;
+  return `${trimmedName}!="${escapeMetricFilterValue(trimmedValue)}"`;
+}
+
 function mergeMetricFilterExpression(currentFilter: string, expression: 
string) {
   const trimmedFilter = currentFilter.trim();
   if (!trimmedFilter) return expression;
@@ -799,6 +806,17 @@ export default function OtlpMetricsPage() {
     replaceMetricsRoute(nextDraft, undefined, series?.key || query.series, 
series ? buildMetricSeriesRouteContext(series) : {});
   }, [draft, query.series, replaceMetricsRoute]);
 
+  const applyMetricAttributeExcludeFilter = useCallback((name: string, value: 
string, series?: OtlpMetricSeriesView | null) => {
+    const expression = buildMetricAttributeExcludeFilterExpression(name, 
value);
+    if (!expression) return;
+    const nextDraft = {
+      ...draft,
+      filter: mergeMetricFilterExpression(draft.filter, expression)
+    };
+    setDraft(nextDraft);
+    replaceMetricsRoute(nextDraft, undefined, series?.key || query.series, 
series ? buildMetricSeriesRouteContext(series) : {});
+  }, [draft, query.series, replaceMetricsRoute]);
+
   const applyMetricAttributeReplaceFilter = useCallback((name: string, value: 
string, series?: OtlpMetricSeriesView | null) => {
     const expression = buildMetricAttributeFilterExpression(name, value);
     if (!expression) return;
@@ -2585,6 +2603,28 @@ export default function OtlpMetricsPage() {
                               </HzButton>
                             )
                           },
+                          {
+                            key: 'exclude',
+                            header: 
t('otlp.metrics.attributes.column.exclude'),
+                            render: row => (
+                              <HzButton
+                                type="button"
+                                size="xs"
+                                intent="ghost"
+                                
data-otlp-metrics-attribute-filter-out-action={row.name}
+                                
data-otlp-metrics-attribute-filter-out-action-owner="hertzbeat-ui-button"
+                                
aria-label={t('otlp.metrics.attributes.filter-out-action.aria', { name: 
row.name, value: row.value })}
+                                onClick={() => 
applyMetricAttributeExcludeFilter(row.name, row.value, selectedMetricSeries)}
+                              >
+                                <HzButtonIcon
+                                  icon={X}
+                                  
data-otlp-metrics-attribute-filter-out-action-icon={row.name}
+                                  
data-otlp-metrics-attribute-filter-out-action-icon-owner="hertzbeat-ui-button-icon"
+                                />
+                                
{t('otlp.metrics.attributes.filter-out-action')}
+                              </HzButton>
+                            )
+                          },
                           {
                             key: 'replace',
                             header: 
t('otlp.metrics.attributes.column.replace'),
diff --git a/web-next/app/ingestion/otlp/metrics/page.test.tsx 
b/web-next/app/ingestion/otlp/metrics/page.test.tsx
index 894799b755..6e88486f00 100644
--- a/web-next/app/ingestion/otlp/metrics/page.test.tsx
+++ b/web-next/app/ingestion/otlp/metrics/page.test.tsx
@@ -1035,6 +1035,11 @@ describe('otlp metrics page', () => {
     
expect(source).toContain('data-otlp-metrics-attribute-table-owner="hertzbeat-ui-data-table"');
     expect(source).toContain("header: 
t('otlp.metrics.attributes.column.name')");
     expect(source).toContain("header: 
t('otlp.metrics.attributes.column.value')");
+    expect(source).toContain("header: 
t('otlp.metrics.attributes.column.exclude')");
+    expect(source).toContain('buildMetricAttributeExcludeFilterExpression');
+    expect(source).toContain('applyMetricAttributeExcludeFilter');
+    
expect(source).toContain('data-otlp-metrics-attribute-filter-out-action={row.name}');
+    
expect(source).toContain('data-otlp-metrics-attribute-filter-out-action-owner="hertzbeat-ui-button"');
     expect(source).toContain('boundary="top"');
     expect(source).not.toContain('className="mt-3 border-t border-[#252b35] 
pt-3"');
     
expect(source).toContain('data-otlp-metrics-linked-record-summary="log-trace-alert-links"');
@@ -3014,6 +3019,25 @@ describe('otlp metrics page', () => {
     expect(filterParams.get('traceId')).toBe('trace-checkout');
     expect(filterParams.get('spanId')).toBe('span-checkout');
 
+    const serviceExcludeAction = 
interactionContainer.querySelector('[data-otlp-metrics-attribute-filter-out-action="service.name"]')
 as HTMLButtonElement | null;
+    
expect(serviceExcludeAction?.getAttribute('data-otlp-metrics-attribute-filter-out-action-owner')).toBe('hertzbeat-ui-button');
+    
expect(serviceExcludeAction?.getAttribute('aria-label')).toContain('service.name');
+
+    await act(async () => {
+      serviceExcludeAction?.dispatchEvent(new MouseEvent('click', { bubbles: 
true }));
+      await Promise.resolve();
+    });
+
+    const excludeHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
+    const excludeParams = new URL(excludeHref, 
'http://localhost').searchParams;
+    expect(excludeParams.get('filter')).toContain('service.name!="checkout"');
+    expect(excludeParams.get('series')).toBe('checkout_latency-0');
+    expect(excludeParams.get('entityId')).toBe('7');
+    expect(excludeParams.get('serviceName')).toBe('checkout');
+    expect(excludeParams.get('environment')).toBe('prod');
+    expect(excludeParams.get('traceId')).toBe('trace-checkout');
+    expect(excludeParams.get('spanId')).toBe('span-checkout');
+
     const routeGroupAction = 
interactionContainer.querySelector('[data-otlp-metrics-attribute-group-action="route"]')
 as HTMLButtonElement | null;
     
expect(routeGroupAction?.getAttribute('data-otlp-metrics-attribute-group-action-owner')).toBe('hertzbeat-ui-button');
     expect(routeGroupAction?.getAttribute('aria-label')).toContain('route');
diff --git a/web-next/lib/i18n-runtime-messages.ts 
b/web-next/lib/i18n-runtime-messages.ts
index 80cc7506a9..e41942bc1d 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -3202,10 +3202,13 @@ export const SUPPLEMENTAL_MESSAGES: 
Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.attributes.column.name': 'Attribute',
     'otlp.metrics.attributes.column.value': 'Value',
     'otlp.metrics.attributes.column.filter': 'Filter',
+    'otlp.metrics.attributes.column.exclude': 'Exclude',
     'otlp.metrics.attributes.column.replace': 'Replace',
     'otlp.metrics.attributes.column.group': 'Group',
     'otlp.metrics.attributes.filter-action': 'Filter',
     'otlp.metrics.attributes.filter-action.aria': 'Filter {{name}} equals 
{{value}}',
+    'otlp.metrics.attributes.filter-out-action': 'Exclude',
+    'otlp.metrics.attributes.filter-out-action.aria': 'Exclude metrics where 
{{name}} equals {{value}}',
     'otlp.metrics.attributes.replace-action': 'Replace',
     'otlp.metrics.attributes.replace-action.aria': 'Replace filters with 
{{name}} equals {{value}}',
     'otlp.metrics.attributes.group-action': 'Group',
@@ -7662,10 +7665,13 @@ export const SUPPLEMENTAL_MESSAGES: 
Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.attributes.column.name': '属性',
     'otlp.metrics.attributes.column.value': '值',
     'otlp.metrics.attributes.column.filter': '过滤',
+    'otlp.metrics.attributes.column.exclude': '排除',
     'otlp.metrics.attributes.column.replace': '替换',
     'otlp.metrics.attributes.column.group': '分组',
     'otlp.metrics.attributes.filter-action': '过滤',
     'otlp.metrics.attributes.filter-action.aria': '过滤 {{name}} 等于 {{value}}',
+    'otlp.metrics.attributes.filter-out-action': '排除',
+    'otlp.metrics.attributes.filter-out-action.aria': '排除 {{name}} 等于 
{{value}} 的指标',
     'otlp.metrics.attributes.replace-action': '替换',
     'otlp.metrics.attributes.replace-action.aria': '替换为 {{name}} 等于 {{value}} 
的过滤条件',
     'otlp.metrics.attributes.group-action': '分组',


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to