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 e29a823a9c Add signal not-exists filter actions
e29a823a9c is described below
commit e29a823a9c12055feeef9f46733bacf8300a15b1
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 22:56:30 2026 +0800
Add signal not-exists filter actions
---
.../ingestion/otlp/metrics/otlp-metrics-page.tsx | 41 ++++++++++++-
web-next/app/ingestion/otlp/metrics/page.test.tsx | 24 ++++++++
web-next/app/log/manage/log-manage-page.tsx | 47 ++++++++++++++-
web-next/app/log/manage/page.test.tsx | 31 ++++++++++
web-next/app/trace/manage/page.test.tsx | 9 +++
web-next/app/trace/manage/trace-manage-page.tsx | 70 +++++++++++++++++++++-
web-next/lib/i18n-runtime-messages.ts | 14 +++++
7 files changed, 230 insertions(+), 6 deletions(-)
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 b00c850f16..e3f13ecff5 100644
--- a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
+++ b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
@@ -2,7 +2,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
-import { BarChart3, Check, Copy, Download, Filter, Pencil, Play, Replace,
RotateCcw, Save, Search, Table2, Trash2, Workflow, X } from 'lucide-react';
+import { Ban, BarChart3, Check, Copy, Download, Filter, Pencil, Play, Replace,
RotateCcw, Save, Search, Table2, Trash2, Workflow, X } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { HzActionGroup, HzAssistiveMarker, HzAttributeDiagnostics, HzButton,
HzButtonIcon, HzButtonLink, HzChipGroup, HzCollapsibleSection,
HzContextHandoff, HzControlStack, HzDataCellStack, HzDataCellText,
HzDataMetaText, HzDataTable, HzDetailRows, HzDisabledActionShell, HzEmptyState,
HzInput, HzPaginationBar, HzPanelHeader, HzPanelSection, HzPanelSurface,
HzPanelTitleLabel, HzQueryActionGroup, HzSearchFieldFrame, HzSearchFieldIcon,
HzSelect, HzSignalSummaryStrip, HzSignalWorkbenchShell [...]
import { EChartsPanel, type EChartsDataZoomRange } from
'@/components/observability/echarts-panel';
@@ -130,6 +130,12 @@ function buildMetricAttributeExistsFilterExpression(name:
string) {
return `${trimmedName} EXISTS`;
}
+function buildMetricAttributeNotExistsFilterExpression(name: string) {
+ const trimmedName = name.trim();
+ if (!trimmedName) return null;
+ return `${trimmedName} NOT EXISTS`;
+}
+
function mergeMetricFilterExpression(currentFilter: string, expression:
string) {
const trimmedFilter = currentFilter.trim();
if (!trimmedFilter) return expression;
@@ -834,6 +840,17 @@ export default function OtlpMetricsPage() {
replaceMetricsRoute(nextDraft, undefined, series?.key || query.series,
series ? buildMetricSeriesRouteContext(series) : {});
}, [draft, query.series, replaceMetricsRoute]);
+ const applyMetricAttributeNotExistsFilter = useCallback((name: string,
series?: OtlpMetricSeriesView | null) => {
+ const expression = buildMetricAttributeNotExistsFilterExpression(name);
+ 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;
@@ -2664,6 +2681,28 @@ export default function OtlpMetricsPage() {
</HzButton>
)
},
+ {
+ key: 'not-exists',
+ header:
t('otlp.metrics.attributes.column.not-exists'),
+ render: row => (
+ <HzButton
+ type="button"
+ size="xs"
+ intent="ghost"
+
data-otlp-metrics-attribute-not-exists-action={row.name}
+
data-otlp-metrics-attribute-not-exists-action-owner="hertzbeat-ui-button"
+
aria-label={t('otlp.metrics.attributes.not-exists-action.aria', { name:
row.name })}
+ onClick={() =>
applyMetricAttributeNotExistsFilter(row.name, selectedMetricSeries)}
+ >
+ <HzButtonIcon
+ icon={Ban}
+
data-otlp-metrics-attribute-not-exists-action-icon={row.name}
+
data-otlp-metrics-attribute-not-exists-action-icon-owner="hertzbeat-ui-button-icon"
+ />
+
{t('otlp.metrics.attributes.not-exists-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 48b93ff998..2cf1ec73a6 100644
--- a/web-next/app/ingestion/otlp/metrics/page.test.tsx
+++ b/web-next/app/ingestion/otlp/metrics/page.test.tsx
@@ -1039,14 +1039,19 @@ describe('otlp metrics page', () => {
expect(source).toContain("header:
t('otlp.metrics.attributes.column.value')");
expect(source).toContain("header:
t('otlp.metrics.attributes.column.exclude')");
expect(source).toContain("header:
t('otlp.metrics.attributes.column.exists')");
+ expect(source).toContain("header:
t('otlp.metrics.attributes.column.not-exists')");
expect(source).toContain('buildMetricAttributeExcludeFilterExpression');
expect(source).toContain('buildMetricAttributeExistsFilterExpression');
+ expect(source).toContain('buildMetricAttributeNotExistsFilterExpression');
expect(source).toContain('applyMetricAttributeExcludeFilter');
expect(source).toContain('applyMetricAttributeExistsFilter');
+ expect(source).toContain('applyMetricAttributeNotExistsFilter');
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('data-otlp-metrics-attribute-exists-action={row.name}');
expect(source).toContain('data-otlp-metrics-attribute-exists-action-owner="hertzbeat-ui-button"');
+
expect(source).toContain('data-otlp-metrics-attribute-not-exists-action={row.name}');
+
expect(source).toContain('data-otlp-metrics-attribute-not-exists-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"');
@@ -3064,6 +3069,25 @@ describe('otlp metrics page', () => {
expect(existsParams.get('traceId')).toBe('trace-checkout');
expect(existsParams.get('spanId')).toBe('span-checkout');
+ const serviceNotExistsAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-not-exists-action="service.name"]')
as HTMLButtonElement | null;
+
expect(serviceNotExistsAction?.getAttribute('data-otlp-metrics-attribute-not-exists-action-owner')).toBe('hertzbeat-ui-button');
+
expect(serviceNotExistsAction?.getAttribute('aria-label')).toContain('service.name');
+
+ await act(async () => {
+ serviceNotExistsAction?.dispatchEvent(new MouseEvent('click', { bubbles:
true }));
+ await Promise.resolve();
+ });
+
+ const notExistsHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
+ const notExistsParams = new URL(notExistsHref,
'http://localhost').searchParams;
+ expect(notExistsParams.get('filter')).toContain('service.name NOT EXISTS');
+ expect(notExistsParams.get('series')).toBe('checkout_latency-0');
+ expect(notExistsParams.get('entityId')).toBe('7');
+ expect(notExistsParams.get('serviceName')).toBe('checkout');
+ expect(notExistsParams.get('environment')).toBe('prod');
+ expect(notExistsParams.get('traceId')).toBe('trace-checkout');
+ expect(notExistsParams.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/app/log/manage/log-manage-page.tsx
b/web-next/app/log/manage/log-manage-page.tsx
index 3f35418a19..1355570ccd 100644
--- a/web-next/app/log/manage/log-manage-page.tsx
+++ b/web-next/app/log/manage/log-manage-page.tsx
@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useMemo, useRef,
useState } from 'react'
import Link from 'next/link';
import {
BarChart3,
+ Ban,
BellPlus,
BellRing,
Check,
@@ -289,6 +290,15 @@ function buildLogAttributeExistsExpression(row:
LogAttributeRow) {
return { kind, expression: `${key} EXISTS` };
}
+function buildLogAttributeNotExistsExpression(row: LogAttributeRow) {
+ const kind = resolveLogAttributeFilterKind(row);
+ const key = row.name.trim();
+ if (!kind || !isSafeLogAttributeFilterKey(key)) {
+ return null;
+ }
+ return { kind, expression: `${key} NOT EXISTS` };
+}
+
function buildLogAttributeGroupBy(row: LogAttributeRow) {
const kind = resolveLogAttributeFilterKind(row);
const key = row.name.trim();
@@ -2244,6 +2254,19 @@ function LogManageExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft]);
+ const applyLogAttributeNotExistsFilter = useCallback((row: LogAttributeRow)
=> {
+ const filter = buildLogAttributeNotExistsExpression(row);
+ if (!filter) return;
+ const nextQuery: LogQueryState = {
+ ...draft,
+ ...(filter.kind === 'resource'
+ ? { resourceFilter:
mergeLogAttributeFilterExpression(draft.resourceFilter, filter.expression) }
+ : { attributeFilter:
mergeLogAttributeFilterExpression(draft.attributeFilter, filter.expression) })
+ };
+ setDraft(nextQuery);
+ applyQuery(nextQuery);
+ }, [applyQuery, draft, setDraft]);
+
const replaceLogAttributeFilter = useCallback((row: LogAttributeRow) => {
const filter = buildLogAttributeFilterExpression(row,
t('log.manage.attributes.value.object'));
if (!filter) return;
@@ -2334,10 +2357,11 @@ function LogManageExplorer({
const filter = buildLogAttributeFilterExpression(row,
t('log.manage.attributes.value.object'));
const excludeFilter = buildLogAttributeExcludeExpression(row,
t('log.manage.attributes.value.object'));
const existsFilter = buildLogAttributeExistsExpression(row);
+ const notExistsFilter = buildLogAttributeNotExistsExpression(row);
const group = buildLogAttributeGroupBy(row);
const fieldColumn = buildLogAttributeFieldColumn(row);
const fieldColumnVisible = Boolean(fieldColumn &&
visibleLogFieldColumns.includes(fieldColumn));
- if (!filter && !excludeFilter && !existsFilter && !group && !fieldColumn)
return null;
+ if (!filter && !excludeFilter && !existsFilter && !notExistsFilter &&
!group && !fieldColumn) return null;
return (
<span className="inline-flex flex-wrap gap-1">
{fieldColumn ? (
@@ -2455,6 +2479,25 @@ function LogManageExplorer({
{t('log.manage.attributes.exists-action')}
</HzButton>
) : null}
+ {notExistsFilter ? (
+ <HzButton
+ data-log-manage-attribute-not-exists-action={notExistsFilter.kind}
+ data-log-manage-attribute-not-exists-owner="hertzbeat-ui-button"
+ data-log-manage-attribute-filter-name={row.name}
+ data-log-manage-attribute-filter-value={row.value}
+ size="sm"
+ intent="secondary"
+ onClick={() => applyLogAttributeNotExistsFilter(row)}
+ aria-label={t('log.manage.attributes.not-exists-action.aria', {
name: row.name })}
+ >
+ <HzButtonIcon
+ icon={Ban}
+ data-log-manage-attribute-not-exists-icon="not-exists"
+
data-log-manage-attribute-not-exists-icon-owner="hertzbeat-ui-button-icon"
+ />
+ {t('log.manage.attributes.not-exists-action')}
+ </HzButton>
+ ) : null}
{group ? (
<HzButton
data-log-manage-attribute-group-action={group.kind}
@@ -2476,7 +2519,7 @@ function LogManageExplorer({
) : null}
</span>
);
- }, [applyLogAttributeExistsFilter, applyLogAttributeFieldColumn,
applyLogAttributeFilter, applyLogContextAttributeFilter,
excludeLogAttributeFilter, groupLogAttribute, replaceLogAttributeFilter, t,
visibleLogFieldColumns]);
+ }, [applyLogAttributeExistsFilter, applyLogAttributeFieldColumn,
applyLogAttributeFilter, applyLogAttributeNotExistsFilter,
applyLogContextAttributeFilter, excludeLogAttributeFilter, groupLogAttribute,
replaceLogAttributeFilter, t, visibleLogFieldColumns]);
const openLogDetails = (entry: LogEntry | null, source: 'history' |
'stream', selectionState: 'attached' | 'detached' = 'attached') => {
if (!entry) return;
diff --git a/web-next/app/log/manage/page.test.tsx
b/web-next/app/log/manage/page.test.tsx
index e188b7ace4..81518bd7b6 100644
--- a/web-next/app/log/manage/page.test.tsx
+++ b/web-next/app/log/manage/page.test.tsx
@@ -654,6 +654,8 @@ describe('log manage page', () => {
expect(source).toContain('buildLogAttributeExistsExpression');
expect(source).toContain('applyLogAttributeExistsFilter');
expect(source).toContain('data-log-manage-attribute-exists-action={existsFilter.kind}');
+
expect(source).toContain('data-log-manage-attribute-not-exists-action={notExistsFilter.kind}');
+ expect(source).toContain('buildLogAttributeNotExistsExpression');
expect(source).toContain('data-log-manage-attribute-exists-owner="hertzbeat-ui-button"');
expect(source).toContain('data-log-manage-stream-selected-detail-owner="hertzbeat-ui-detail-rows"');
expect(source).toContain('data-log-manage-stream-detail-action-stack="shared-control-stack"');
@@ -3479,6 +3481,35 @@ describe('log manage page', () => {
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('attributeFilter')).toContain('http.route
EXISTS');
+
+ const notExistsResourceAction = interactionContainer.querySelector(
+
'[data-log-manage-attribute-not-exists-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
+ ) as HTMLButtonElement | null;
+ const notExistsAttributeAction = interactionContainer.querySelector(
+
'[data-log-manage-attribute-not-exists-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
+ ) as HTMLButtonElement | null;
+ expect(notExistsResourceAction).toBeTruthy();
+ expect(notExistsAttributeAction).toBeTruthy();
+
expect(notExistsResourceAction?.getAttribute('data-log-manage-attribute-not-exists-owner')).toBe('hertzbeat-ui-button');
+
expect(notExistsAttributeAction?.getAttribute('data-log-manage-attribute-not-exists-owner')).toBe('hertzbeat-ui-button');
+
+ mockState.replace.mockClear();
+ await act(async () => {
+ notExistsResourceAction?.click();
+ await Promise.resolve();
+ });
+
+ expect(mockState.replace).toHaveBeenCalledTimes(1);
+ expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('resourceFilter')).toContain('service.version
NOT EXISTS');
+
+ mockState.replace.mockClear();
+ await act(async () => {
+ notExistsAttributeAction?.click();
+ await Promise.resolve();
+ });
+
+ expect(mockState.replace).toHaveBeenCalledTimes(1);
+ expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('attributeFilter')).toContain('http.route
NOT EXISTS');
} finally {
mockState.renderData.list.content = originalContent;
}
diff --git a/web-next/app/trace/manage/page.test.tsx
b/web-next/app/trace/manage/page.test.tsx
index fb119db068..448cefdc93 100644
--- a/web-next/app/trace/manage/page.test.tsx
+++ b/web-next/app/trace/manage/page.test.tsx
@@ -1801,18 +1801,22 @@ describe('trace manage page', () => {
expect(source).toContain('buildTraceResourceFilterExpression');
expect(source).toContain('buildTraceResourceExcludeFilterExpression');
expect(source).toContain('buildTraceResourceExistsFilterExpression');
+ expect(source).toContain('buildTraceResourceNotExistsFilterExpression');
expect(source).toContain('buildTraceSpanAttributeFilterExpression');
expect(source).toContain('buildTraceSpanAttributeExcludeFilterExpression');
expect(source).toContain('buildTraceSpanAttributeExistsFilterExpression');
+
expect(source).toContain('buildTraceSpanAttributeNotExistsFilterExpression');
expect(source).toContain('buildTraceSpanAttributeGroupBy');
expect(source).toContain('mergeTraceResourceFilterExpression');
expect(source).toContain('applyTraceResourceFilter');
expect(source).toContain('excludeTraceResourceFilter');
expect(source).toContain('applyTraceResourceExistsFilter');
+ expect(source).toContain('applyTraceResourceNotExistsFilter');
expect(source).toContain('replaceTraceResourceFilter');
expect(source).toContain('applyTraceSpanAttributeFilter');
expect(source).toContain('excludeTraceSpanAttributeFilter');
expect(source).toContain('applyTraceSpanAttributeExistsFilter');
+ expect(source).toContain('applyTraceSpanAttributeNotExistsFilter');
expect(source).toContain('replaceTraceSpanAttributeFilter');
expect(source).toContain('applyTraceSpanAttributeGroupBy');
expect(source).toContain('buildTraceResourceGroupBy');
@@ -1825,6 +1829,8 @@ describe('trace manage page', () => {
expect(source).toContain('data-trace-manage-drawer-span-attribute-filter-out-action-owner="hertzbeat-ui-button"');
expect(source).toContain('data-trace-manage-drawer-span-attribute-exists-action="true"');
expect(source).toContain('data-trace-manage-drawer-span-attribute-exists-action-owner="hertzbeat-ui-button"');
+
expect(source).toContain('data-trace-manage-drawer-span-attribute-not-exists-action="true"');
+
expect(source).toContain('data-trace-manage-drawer-span-attribute-not-exists-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"');
@@ -1843,7 +1849,10 @@ describe('trace manage page', () => {
expect(source).toContain("t('trace.manage.drawer.attributes.filter-out-action')");
expect(source).toContain('data-trace-manage-drawer-resource-exists-action="true"');
expect(source).toContain('data-trace-manage-drawer-resource-exists-action-owner="hertzbeat-ui-button"');
+
expect(source).toContain('data-trace-manage-drawer-resource-not-exists-action="true"');
+
expect(source).toContain('data-trace-manage-drawer-resource-not-exists-action-owner="hertzbeat-ui-button"');
expect(source).toContain("t('trace.manage.drawer.attributes.exists-action')");
+
expect(source).toContain("t('trace.manage.drawer.attributes.not-exists-action')");
expect(source).toContain('data-trace-manage-drawer-resource-replace-action="true"');
expect(source).toContain('data-trace-manage-drawer-resource-replace-action-owner="hertzbeat-ui-button"');
expect(source).toContain("t('trace.manage.drawer.attributes.replace-action')");
diff --git a/web-next/app/trace/manage/trace-manage-page.tsx
b/web-next/app/trace/manage/trace-manage-page.tsx
index 26dbb2e95e..f9b934b718 100644
--- a/web-next/app/trace/manage/trace-manage-page.tsx
+++ b/web-next/app/trace/manage/trace-manage-page.tsx
@@ -2,7 +2,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from
'react';
import Link from 'next/link';
-import { BarChart3, BellPlus, BellRing, Check, Copy, Download, Filter,
ListChecks, Pencil, Play, Replace, RotateCcw, Save, Search, ScrollText, Server,
Timer, Trash2, Workflow, X } from 'lucide-react';
+import { BarChart3, Ban, BellPlus, BellRing, Check, Copy, Download, Filter,
ListChecks, Pencil, Play, Replace, RotateCcw, Save, Search, ScrollText, Server,
Timer, Trash2, Workflow, X } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { HzActionGroup, HzAttributeDiagnostics, HzButton, HzButtonIcon,
HzButtonLink, HzCheckbox, HzChipGroup, HzControlStack, HzDataCellText,
HzDataTable, HzDetailRows, HzDialogBodyLayout, HzDialogEventNotice,
HzDialogEventText, HzDialogMetaItem, HzDisabledActionShell, HzEmptyState,
HzInput, HzPanelHeader, HzPanelSurface, HzQueryActionGroup,
HzQueryStatusSelect, HzQueryTokenField, HzSearchFieldFrame, HzSearchFieldIcon,
HzSelect, HzSignalSummaryStrip, HzSignalTrendBars, HzSignalWorkbench [...]
import { buildTimeRangeControlLabels, TimeRangeControl } from
'@/components/observability/time-range-control';
@@ -523,6 +523,14 @@ function buildTraceResourceExistsFilterExpression(name:
React.ReactNode) {
return `${key} EXISTS`;
}
+function buildTraceResourceNotExistsFilterExpression(name: React.ReactNode) {
+ const key = String(name ?? '').trim();
+ if (!isSafeTraceResourceFilterKey(key)) {
+ return null;
+ }
+ return `${key} NOT EXISTS`;
+}
+
function buildTraceSpanAttributeFilterExpression(name: React.ReactNode, value:
React.ReactNode) {
return buildTraceResourceFilterExpression(name, value);
}
@@ -535,6 +543,10 @@ function
buildTraceSpanAttributeExistsFilterExpression(name: React.ReactNode) {
return buildTraceResourceExistsFilterExpression(name);
}
+function buildTraceSpanAttributeNotExistsFilterExpression(name:
React.ReactNode) {
+ return buildTraceResourceNotExistsFilterExpression(name);
+}
+
function buildTraceSpanAttributeGroupBy(name: React.ReactNode) {
const key = String(name ?? '').trim();
if (!isSafeTraceResourceFilterKey(key)) {
@@ -790,11 +802,13 @@ function TraceWaterfallDrawer({
onApplySpanAttributeFilter,
onExcludeSpanAttributeFilter,
onApplySpanAttributeExistsFilter,
+ onApplySpanAttributeNotExistsFilter,
onReplaceSpanAttributeFilter,
onApplySpanAttributeGroupBy,
onApplyResourceFilter,
onExcludeResourceFilter,
onApplyResourceExistsFilter,
+ onApplyResourceNotExistsFilter,
onReplaceResourceFilter,
onApplyResourceGroupBy
}: {
@@ -810,11 +824,13 @@ function TraceWaterfallDrawer({
onApplySpanAttributeFilter: (name: string, value: string) => void;
onExcludeSpanAttributeFilter: (name: string, value: string) => void;
onApplySpanAttributeExistsFilter: (name: string) => void;
+ onApplySpanAttributeNotExistsFilter: (name: string) => void;
onReplaceSpanAttributeFilter: (name: string, value: string) => void;
onApplySpanAttributeGroupBy: (name: string) => void;
onApplyResourceFilter: (name: string, value: string) => void;
onExcludeResourceFilter: (name: string, value: string) => void;
onApplyResourceExistsFilter: (name: string) => void;
+ onApplyResourceNotExistsFilter: (name: string) => void;
onReplaceResourceFilter: (name: string, value: string) => void;
onApplyResourceGroupBy: (name: string) => void;
}) {
@@ -1250,7 +1266,7 @@ function TraceWaterfallDrawer({
title: row.title,
copy: row.copy,
meta: row.meta,
- action: buildTraceSpanAttributeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeExcludeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeExistsFilterExpression(row.title) ||
buildTraceSpanAttributeGroupBy(row.title) ? (
+ action: buildTraceSpanAttributeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeExcludeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeExistsFilterExpression(row.title) ||
buildTraceSpanAttributeNotExistsFilterExpression(row.title) ||
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"
@@ -1296,6 +1312,18 @@ function TraceWaterfallDrawer({
<HzButtonIcon icon={Check}
data-trace-manage-drawer-span-attribute-exists-action-icon="exists"
data-trace-manage-drawer-span-attribute-exists-action-icon-owner="hertzbeat-ui-button-icon"
/>
{t('trace.manage.drawer.attributes.exists-action')}
</HzButton>
+ <HzButton
+
data-trace-manage-drawer-span-attribute-not-exists-action="true"
+
data-trace-manage-drawer-span-attribute-not-exists-action-owner="hertzbeat-ui-button"
+
data-trace-manage-drawer-span-attribute-filter-name={row.title}
+ size="sm"
+ intent="secondary"
+ onClick={() =>
onApplySpanAttributeNotExistsFilter(row.title)}
+
aria-label={t('trace.manage.drawer.attributes.not-exists-action.aria', { name:
row.title })}
+ >
+ <HzButtonIcon icon={Ban}
data-trace-manage-drawer-span-attribute-not-exists-action-icon="not-exists"
data-trace-manage-drawer-span-attribute-not-exists-action-icon-owner="hertzbeat-ui-button-icon"
/>
+
{t('trace.manage.drawer.attributes.not-exists-action')}
+ </HzButton>
<HzButton
data-trace-manage-drawer-span-attribute-replace-action="true"
data-trace-manage-drawer-span-attribute-replace-action-owner="hertzbeat-ui-button"
@@ -1339,7 +1367,7 @@ function TraceWaterfallDrawer({
title: row.title,
copy: row.copy,
meta: row.meta,
- action: buildTraceResourceFilterExpression(row.title,
row.copy) || buildTraceResourceExcludeFilterExpression(row.title, row.copy) ||
buildTraceResourceExistsFilterExpression(row.title) ||
buildTraceResourceGroupBy(row.title) ? (
+ action: buildTraceResourceFilterExpression(row.title,
row.copy) || buildTraceResourceExcludeFilterExpression(row.title, row.copy) ||
buildTraceResourceExistsFilterExpression(row.title) ||
buildTraceResourceNotExistsFilterExpression(row.title) ||
buildTraceResourceGroupBy(row.title) ? (
<HzActionGroup
data-trace-manage-drawer-resource-action-group="filter-group"
data-trace-manage-drawer-resource-action-group-owner="hertzbeat-ui-action-group"
@@ -1385,6 +1413,18 @@ function TraceWaterfallDrawer({
<HzButtonIcon icon={Check}
data-trace-manage-drawer-resource-exists-action-icon="exists"
data-trace-manage-drawer-resource-exists-action-icon-owner="hertzbeat-ui-button-icon"
/>
{t('trace.manage.drawer.attributes.exists-action')}
</HzButton>
+ <HzButton
+
data-trace-manage-drawer-resource-not-exists-action="true"
+
data-trace-manage-drawer-resource-not-exists-action-owner="hertzbeat-ui-button"
+
data-trace-manage-drawer-resource-filter-name={row.title}
+ size="sm"
+ intent="secondary"
+ onClick={() =>
onApplyResourceNotExistsFilter(row.title)}
+
aria-label={t('trace.manage.drawer.attributes.not-exists-action.aria', { name:
row.title })}
+ >
+ <HzButtonIcon icon={Ban}
data-trace-manage-drawer-resource-not-exists-action-icon="not-exists"
data-trace-manage-drawer-resource-not-exists-action-icon-owner="hertzbeat-ui-button-icon"
/>
+
{t('trace.manage.drawer.attributes.not-exists-action')}
+ </HzButton>
<HzButton
data-trace-manage-drawer-resource-replace-action="true"
data-trace-manage-drawer-resource-replace-action-owner="hertzbeat-ui-button"
@@ -1926,6 +1966,17 @@ function TraceExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft]);
+ const applyTraceResourceNotExistsFilter = useCallback((name: string) => {
+ const expression = buildTraceResourceNotExistsFilterExpression(name);
+ if (!expression) return;
+ const nextQuery: TraceQueryState = {
+ ...draft,
+ resourceFilter: mergeTraceResourceFilterExpression(draft.resourceFilter,
expression)
+ };
+ setDraft(nextQuery);
+ applyQuery(nextQuery);
+ }, [applyQuery, draft, setDraft]);
+
const replaceTraceResourceFilter = useCallback((name: string, value: string)
=> {
const expression = buildTraceResourceFilterExpression(name, value);
if (!expression) return;
@@ -1970,6 +2021,17 @@ function TraceExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft]);
+ const applyTraceSpanAttributeNotExistsFilter = useCallback((name: string) =>
{
+ const expression = buildTraceSpanAttributeNotExistsFilterExpression(name);
+ if (!expression) return;
+ const nextQuery: TraceQueryState = {
+ ...draft,
+ attributeFilter:
mergeTraceResourceFilterExpression(draft.attributeFilter, expression)
+ };
+ setDraft(nextQuery);
+ applyQuery(nextQuery);
+ }, [applyQuery, draft, setDraft]);
+
const replaceTraceSpanAttributeFilter = useCallback((name: string, value:
string) => {
const expression = buildTraceSpanAttributeFilterExpression(name, value);
if (!expression) return;
@@ -3529,11 +3591,13 @@ function TraceExplorer({
onApplySpanAttributeFilter={applyTraceSpanAttributeFilter}
onExcludeSpanAttributeFilter={excludeTraceSpanAttributeFilter}
onApplySpanAttributeExistsFilter={applyTraceSpanAttributeExistsFilter}
+
onApplySpanAttributeNotExistsFilter={applyTraceSpanAttributeNotExistsFilter}
onReplaceSpanAttributeFilter={replaceTraceSpanAttributeFilter}
onApplySpanAttributeGroupBy={applyTraceSpanAttributeGroupBy}
onApplyResourceFilter={applyTraceResourceFilter}
onExcludeResourceFilter={excludeTraceResourceFilter}
onApplyResourceExistsFilter={applyTraceResourceExistsFilter}
+ onApplyResourceNotExistsFilter={applyTraceResourceNotExistsFilter}
onReplaceResourceFilter={replaceTraceResourceFilter}
onApplyResourceGroupBy={applyTraceResourceGroupBy}
/>
diff --git a/web-next/lib/i18n-runtime-messages.ts
b/web-next/lib/i18n-runtime-messages.ts
index 91bdac484b..07f90978ce 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -2743,6 +2743,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'trace.manage.drawer.attributes.filter-out-action.aria': 'Exclude traces
where {{name}} equals {{value}}',
'trace.manage.drawer.attributes.exists-action': 'Exists',
'trace.manage.drawer.attributes.exists-action.aria': 'Filter traces where
{{name}} exists',
+ 'trace.manage.drawer.attributes.not-exists-action': 'Not exists',
+ 'trace.manage.drawer.attributes.not-exists-action.aria': 'Filter traces
where {{name}} does not exist',
'trace.manage.drawer.attributes.replace-action': 'Replace',
'trace.manage.drawer.attributes.replace-action.aria': 'Replace filters
with {{name}} equals {{value}}',
'trace.manage.drawer.attributes.empty.title': 'No attributes',
@@ -3208,6 +3210,7 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.column.filter': 'Filter',
'otlp.metrics.attributes.column.exclude': 'Exclude',
'otlp.metrics.attributes.column.exists': 'Exists',
+ 'otlp.metrics.attributes.column.not-exists': 'Not exists',
'otlp.metrics.attributes.column.replace': 'Replace',
'otlp.metrics.attributes.column.group': 'Group',
'otlp.metrics.attributes.filter-action': 'Filter',
@@ -3216,6 +3219,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.filter-out-action.aria': 'Exclude metrics where
{{name}} equals {{value}}',
'otlp.metrics.attributes.exists-action': 'Exists',
'otlp.metrics.attributes.exists-action.aria': 'Filter metrics where
{{name}} exists',
+ 'otlp.metrics.attributes.not-exists-action': 'Not exists',
+ 'otlp.metrics.attributes.not-exists-action.aria': 'Filter metrics where
{{name}} does not exist',
'otlp.metrics.attributes.replace-action': 'Replace',
'otlp.metrics.attributes.replace-action.aria': 'Replace filters with
{{name}} equals {{value}}',
'otlp.metrics.attributes.group-action': 'Group',
@@ -3997,6 +4002,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'log.manage.attributes.filter-out-action.aria': 'Exclude logs where
{{name}} equals {{value}}',
'log.manage.attributes.exists-action': 'Exists',
'log.manage.attributes.exists-action.aria': 'Filter logs where {{name}}
exists',
+ 'log.manage.attributes.not-exists-action': 'Not exists',
+ 'log.manage.attributes.not-exists-action.aria': 'Filter logs where
{{name}} does not exist',
'log.manage.attributes.replace-action': 'Replace',
'log.manage.attributes.replace-action.aria': 'Replace filters with
{{name}} equals {{value}}',
'log.manage.attributes.group-action': 'Group',
@@ -7219,6 +7226,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'trace.manage.drawer.attributes.filter-out-action.aria': '排除 {{name}} 等于
{{value}} 的链路',
'trace.manage.drawer.attributes.exists-action': '存在',
'trace.manage.drawer.attributes.exists-action.aria': '过滤存在 {{name}} 的链路',
+ 'trace.manage.drawer.attributes.not-exists-action': '不存在',
+ 'trace.manage.drawer.attributes.not-exists-action.aria': '过滤不存在 {{name}}
的链路',
'trace.manage.drawer.attributes.replace-action': '替换',
'trace.manage.drawer.attributes.replace-action.aria': '替换为 {{name}} 等于
{{value}} 的过滤条件',
'trace.manage.drawer.attributes.empty.title': '无属性',
@@ -7684,6 +7693,7 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.column.filter': '过滤',
'otlp.metrics.attributes.column.exclude': '排除',
'otlp.metrics.attributes.column.exists': '存在',
+ 'otlp.metrics.attributes.column.not-exists': '不存在',
'otlp.metrics.attributes.column.replace': '替换',
'otlp.metrics.attributes.column.group': '分组',
'otlp.metrics.attributes.filter-action': '过滤',
@@ -7692,6 +7702,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.filter-out-action.aria': '排除 {{name}} 等于
{{value}} 的指标',
'otlp.metrics.attributes.exists-action': '存在',
'otlp.metrics.attributes.exists-action.aria': '过滤存在 {{name}} 的指标',
+ 'otlp.metrics.attributes.not-exists-action': '不存在',
+ 'otlp.metrics.attributes.not-exists-action.aria': '过滤不存在 {{name}} 的指标',
'otlp.metrics.attributes.replace-action': '替换',
'otlp.metrics.attributes.replace-action.aria': '替换为 {{name}} 等于 {{value}}
的过滤条件',
'otlp.metrics.attributes.group-action': '分组',
@@ -8473,6 +8485,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'log.manage.attributes.filter-out-action.aria': '排除 {{name}} 等于 {{value}}
的日志',
'log.manage.attributes.exists-action': '存在',
'log.manage.attributes.exists-action.aria': '过滤存在 {{name}} 的日志',
+ 'log.manage.attributes.not-exists-action': '不存在',
+ 'log.manage.attributes.not-exists-action.aria': '过滤不存在 {{name}} 的日志',
'log.manage.attributes.replace-action': '替换',
'log.manage.attributes.replace-action.aria': '替换为 {{name}} 等于 {{value}}
的过滤条件',
'log.manage.attributes.group-action': '分组',
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]