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 9adc23e78a Add signal list filter actions
9adc23e78a is described below
commit 9adc23e78a3b468a3cc57018bc678d6969a25aec
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 23:25:23 2026 +0800
Add signal list filter actions
---
.../ingestion/otlp/metrics/otlp-metrics-page.tsx | 82 +++++++++++-
web-next/app/ingestion/otlp/metrics/page.test.tsx | 48 +++++++
web-next/app/log/manage/log-manage-page.tsx | 94 +++++++++++++-
web-next/app/log/manage/page.test.tsx | 64 ++++++++++
web-next/app/trace/manage/page.test.tsx | 18 +++
web-next/app/trace/manage/trace-manage-page.tsx | 142 ++++++++++++++++++++-
web-next/lib/i18n-runtime-messages.ts | 28 ++++
7 files changed, 471 insertions(+), 5 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 7e462df32f..6f6f7e8f4f 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 { Ban, 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, ListChecks, 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';
@@ -138,6 +138,20 @@ function
buildMetricAttributeNotContainsFilterExpression(name: string, value: st
return `${trimmedName} NOT CONTAINS
${escapeMetricFilterValue(trimmedValue)}`;
}
+function buildMetricAttributeInFilterExpression(name: string, value: string) {
+ const trimmedName = name.trim();
+ const trimmedValue = value.trim();
+ if (!trimmedName || !trimmedValue) return null;
+ return `${trimmedName} IN ("${escapeMetricFilterValue(trimmedValue)}")`;
+}
+
+function buildMetricAttributeNotInFilterExpression(name: string, value:
string) {
+ const trimmedName = name.trim();
+ const trimmedValue = value.trim();
+ if (!trimmedName || !trimmedValue) return null;
+ return `${trimmedName} NOT IN ("${escapeMetricFilterValue(trimmedValue)}")`;
+}
+
function buildMetricAttributeExistsFilterExpression(name: string) {
const trimmedName = name.trim();
if (!trimmedName) return null;
@@ -865,6 +879,28 @@ export default function OtlpMetricsPage() {
replaceMetricsRoute(nextDraft, undefined, series?.key || query.series,
series ? buildMetricSeriesRouteContext(series) : {});
}, [draft, query.series, replaceMetricsRoute]);
+ const applyMetricAttributeInFilter = useCallback((name: string, value:
string, series?: OtlpMetricSeriesView | null) => {
+ const expression = buildMetricAttributeInFilterExpression(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 applyMetricAttributeNotInFilter = useCallback((name: string, value:
string, series?: OtlpMetricSeriesView | null) => {
+ const expression = buildMetricAttributeNotInFilterExpression(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 applyMetricAttributeExistsFilter = useCallback((name: string, series?:
OtlpMetricSeriesView | null) => {
const expression = buildMetricAttributeExistsFilterExpression(name);
if (!expression) return;
@@ -2717,6 +2753,50 @@ export default function OtlpMetricsPage() {
</HzButton>
)
},
+ {
+ key: 'in',
+ header: t('otlp.metrics.attributes.column.in'),
+ render: row => (
+ <HzButton
+ type="button"
+ size="xs"
+ intent="ghost"
+
data-otlp-metrics-attribute-in-action={row.name}
+
data-otlp-metrics-attribute-in-action-owner="hertzbeat-ui-button"
+
aria-label={t('otlp.metrics.attributes.in-action.aria', { name: row.name,
value: row.value })}
+ onClick={() =>
applyMetricAttributeInFilter(row.name, row.value, selectedMetricSeries)}
+ >
+ <HzButtonIcon
+ icon={ListChecks}
+
data-otlp-metrics-attribute-in-action-icon={row.name}
+
data-otlp-metrics-attribute-in-action-icon-owner="hertzbeat-ui-button-icon"
+ />
+ {t('otlp.metrics.attributes.in-action')}
+ </HzButton>
+ )
+ },
+ {
+ key: 'not-in',
+ header: t('otlp.metrics.attributes.column.not-in'),
+ render: row => (
+ <HzButton
+ type="button"
+ size="xs"
+ intent="ghost"
+
data-otlp-metrics-attribute-not-in-action={row.name}
+
data-otlp-metrics-attribute-not-in-action-owner="hertzbeat-ui-button"
+
aria-label={t('otlp.metrics.attributes.not-in-action.aria', { name: row.name,
value: row.value })}
+ onClick={() =>
applyMetricAttributeNotInFilter(row.name, row.value, selectedMetricSeries)}
+ >
+ <HzButtonIcon
+ icon={Ban}
+
data-otlp-metrics-attribute-not-in-action-icon={row.name}
+
data-otlp-metrics-attribute-not-in-action-icon-owner="hertzbeat-ui-button-icon"
+ />
+ {t('otlp.metrics.attributes.not-in-action')}
+ </HzButton>
+ )
+ },
{
key: 'exclude',
header:
t('otlp.metrics.attributes.column.exclude'),
diff --git a/web-next/app/ingestion/otlp/metrics/page.test.tsx
b/web-next/app/ingestion/otlp/metrics/page.test.tsx
index 12c3bb7ec8..7364946b7e 100644
--- a/web-next/app/ingestion/otlp/metrics/page.test.tsx
+++ b/web-next/app/ingestion/otlp/metrics/page.test.tsx
@@ -1039,17 +1039,23 @@ describe('otlp metrics page', () => {
expect(source).toContain("header:
t('otlp.metrics.attributes.column.value')");
expect(source).toContain("header:
t('otlp.metrics.attributes.column.contains')");
expect(source).toContain("header:
t('otlp.metrics.attributes.column.not-contains')");
+ expect(source).toContain("header: t('otlp.metrics.attributes.column.in')");
+ expect(source).toContain("header:
t('otlp.metrics.attributes.column.not-in')");
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('buildMetricAttributeContainsFilterExpression');
expect(source).toContain('buildMetricAttributeNotContainsFilterExpression');
+ expect(source).toContain('buildMetricAttributeInFilterExpression');
+ expect(source).toContain('buildMetricAttributeNotInFilterExpression');
expect(source).toContain('buildMetricAttributeExistsFilterExpression');
expect(source).toContain('buildMetricAttributeNotExistsFilterExpression');
expect(source).toContain('applyMetricAttributeExcludeFilter');
expect(source).toContain('applyMetricAttributeContainsFilter');
expect(source).toContain('applyMetricAttributeNotContainsFilter');
+ expect(source).toContain('applyMetricAttributeInFilter');
+ expect(source).toContain('applyMetricAttributeNotInFilter');
expect(source).toContain('applyMetricAttributeExistsFilter');
expect(source).toContain('applyMetricAttributeNotExistsFilter');
expect(source).toContain('data-otlp-metrics-attribute-filter-out-action={row.name}');
@@ -1058,6 +1064,10 @@ describe('otlp metrics page', () => {
expect(source).toContain('data-otlp-metrics-attribute-contains-action-owner="hertzbeat-ui-button"');
expect(source).toContain('data-otlp-metrics-attribute-not-contains-action={row.name}');
expect(source).toContain('data-otlp-metrics-attribute-not-contains-action-owner="hertzbeat-ui-button"');
+
expect(source).toContain('data-otlp-metrics-attribute-in-action={row.name}');
+
expect(source).toContain('data-otlp-metrics-attribute-in-action-owner="hertzbeat-ui-button"');
+
expect(source).toContain('data-otlp-metrics-attribute-not-in-action={row.name}');
+
expect(source).toContain('data-otlp-metrics-attribute-not-in-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}');
@@ -3079,6 +3089,44 @@ describe('otlp metrics page', () => {
expect(notContainsParams.get('traceId')).toBe('trace-checkout');
expect(notContainsParams.get('spanId')).toBe('span-checkout');
+ const serviceInAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-in-action="service.name"]')
as HTMLButtonElement | null;
+
expect(serviceInAction?.getAttribute('data-otlp-metrics-attribute-in-action-owner')).toBe('hertzbeat-ui-button');
+
expect(serviceInAction?.getAttribute('aria-label')).toContain('service.name');
+
+ await act(async () => {
+ serviceInAction?.dispatchEvent(new MouseEvent('click', { bubbles: true
}));
+ await Promise.resolve();
+ });
+
+ const inHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
+ const inParams = new URL(inHref, 'http://localhost').searchParams;
+ expect(inParams.get('filter')).toContain('service.name IN ("checkout")');
+ expect(inParams.get('series')).toBe('checkout_latency-0');
+ expect(inParams.get('entityId')).toBe('7');
+ expect(inParams.get('serviceName')).toBe('checkout');
+ expect(inParams.get('environment')).toBe('prod');
+ expect(inParams.get('traceId')).toBe('trace-checkout');
+ expect(inParams.get('spanId')).toBe('span-checkout');
+
+ const serviceNotInAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-not-in-action="service.name"]')
as HTMLButtonElement | null;
+
expect(serviceNotInAction?.getAttribute('data-otlp-metrics-attribute-not-in-action-owner')).toBe('hertzbeat-ui-button');
+
expect(serviceNotInAction?.getAttribute('aria-label')).toContain('service.name');
+
+ await act(async () => {
+ serviceNotInAction?.dispatchEvent(new MouseEvent('click', { bubbles:
true }));
+ await Promise.resolve();
+ });
+
+ const notInHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
+ const notInParams = new URL(notInHref, 'http://localhost').searchParams;
+ expect(notInParams.get('filter')).toContain('service.name NOT IN
("checkout")');
+ expect(notInParams.get('series')).toBe('checkout_latency-0');
+ expect(notInParams.get('entityId')).toBe('7');
+ expect(notInParams.get('serviceName')).toBe('checkout');
+ expect(notInParams.get('environment')).toBe('prod');
+ expect(notInParams.get('traceId')).toBe('trace-checkout');
+ expect(notInParams.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');
diff --git a/web-next/app/log/manage/log-manage-page.tsx
b/web-next/app/log/manage/log-manage-page.tsx
index a683f3d229..2ee01d09ef 100644
--- a/web-next/app/log/manage/log-manage-page.tsx
+++ b/web-next/app/log/manage/log-manage-page.tsx
@@ -255,6 +255,10 @@ function isSafeLogAttributeFilterValue(value: string) {
return Boolean(trimmed && trimmed !== '-' && !trimmed.includes(',') &&
!/\s+and\s+/i.test(trimmed));
}
+function formatLogAttributeListFilterValue(value: string) {
+ return `"${value.trim().replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
+}
+
function resolveLogAttributeFilterKind(row: LogAttributeRow): 'resource' |
'attribute' | null {
if (row.key.startsWith('resource-')) return 'resource';
if (row.key.startsWith('attribute-')) return 'attribute';
@@ -301,6 +305,26 @@ function buildLogAttributeNotContainsExpression(row:
LogAttributeRow, objectValu
return { kind, expression: `${key} NOT CONTAINS ${value}` };
}
+function buildLogAttributeInExpression(row: LogAttributeRow, objectValueLabel:
string) {
+ const kind = resolveLogAttributeFilterKind(row);
+ const key = row.name.trim();
+ const value = row.value.trim();
+ if (!kind || value === objectValueLabel || !isSafeLogAttributeFilterKey(key)
|| !isSafeLogAttributeFilterValue(value)) {
+ return null;
+ }
+ return { kind, expression: `${key} IN
(${formatLogAttributeListFilterValue(value)})` };
+}
+
+function buildLogAttributeNotInExpression(row: LogAttributeRow,
objectValueLabel: string) {
+ const kind = resolveLogAttributeFilterKind(row);
+ const key = row.name.trim();
+ const value = row.value.trim();
+ if (!kind || value === objectValueLabel || !isSafeLogAttributeFilterKey(key)
|| !isSafeLogAttributeFilterValue(value)) {
+ return null;
+ }
+ return { kind, expression: `${key} NOT IN
(${formatLogAttributeListFilterValue(value)})` };
+}
+
function buildLogAttributeExistsExpression(row: LogAttributeRow) {
const kind = resolveLogAttributeFilterKind(row);
const key = row.name.trim();
@@ -2287,6 +2311,32 @@ function LogManageExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft, t]);
+ const applyLogAttributeInFilter = useCallback((row: LogAttributeRow) => {
+ const filter = buildLogAttributeInExpression(row,
t('log.manage.attributes.value.object'));
+ 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, t]);
+
+ const applyLogAttributeNotInFilter = useCallback((row: LogAttributeRow) => {
+ const filter = buildLogAttributeNotInExpression(row,
t('log.manage.attributes.value.object'));
+ 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, t]);
+
const applyLogAttributeExistsFilter = useCallback((row: LogAttributeRow) => {
const filter = buildLogAttributeExistsExpression(row);
if (!filter) return;
@@ -2404,12 +2454,14 @@ function LogManageExplorer({
const excludeFilter = buildLogAttributeExcludeExpression(row,
t('log.manage.attributes.value.object'));
const containsFilter = buildLogAttributeContainsExpression(row,
t('log.manage.attributes.value.object'));
const notContainsFilter = buildLogAttributeNotContainsExpression(row,
t('log.manage.attributes.value.object'));
+ const inFilter = buildLogAttributeInExpression(row,
t('log.manage.attributes.value.object'));
+ const notInFilter = buildLogAttributeNotInExpression(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 && !containsFilter && !notContainsFilter &&
!existsFilter && !notExistsFilter && !group && !fieldColumn) return null;
+ if (!filter && !excludeFilter && !containsFilter && !notContainsFilter &&
!inFilter && !notInFilter && !existsFilter && !notExistsFilter && !group &&
!fieldColumn) return null;
return (
<span className="inline-flex flex-wrap gap-1">
{fieldColumn ? (
@@ -2546,6 +2598,44 @@ function LogManageExplorer({
{t('log.manage.attributes.not-contains-action')}
</HzButton>
) : null}
+ {inFilter ? (
+ <HzButton
+ data-log-manage-attribute-in-action={inFilter.kind}
+ data-log-manage-attribute-in-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={() => applyLogAttributeInFilter(row)}
+ aria-label={t('log.manage.attributes.in-action.aria', { name:
row.name, value: row.value })}
+ >
+ <HzButtonIcon
+ icon={ListChecks}
+ data-log-manage-attribute-in-icon="in"
+
data-log-manage-attribute-in-icon-owner="hertzbeat-ui-button-icon"
+ />
+ {t('log.manage.attributes.in-action')}
+ </HzButton>
+ ) : null}
+ {notInFilter ? (
+ <HzButton
+ data-log-manage-attribute-not-in-action={notInFilter.kind}
+ data-log-manage-attribute-not-in-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={() => applyLogAttributeNotInFilter(row)}
+ aria-label={t('log.manage.attributes.not-in-action.aria', { name:
row.name, value: row.value })}
+ >
+ <HzButtonIcon
+ icon={Ban}
+ data-log-manage-attribute-not-in-icon="not-in"
+
data-log-manage-attribute-not-in-icon-owner="hertzbeat-ui-button-icon"
+ />
+ {t('log.manage.attributes.not-in-action')}
+ </HzButton>
+ ) : null}
{existsFilter ? (
<HzButton
data-log-manage-attribute-exists-action={existsFilter.kind}
@@ -2605,7 +2695,7 @@ function LogManageExplorer({
) : null}
</span>
);
- }, [applyLogAttributeContainsFilter, applyLogAttributeExistsFilter,
applyLogAttributeFieldColumn, applyLogAttributeFilter,
applyLogAttributeNotContainsFilter, applyLogAttributeNotExistsFilter,
applyLogContextAttributeFilter, excludeLogAttributeFilter, groupLogAttribute,
replaceLogAttributeFilter, t, visibleLogFieldColumns]);
+ }, [applyLogAttributeContainsFilter, applyLogAttributeExistsFilter,
applyLogAttributeFieldColumn, applyLogAttributeFilter,
applyLogAttributeInFilter, applyLogAttributeNotContainsFilter,
applyLogAttributeNotExistsFilter, applyLogAttributeNotInFilter,
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 41c4004a3b..3fcac0abca 100644
--- a/web-next/app/log/manage/page.test.tsx
+++ b/web-next/app/log/manage/page.test.tsx
@@ -659,6 +659,12 @@ describe('log manage page', () => {
expect(source).toContain('buildLogAttributeNotContainsExpression');
expect(source).toContain('applyLogAttributeNotContainsFilter');
expect(source).toContain('data-log-manage-attribute-not-contains-action={notContainsFilter.kind}');
+ expect(source).toContain('buildLogAttributeInExpression');
+ expect(source).toContain('applyLogAttributeInFilter');
+
expect(source).toContain('data-log-manage-attribute-in-action={inFilter.kind}');
+ expect(source).toContain('buildLogAttributeNotInExpression');
+ expect(source).toContain('applyLogAttributeNotInFilter');
+
expect(source).toContain('data-log-manage-attribute-not-in-action={notInFilter.kind}');
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');
@@ -3574,6 +3580,64 @@ 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
NOT CONTAINS /checkout/:id');
+
+ const inResourceAction = interactionContainer.querySelector(
+
'[data-log-manage-attribute-in-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
+ ) as HTMLButtonElement | null;
+ const inAttributeAction = interactionContainer.querySelector(
+
'[data-log-manage-attribute-in-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
+ ) as HTMLButtonElement | null;
+ expect(inResourceAction).toBeTruthy();
+ expect(inAttributeAction).toBeTruthy();
+
expect(inResourceAction?.getAttribute('data-log-manage-attribute-in-owner')).toBe('hertzbeat-ui-button');
+
expect(inAttributeAction?.getAttribute('data-log-manage-attribute-in-owner')).toBe('hertzbeat-ui-button');
+
+ mockState.replace.mockClear();
+ await act(async () => {
+ inResourceAction?.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
IN ("1.2.3")');
+
+ mockState.replace.mockClear();
+ await act(async () => {
+ inAttributeAction?.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
IN ("/checkout/:id")');
+
+ const notInResourceAction = interactionContainer.querySelector(
+
'[data-log-manage-attribute-not-in-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
+ ) as HTMLButtonElement | null;
+ const notInAttributeAction = interactionContainer.querySelector(
+
'[data-log-manage-attribute-not-in-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
+ ) as HTMLButtonElement | null;
+ expect(notInResourceAction).toBeTruthy();
+ expect(notInAttributeAction).toBeTruthy();
+
expect(notInResourceAction?.getAttribute('data-log-manage-attribute-not-in-owner')).toBe('hertzbeat-ui-button');
+
expect(notInAttributeAction?.getAttribute('data-log-manage-attribute-not-in-owner')).toBe('hertzbeat-ui-button');
+
+ mockState.replace.mockClear();
+ await act(async () => {
+ notInResourceAction?.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 IN ("1.2.3")');
+
+ mockState.replace.mockClear();
+ await act(async () => {
+ notInAttributeAction?.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 IN ("/checkout/:id")');
} 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 85819e546f..3faab0651c 100644
--- a/web-next/app/trace/manage/page.test.tsx
+++ b/web-next/app/trace/manage/page.test.tsx
@@ -1802,12 +1802,16 @@ describe('trace manage page', () => {
expect(source).toContain('buildTraceResourceExcludeFilterExpression');
expect(source).toContain('buildTraceResourceContainsFilterExpression');
expect(source).toContain('buildTraceResourceNotContainsFilterExpression');
+ expect(source).toContain('buildTraceResourceInFilterExpression');
+ expect(source).toContain('buildTraceResourceNotInFilterExpression');
expect(source).toContain('buildTraceResourceExistsFilterExpression');
expect(source).toContain('buildTraceResourceNotExistsFilterExpression');
expect(source).toContain('buildTraceSpanAttributeFilterExpression');
expect(source).toContain('buildTraceSpanAttributeExcludeFilterExpression');
expect(source).toContain('buildTraceSpanAttributeContainsFilterExpression');
expect(source).toContain('buildTraceSpanAttributeNotContainsFilterExpression');
+ expect(source).toContain('buildTraceSpanAttributeInFilterExpression');
+ expect(source).toContain('buildTraceSpanAttributeNotInFilterExpression');
expect(source).toContain('buildTraceSpanAttributeExistsFilterExpression');
expect(source).toContain('buildTraceSpanAttributeNotExistsFilterExpression');
expect(source).toContain('buildTraceSpanAttributeGroupBy');
@@ -1816,6 +1820,8 @@ describe('trace manage page', () => {
expect(source).toContain('excludeTraceResourceFilter');
expect(source).toContain('applyTraceResourceContainsFilter');
expect(source).toContain('applyTraceResourceNotContainsFilter');
+ expect(source).toContain('applyTraceResourceInFilter');
+ expect(source).toContain('applyTraceResourceNotInFilter');
expect(source).toContain('applyTraceResourceExistsFilter');
expect(source).toContain('applyTraceResourceNotExistsFilter');
expect(source).toContain('replaceTraceResourceFilter');
@@ -1823,6 +1829,8 @@ describe('trace manage page', () => {
expect(source).toContain('excludeTraceSpanAttributeFilter');
expect(source).toContain('applyTraceSpanAttributeContainsFilter');
expect(source).toContain('applyTraceSpanAttributeNotContainsFilter');
+ expect(source).toContain('applyTraceSpanAttributeInFilter');
+ expect(source).toContain('applyTraceSpanAttributeNotInFilter');
expect(source).toContain('applyTraceSpanAttributeExistsFilter');
expect(source).toContain('applyTraceSpanAttributeNotExistsFilter');
expect(source).toContain('replaceTraceSpanAttributeFilter');
@@ -1839,6 +1847,10 @@ describe('trace manage page', () => {
expect(source).toContain('data-trace-manage-drawer-span-attribute-contains-action-owner="hertzbeat-ui-button"');
expect(source).toContain('data-trace-manage-drawer-span-attribute-not-contains-action="true"');
expect(source).toContain('data-trace-manage-drawer-span-attribute-not-contains-action-owner="hertzbeat-ui-button"');
+
expect(source).toContain('data-trace-manage-drawer-span-attribute-in-action="true"');
+
expect(source).toContain('data-trace-manage-drawer-span-attribute-in-action-owner="hertzbeat-ui-button"');
+
expect(source).toContain('data-trace-manage-drawer-span-attribute-not-in-action="true"');
+
expect(source).toContain('data-trace-manage-drawer-span-attribute-not-in-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"');
@@ -1865,6 +1877,12 @@ describe('trace manage page', () => {
expect(source).toContain('data-trace-manage-drawer-resource-not-contains-action="true"');
expect(source).toContain('data-trace-manage-drawer-resource-not-contains-action-owner="hertzbeat-ui-button"');
expect(source).toContain("t('trace.manage.drawer.attributes.not-contains-action')");
+
expect(source).toContain('data-trace-manage-drawer-resource-in-action="true"');
+
expect(source).toContain('data-trace-manage-drawer-resource-in-action-owner="hertzbeat-ui-button"');
+ expect(source).toContain("t('trace.manage.drawer.attributes.in-action')");
+
expect(source).toContain('data-trace-manage-drawer-resource-not-in-action="true"');
+
expect(source).toContain('data-trace-manage-drawer-resource-not-in-action-owner="hertzbeat-ui-button"');
+
expect(source).toContain("t('trace.manage.drawer.attributes.not-in-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"');
diff --git a/web-next/app/trace/manage/trace-manage-page.tsx
b/web-next/app/trace/manage/trace-manage-page.tsx
index 10385658ad..9a2d006b61 100644
--- a/web-next/app/trace/manage/trace-manage-page.tsx
+++ b/web-next/app/trace/manage/trace-manage-page.tsx
@@ -497,6 +497,10 @@ function isSafeTraceResourceFilterValue(value: string) {
return Boolean(trimmed && trimmed !== '-' && !trimmed.includes(',') &&
!/\s+and\s+/i.test(trimmed));
}
+function formatTraceResourceListFilterValue(value: string) {
+ return `"${value.trim().replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
+}
+
function buildTraceResourceFilterExpression(name: React.ReactNode, value:
React.ReactNode) {
const key = String(name ?? '').trim();
const filterValue = String(value ?? '').trim();
@@ -533,6 +537,24 @@ function
buildTraceResourceNotContainsFilterExpression(name: React.ReactNode, va
return `${key} NOT CONTAINS ${filterValue}`;
}
+function buildTraceResourceInFilterExpression(name: React.ReactNode, value:
React.ReactNode) {
+ const key = String(name ?? '').trim();
+ const filterValue = String(value ?? '').trim();
+ if (!isSafeTraceResourceFilterKey(key) ||
!isSafeTraceResourceFilterValue(filterValue)) {
+ return null;
+ }
+ return `${key} IN (${formatTraceResourceListFilterValue(filterValue)})`;
+}
+
+function buildTraceResourceNotInFilterExpression(name: React.ReactNode, value:
React.ReactNode) {
+ const key = String(name ?? '').trim();
+ const filterValue = String(value ?? '').trim();
+ if (!isSafeTraceResourceFilterKey(key) ||
!isSafeTraceResourceFilterValue(filterValue)) {
+ return null;
+ }
+ return `${key} NOT IN (${formatTraceResourceListFilterValue(filterValue)})`;
+}
+
function buildTraceResourceExistsFilterExpression(name: React.ReactNode) {
const key = String(name ?? '').trim();
if (!isSafeTraceResourceFilterKey(key)) {
@@ -565,6 +587,14 @@ function
buildTraceSpanAttributeNotContainsFilterExpression(name: React.ReactNod
return buildTraceResourceNotContainsFilterExpression(name, value);
}
+function buildTraceSpanAttributeInFilterExpression(name: React.ReactNode,
value: React.ReactNode) {
+ return buildTraceResourceInFilterExpression(name, value);
+}
+
+function buildTraceSpanAttributeNotInFilterExpression(name: React.ReactNode,
value: React.ReactNode) {
+ return buildTraceResourceNotInFilterExpression(name, value);
+}
+
function buildTraceSpanAttributeExistsFilterExpression(name: React.ReactNode) {
return buildTraceResourceExistsFilterExpression(name);
}
@@ -829,6 +859,8 @@ function TraceWaterfallDrawer({
onExcludeSpanAttributeFilter,
onApplySpanAttributeContainsFilter,
onApplySpanAttributeNotContainsFilter,
+ onApplySpanAttributeInFilter,
+ onApplySpanAttributeNotInFilter,
onApplySpanAttributeExistsFilter,
onApplySpanAttributeNotExistsFilter,
onReplaceSpanAttributeFilter,
@@ -837,6 +869,8 @@ function TraceWaterfallDrawer({
onExcludeResourceFilter,
onApplyResourceContainsFilter,
onApplyResourceNotContainsFilter,
+ onApplyResourceInFilter,
+ onApplyResourceNotInFilter,
onApplyResourceExistsFilter,
onApplyResourceNotExistsFilter,
onReplaceResourceFilter,
@@ -855,6 +889,8 @@ function TraceWaterfallDrawer({
onExcludeSpanAttributeFilter: (name: string, value: string) => void;
onApplySpanAttributeContainsFilter: (name: string, value: string) => void;
onApplySpanAttributeNotContainsFilter: (name: string, value: string) => void;
+ onApplySpanAttributeInFilter: (name: string, value: string) => void;
+ onApplySpanAttributeNotInFilter: (name: string, value: string) => void;
onApplySpanAttributeExistsFilter: (name: string) => void;
onApplySpanAttributeNotExistsFilter: (name: string) => void;
onReplaceSpanAttributeFilter: (name: string, value: string) => void;
@@ -863,6 +899,8 @@ function TraceWaterfallDrawer({
onExcludeResourceFilter: (name: string, value: string) => void;
onApplyResourceContainsFilter: (name: string, value: string) => void;
onApplyResourceNotContainsFilter: (name: string, value: string) => void;
+ onApplyResourceInFilter: (name: string, value: string) => void;
+ onApplyResourceNotInFilter: (name: string, value: string) => void;
onApplyResourceExistsFilter: (name: string) => void;
onApplyResourceNotExistsFilter: (name: string) => void;
onReplaceResourceFilter: (name: string, value: string) => void;
@@ -1300,7 +1338,7 @@ function TraceWaterfallDrawer({
title: row.title,
copy: row.copy,
meta: row.meta,
- action: buildTraceSpanAttributeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeExcludeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeContainsFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeNotContainsFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeExistsFilterExpression(row.title) ||
buildTraceSpanAttributeNotExistsFilterExpression(row.title) ||
buildTraceSpanAttributeGroupBy(row.title) ? (
+ action: buildTraceSpanAttributeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeExcludeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeContainsFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeNotContainsFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeInFilterExpression(row.title, row.copy) ||
buildTraceSpanAttributeNotInFilterExpression(row.title, row.copy) ||
buildTraceSpanAttributeExistsFilterExpression(r [...]
<HzActionGroup
data-trace-manage-drawer-span-attribute-action-group="filter-group"
data-trace-manage-drawer-span-attribute-action-group-owner="hertzbeat-ui-action-group"
@@ -1360,6 +1398,32 @@ function TraceWaterfallDrawer({
<HzButtonIcon icon={Ban}
data-trace-manage-drawer-span-attribute-not-contains-action-icon="not-contains"
data-trace-manage-drawer-span-attribute-not-contains-action-icon-owner="hertzbeat-ui-button-icon"
/>
{t('trace.manage.drawer.attributes.not-contains-action')}
</HzButton>
+ <HzButton
+
data-trace-manage-drawer-span-attribute-in-action="true"
+
data-trace-manage-drawer-span-attribute-in-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={() =>
onApplySpanAttributeInFilter(row.title, row.copy)}
+
aria-label={t('trace.manage.drawer.attributes.in-action.aria', { name:
row.title, value: row.copy })}
+ >
+ <HzButtonIcon icon={ListChecks}
data-trace-manage-drawer-span-attribute-in-action-icon="in"
data-trace-manage-drawer-span-attribute-in-action-icon-owner="hertzbeat-ui-button-icon"
/>
+ {t('trace.manage.drawer.attributes.in-action')}
+ </HzButton>
+ <HzButton
+
data-trace-manage-drawer-span-attribute-not-in-action="true"
+
data-trace-manage-drawer-span-attribute-not-in-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={() =>
onApplySpanAttributeNotInFilter(row.title, row.copy)}
+
aria-label={t('trace.manage.drawer.attributes.not-in-action.aria', { name:
row.title, value: row.copy })}
+ >
+ <HzButtonIcon icon={Ban}
data-trace-manage-drawer-span-attribute-not-in-action-icon="not-in"
data-trace-manage-drawer-span-attribute-not-in-action-icon-owner="hertzbeat-ui-button-icon"
/>
+
{t('trace.manage.drawer.attributes.not-in-action')}
+ </HzButton>
<HzButton
data-trace-manage-drawer-span-attribute-exists-action="true"
data-trace-manage-drawer-span-attribute-exists-action-owner="hertzbeat-ui-button"
@@ -1427,7 +1491,7 @@ function TraceWaterfallDrawer({
title: row.title,
copy: row.copy,
meta: row.meta,
- action: buildTraceResourceFilterExpression(row.title,
row.copy) || buildTraceResourceExcludeFilterExpression(row.title, row.copy) ||
buildTraceResourceContainsFilterExpression(row.title, row.copy) ||
buildTraceResourceNotContainsFilterExpression(row.title, row.copy) ||
buildTraceResourceExistsFilterExpression(row.title) ||
buildTraceResourceNotExistsFilterExpression(row.title) ||
buildTraceResourceGroupBy(row.title) ? (
+ action: buildTraceResourceFilterExpression(row.title,
row.copy) || buildTraceResourceExcludeFilterExpression(row.title, row.copy) ||
buildTraceResourceContainsFilterExpression(row.title, row.copy) ||
buildTraceResourceNotContainsFilterExpression(row.title, row.copy) ||
buildTraceResourceInFilterExpression(row.title, row.copy) ||
buildTraceResourceNotInFilterExpression(row.title, row.copy) ||
buildTraceResourceExistsFilterExpression(row.title) || buildTraceResourceNotE
[...]
<HzActionGroup
data-trace-manage-drawer-resource-action-group="filter-group"
data-trace-manage-drawer-resource-action-group-owner="hertzbeat-ui-action-group"
@@ -1487,6 +1551,32 @@ function TraceWaterfallDrawer({
<HzButtonIcon icon={Ban}
data-trace-manage-drawer-resource-not-contains-action-icon="not-contains"
data-trace-manage-drawer-resource-not-contains-action-icon-owner="hertzbeat-ui-button-icon"
/>
{t('trace.manage.drawer.attributes.not-contains-action')}
</HzButton>
+ <HzButton
+ data-trace-manage-drawer-resource-in-action="true"
+
data-trace-manage-drawer-resource-in-action-owner="hertzbeat-ui-button"
+
data-trace-manage-drawer-resource-filter-name={row.title}
+
data-trace-manage-drawer-resource-filter-value={row.copy}
+ size="sm"
+ intent="secondary"
+ onClick={() => onApplyResourceInFilter(row.title,
row.copy)}
+
aria-label={t('trace.manage.drawer.attributes.in-action.aria', { name:
row.title, value: row.copy })}
+ >
+ <HzButtonIcon icon={ListChecks}
data-trace-manage-drawer-resource-in-action-icon="in"
data-trace-manage-drawer-resource-in-action-icon-owner="hertzbeat-ui-button-icon"
/>
+ {t('trace.manage.drawer.attributes.in-action')}
+ </HzButton>
+ <HzButton
+
data-trace-manage-drawer-resource-not-in-action="true"
+
data-trace-manage-drawer-resource-not-in-action-owner="hertzbeat-ui-button"
+
data-trace-manage-drawer-resource-filter-name={row.title}
+
data-trace-manage-drawer-resource-filter-value={row.copy}
+ size="sm"
+ intent="secondary"
+ onClick={() =>
onApplyResourceNotInFilter(row.title, row.copy)}
+
aria-label={t('trace.manage.drawer.attributes.not-in-action.aria', { name:
row.title, value: row.copy })}
+ >
+ <HzButtonIcon icon={Ban}
data-trace-manage-drawer-resource-not-in-action-icon="not-in"
data-trace-manage-drawer-resource-not-in-action-icon-owner="hertzbeat-ui-button-icon"
/>
+ {t('trace.manage.drawer.attributes.not-in-action')}
+ </HzButton>
<HzButton
data-trace-manage-drawer-resource-exists-action="true"
data-trace-manage-drawer-resource-exists-action-owner="hertzbeat-ui-button"
@@ -2063,6 +2153,28 @@ function TraceExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft]);
+ const applyTraceResourceInFilter = useCallback((name: string, value: string)
=> {
+ const expression = buildTraceResourceInFilterExpression(name, value);
+ if (!expression) return;
+ const nextQuery: TraceQueryState = {
+ ...draft,
+ resourceFilter: mergeTraceResourceFilterExpression(draft.resourceFilter,
expression)
+ };
+ setDraft(nextQuery);
+ applyQuery(nextQuery);
+ }, [applyQuery, draft, setDraft]);
+
+ const applyTraceResourceNotInFilter = useCallback((name: string, value:
string) => {
+ const expression = buildTraceResourceNotInFilterExpression(name, value);
+ if (!expression) return;
+ const nextQuery: TraceQueryState = {
+ ...draft,
+ resourceFilter: mergeTraceResourceFilterExpression(draft.resourceFilter,
expression)
+ };
+ setDraft(nextQuery);
+ applyQuery(nextQuery);
+ }, [applyQuery, draft, setDraft]);
+
const applyTraceResourceExistsFilter = useCallback((name: string) => {
const expression = buildTraceResourceExistsFilterExpression(name);
if (!expression) return;
@@ -2140,6 +2252,28 @@ function TraceExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft]);
+ const applyTraceSpanAttributeInFilter = useCallback((name: string, value:
string) => {
+ const expression = buildTraceSpanAttributeInFilterExpression(name, value);
+ if (!expression) return;
+ const nextQuery: TraceQueryState = {
+ ...draft,
+ attributeFilter:
mergeTraceResourceFilterExpression(draft.attributeFilter, expression)
+ };
+ setDraft(nextQuery);
+ applyQuery(nextQuery);
+ }, [applyQuery, draft, setDraft]);
+
+ const applyTraceSpanAttributeNotInFilter = useCallback((name: string, value:
string) => {
+ const expression = buildTraceSpanAttributeNotInFilterExpression(name,
value);
+ if (!expression) return;
+ const nextQuery: TraceQueryState = {
+ ...draft,
+ attributeFilter:
mergeTraceResourceFilterExpression(draft.attributeFilter, expression)
+ };
+ setDraft(nextQuery);
+ applyQuery(nextQuery);
+ }, [applyQuery, draft, setDraft]);
+
const applyTraceSpanAttributeExistsFilter = useCallback((name: string) => {
const expression = buildTraceSpanAttributeExistsFilterExpression(name);
if (!expression) return;
@@ -3722,6 +3856,8 @@ function TraceExplorer({
onExcludeSpanAttributeFilter={excludeTraceSpanAttributeFilter}
onApplySpanAttributeContainsFilter={applyTraceSpanAttributeContainsFilter}
onApplySpanAttributeNotContainsFilter={applyTraceSpanAttributeNotContainsFilter}
+ onApplySpanAttributeInFilter={applyTraceSpanAttributeInFilter}
+ onApplySpanAttributeNotInFilter={applyTraceSpanAttributeNotInFilter}
onApplySpanAttributeExistsFilter={applyTraceSpanAttributeExistsFilter}
onApplySpanAttributeNotExistsFilter={applyTraceSpanAttributeNotExistsFilter}
onReplaceSpanAttributeFilter={replaceTraceSpanAttributeFilter}
@@ -3730,6 +3866,8 @@ function TraceExplorer({
onExcludeResourceFilter={excludeTraceResourceFilter}
onApplyResourceContainsFilter={applyTraceResourceContainsFilter}
onApplyResourceNotContainsFilter={applyTraceResourceNotContainsFilter}
+ onApplyResourceInFilter={applyTraceResourceInFilter}
+ onApplyResourceNotInFilter={applyTraceResourceNotInFilter}
onApplyResourceExistsFilter={applyTraceResourceExistsFilter}
onApplyResourceNotExistsFilter={applyTraceResourceNotExistsFilter}
onReplaceResourceFilter={replaceTraceResourceFilter}
diff --git a/web-next/lib/i18n-runtime-messages.ts
b/web-next/lib/i18n-runtime-messages.ts
index cfa9be754b..b83f21414a 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -2743,6 +2743,10 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'trace.manage.drawer.attributes.contains-action.aria': 'Filter traces
where {{name}} contains {{value}}',
'trace.manage.drawer.attributes.not-contains-action': 'Not contains',
'trace.manage.drawer.attributes.not-contains-action.aria': 'Filter traces
where {{name}} does not contain {{value}}',
+ 'trace.manage.drawer.attributes.in-action': 'In',
+ 'trace.manage.drawer.attributes.in-action.aria': 'Filter traces where
{{name}} is in a list containing {{value}}',
+ 'trace.manage.drawer.attributes.not-in-action': 'Not in',
+ 'trace.manage.drawer.attributes.not-in-action.aria': 'Filter traces where
{{name}} is not in a list containing {{value}}',
'trace.manage.drawer.attributes.filter-out-action': 'Exclude',
'trace.manage.drawer.attributes.filter-out-action.aria': 'Exclude traces
where {{name}} equals {{value}}',
'trace.manage.drawer.attributes.exists-action': 'Exists',
@@ -3214,6 +3218,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.column.filter': 'Filter',
'otlp.metrics.attributes.column.contains': 'Contains',
'otlp.metrics.attributes.column.not-contains': 'Not contains',
+ 'otlp.metrics.attributes.column.in': 'In',
+ 'otlp.metrics.attributes.column.not-in': 'Not in',
'otlp.metrics.attributes.column.exclude': 'Exclude',
'otlp.metrics.attributes.column.exists': 'Exists',
'otlp.metrics.attributes.column.not-exists': 'Not exists',
@@ -3225,6 +3231,10 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.contains-action.aria': 'Filter metrics where
{{name}} contains {{value}}',
'otlp.metrics.attributes.not-contains-action': 'Not contains',
'otlp.metrics.attributes.not-contains-action.aria': 'Filter metrics where
{{name}} does not contain {{value}}',
+ 'otlp.metrics.attributes.in-action': 'In',
+ 'otlp.metrics.attributes.in-action.aria': 'Filter metrics where {{name}}
is in a list containing {{value}}',
+ 'otlp.metrics.attributes.not-in-action': 'Not in',
+ 'otlp.metrics.attributes.not-in-action.aria': 'Filter metrics where
{{name}} is not in a list containing {{value}}',
'otlp.metrics.attributes.filter-out-action': 'Exclude',
'otlp.metrics.attributes.filter-out-action.aria': 'Exclude metrics where
{{name}} equals {{value}}',
'otlp.metrics.attributes.exists-action': 'Exists',
@@ -4010,6 +4020,10 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'log.manage.attributes.contains-action.aria': 'Filter logs where {{name}}
contains {{value}}',
'log.manage.attributes.not-contains-action': 'Not contains',
'log.manage.attributes.not-contains-action.aria': 'Filter logs where
{{name}} does not contain {{value}}',
+ 'log.manage.attributes.in-action': 'In',
+ 'log.manage.attributes.in-action.aria': 'Filter logs where {{name}} is in
a list containing {{value}}',
+ 'log.manage.attributes.not-in-action': 'Not in',
+ 'log.manage.attributes.not-in-action.aria': 'Filter logs where {{name}} is
not in a list containing {{value}}',
'log.manage.attributes.context-filter-action': 'Filter context',
'log.manage.attributes.context-filter-action.aria': 'Filter surrounding
context where {{name}} equals {{value}}',
'log.manage.attributes.filter-out-action': 'Exclude',
@@ -7240,6 +7254,10 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'trace.manage.drawer.attributes.contains-action.aria': '过滤 {{name}} 包含
{{value}} 的链路',
'trace.manage.drawer.attributes.not-contains-action': '不包含',
'trace.manage.drawer.attributes.not-contains-action.aria': '过滤 {{name}}
不包含 {{value}} 的链路',
+ 'trace.manage.drawer.attributes.in-action': '属于',
+ 'trace.manage.drawer.attributes.in-action.aria': '过滤 {{name}} 属于包含
{{value}} 的列表的链路',
+ 'trace.manage.drawer.attributes.not-in-action': '不属于',
+ 'trace.manage.drawer.attributes.not-in-action.aria': '过滤 {{name}} 不属于包含
{{value}} 的列表的链路',
'trace.manage.drawer.attributes.filter-out-action': '排除',
'trace.manage.drawer.attributes.filter-out-action.aria': '排除 {{name}} 等于
{{value}} 的链路',
'trace.manage.drawer.attributes.exists-action': '存在',
@@ -7711,6 +7729,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.column.filter': '过滤',
'otlp.metrics.attributes.column.contains': '包含',
'otlp.metrics.attributes.column.not-contains': '不包含',
+ 'otlp.metrics.attributes.column.in': '属于',
+ 'otlp.metrics.attributes.column.not-in': '不属于',
'otlp.metrics.attributes.column.exclude': '排除',
'otlp.metrics.attributes.column.exists': '存在',
'otlp.metrics.attributes.column.not-exists': '不存在',
@@ -7722,6 +7742,10 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.contains-action.aria': '过滤 {{name}} 包含 {{value}}
的指标',
'otlp.metrics.attributes.not-contains-action': '不包含',
'otlp.metrics.attributes.not-contains-action.aria': '过滤 {{name}} 不包含
{{value}} 的指标',
+ 'otlp.metrics.attributes.in-action': '属于',
+ 'otlp.metrics.attributes.in-action.aria': '过滤 {{name}} 属于包含 {{value}}
的列表的指标',
+ 'otlp.metrics.attributes.not-in-action': '不属于',
+ 'otlp.metrics.attributes.not-in-action.aria': '过滤 {{name}} 不属于包含 {{value}}
的列表的指标',
'otlp.metrics.attributes.filter-out-action': '排除',
'otlp.metrics.attributes.filter-out-action.aria': '排除 {{name}} 等于
{{value}} 的指标',
'otlp.metrics.attributes.exists-action': '存在',
@@ -8507,6 +8531,10 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'log.manage.attributes.contains-action.aria': '过滤 {{name}} 包含 {{value}}
的日志',
'log.manage.attributes.not-contains-action': '不包含',
'log.manage.attributes.not-contains-action.aria': '过滤 {{name}} 不包含
{{value}} 的日志',
+ 'log.manage.attributes.in-action': '属于',
+ 'log.manage.attributes.in-action.aria': '过滤 {{name}} 属于包含 {{value}}
的列表的日志',
+ 'log.manage.attributes.not-in-action': '不属于',
+ 'log.manage.attributes.not-in-action.aria': '过滤 {{name}} 不属于包含 {{value}}
的列表的日志',
'log.manage.attributes.context-filter-action': '筛选上下文',
'log.manage.attributes.context-filter-action.aria': '筛选 {{name}} 等于
{{value}} 的周边日志',
'log.manage.attributes.filter-out-action': '排除',
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]