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]