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 89bb210d29 Add signal contains filter actions
89bb210d29 is described below
commit 89bb210d29d1283873e6d170a6c97534aa6cb063
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 23:03:16 2026 +0800
Add signal contains filter actions
---
.../ingestion/otlp/metrics/otlp-metrics-page.tsx | 40 ++++++++++++
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 | 32 ++++++++++
web-next/app/trace/manage/page.test.tsx | 9 +++
web-next/app/trace/manage/trace-manage-page.tsx | 71 +++++++++++++++++++++-
web-next/lib/i18n-runtime-messages.ts | 14 +++++
7 files changed, 233 insertions(+), 4 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 e3f13ecff5..36e95fbb0e 100644
--- a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
+++ b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
@@ -124,6 +124,13 @@ function buildMetricAttributeExcludeFilterExpression(name:
string, value: string
return `${trimmedName}!="${escapeMetricFilterValue(trimmedValue)}"`;
}
+function buildMetricAttributeContainsFilterExpression(name: string, value:
string) {
+ const trimmedName = name.trim();
+ const trimmedValue = value.trim();
+ if (!trimmedName || !trimmedValue) return null;
+ return `${trimmedName} CONTAINS ${escapeMetricFilterValue(trimmedValue)}`;
+}
+
function buildMetricAttributeExistsFilterExpression(name: string) {
const trimmedName = name.trim();
if (!trimmedName) return null;
@@ -829,6 +836,17 @@ export default function OtlpMetricsPage() {
replaceMetricsRoute(nextDraft, undefined, series?.key || query.series,
series ? buildMetricSeriesRouteContext(series) : {});
}, [draft, query.series, replaceMetricsRoute]);
+ const applyMetricAttributeContainsFilter = useCallback((name: string, value:
string, series?: OtlpMetricSeriesView | null) => {
+ const expression = buildMetricAttributeContainsFilterExpression(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;
@@ -2637,6 +2655,28 @@ export default function OtlpMetricsPage() {
</HzButton>
)
},
+ {
+ key: 'contains',
+ header:
t('otlp.metrics.attributes.column.contains'),
+ render: row => (
+ <HzButton
+ type="button"
+ size="xs"
+ intent="ghost"
+
data-otlp-metrics-attribute-contains-action={row.name}
+
data-otlp-metrics-attribute-contains-action-owner="hertzbeat-ui-button"
+
aria-label={t('otlp.metrics.attributes.contains-action.aria', { name: row.name,
value: row.value })}
+ onClick={() =>
applyMetricAttributeContainsFilter(row.name, row.value, selectedMetricSeries)}
+ >
+ <HzButtonIcon
+ icon={Search}
+
data-otlp-metrics-attribute-contains-action-icon={row.name}
+
data-otlp-metrics-attribute-contains-action-icon-owner="hertzbeat-ui-button-icon"
+ />
+ {t('otlp.metrics.attributes.contains-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 2cf1ec73a6..a327961347 100644
--- a/web-next/app/ingestion/otlp/metrics/page.test.tsx
+++ b/web-next/app/ingestion/otlp/metrics/page.test.tsx
@@ -1037,17 +1037,22 @@ 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.contains')");
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('buildMetricAttributeExistsFilterExpression');
expect(source).toContain('buildMetricAttributeNotExistsFilterExpression');
expect(source).toContain('applyMetricAttributeExcludeFilter');
+ expect(source).toContain('applyMetricAttributeContainsFilter');
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-contains-action={row.name}');
+
expect(source).toContain('data-otlp-metrics-attribute-contains-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}');
@@ -3031,6 +3036,25 @@ describe('otlp metrics page', () => {
expect(filterParams.get('traceId')).toBe('trace-checkout');
expect(filterParams.get('spanId')).toBe('span-checkout');
+ const serviceContainsAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-contains-action="service.name"]')
as HTMLButtonElement | null;
+
expect(serviceContainsAction?.getAttribute('data-otlp-metrics-attribute-contains-action-owner')).toBe('hertzbeat-ui-button');
+
expect(serviceContainsAction?.getAttribute('aria-label')).toContain('service.name');
+
+ await act(async () => {
+ serviceContainsAction?.dispatchEvent(new MouseEvent('click', { bubbles:
true }));
+ await Promise.resolve();
+ });
+
+ const containsHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
+ const containsParams = new URL(containsHref,
'http://localhost').searchParams;
+ expect(containsParams.get('filter')).toContain('service.name CONTAINS
checkout');
+ expect(containsParams.get('series')).toBe('checkout_latency-0');
+ expect(containsParams.get('entityId')).toBe('7');
+ expect(containsParams.get('serviceName')).toBe('checkout');
+ expect(containsParams.get('environment')).toBe('prod');
+ expect(containsParams.get('traceId')).toBe('trace-checkout');
+ expect(containsParams.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 1355570ccd..3ff4f3af87 100644
--- a/web-next/app/log/manage/log-manage-page.tsx
+++ b/web-next/app/log/manage/log-manage-page.tsx
@@ -281,6 +281,16 @@ function buildLogAttributeExcludeExpression(row:
LogAttributeRow, objectValueLab
return { kind, expression: `${key}!=${value}` };
}
+function buildLogAttributeContainsExpression(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} CONTAINS ${value}` };
+}
+
function buildLogAttributeExistsExpression(row: LogAttributeRow) {
const kind = resolveLogAttributeFilterKind(row);
const key = row.name.trim();
@@ -2241,6 +2251,19 @@ function LogManageExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft, t]);
+ const applyLogAttributeContainsFilter = useCallback((row: LogAttributeRow)
=> {
+ const filter = buildLogAttributeContainsExpression(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;
@@ -2356,12 +2379,13 @@ function LogManageExplorer({
const renderLogAttributeFilterAction = useCallback((row: LogAttributeRow) =>
{
const filter = buildLogAttributeFilterExpression(row,
t('log.manage.attributes.value.object'));
const excludeFilter = buildLogAttributeExcludeExpression(row,
t('log.manage.attributes.value.object'));
+ const containsFilter = buildLogAttributeContainsExpression(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 && !notExistsFilter &&
!group && !fieldColumn) return null;
+ if (!filter && !excludeFilter && !containsFilter && !existsFilter &&
!notExistsFilter && !group && !fieldColumn) return null;
return (
<span className="inline-flex flex-wrap gap-1">
{fieldColumn ? (
@@ -2460,6 +2484,25 @@ function LogManageExplorer({
{t('log.manage.attributes.filter-out-action')}
</HzButton>
) : null}
+ {containsFilter ? (
+ <HzButton
+ data-log-manage-attribute-contains-action={containsFilter.kind}
+ data-log-manage-attribute-contains-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={() => applyLogAttributeContainsFilter(row)}
+ aria-label={t('log.manage.attributes.contains-action.aria', {
name: row.name, value: row.value })}
+ >
+ <HzButtonIcon
+ icon={Search}
+ data-log-manage-attribute-contains-icon="contains"
+
data-log-manage-attribute-contains-icon-owner="hertzbeat-ui-button-icon"
+ />
+ {t('log.manage.attributes.contains-action')}
+ </HzButton>
+ ) : null}
{existsFilter ? (
<HzButton
data-log-manage-attribute-exists-action={existsFilter.kind}
@@ -2519,7 +2562,7 @@ function LogManageExplorer({
) : null}
</span>
);
- }, [applyLogAttributeExistsFilter, applyLogAttributeFieldColumn,
applyLogAttributeFilter, applyLogAttributeNotExistsFilter,
applyLogContextAttributeFilter, excludeLogAttributeFilter, groupLogAttribute,
replaceLogAttributeFilter, t, visibleLogFieldColumns]);
+ }, [applyLogAttributeContainsFilter, 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 81518bd7b6..b3449a46a5 100644
--- a/web-next/app/log/manage/page.test.tsx
+++ b/web-next/app/log/manage/page.test.tsx
@@ -653,6 +653,9 @@ describe('log manage page', () => {
expect(source).toContain('data-log-manage-detail-facts-owner="hertzbeat-ui-detail-rows"');
expect(source).toContain('buildLogAttributeExistsExpression');
expect(source).toContain('applyLogAttributeExistsFilter');
+ expect(source).toContain('buildLogAttributeContainsExpression');
+ expect(source).toContain('applyLogAttributeContainsFilter');
+
expect(source).toContain('data-log-manage-attribute-contains-action={containsFilter.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');
@@ -3510,6 +3513,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
NOT EXISTS');
+
+ const containsResourceAction = interactionContainer.querySelector(
+
'[data-log-manage-attribute-contains-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
+ ) as HTMLButtonElement | null;
+ const containsAttributeAction = interactionContainer.querySelector(
+
'[data-log-manage-attribute-contains-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
+ ) as HTMLButtonElement | null;
+ expect(containsResourceAction).toBeTruthy();
+ expect(containsAttributeAction).toBeTruthy();
+
expect(containsResourceAction?.getAttribute('data-log-manage-attribute-contains-owner')).toBe('hertzbeat-ui-button');
+
expect(containsAttributeAction?.getAttribute('data-log-manage-attribute-contains-owner')).toBe('hertzbeat-ui-button');
+
+ mockState.replace.mockClear();
+ await act(async () => {
+ containsResourceAction?.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
CONTAINS 1.2.3');
+
+ mockState.replace.mockClear();
+ await act(async () => {
+ containsAttributeAction?.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
CONTAINS /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 448cefdc93..69c0fa1bae 100644
--- a/web-next/app/trace/manage/page.test.tsx
+++ b/web-next/app/trace/manage/page.test.tsx
@@ -1800,21 +1800,25 @@ describe('trace manage page', () => {
expect(source).toContain('selectedResourceAttributeRows');
expect(source).toContain('buildTraceResourceFilterExpression');
expect(source).toContain('buildTraceResourceExcludeFilterExpression');
+ expect(source).toContain('buildTraceResourceContainsFilterExpression');
expect(source).toContain('buildTraceResourceExistsFilterExpression');
expect(source).toContain('buildTraceResourceNotExistsFilterExpression');
expect(source).toContain('buildTraceSpanAttributeFilterExpression');
expect(source).toContain('buildTraceSpanAttributeExcludeFilterExpression');
+
expect(source).toContain('buildTraceSpanAttributeContainsFilterExpression');
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('applyTraceResourceContainsFilter');
expect(source).toContain('applyTraceResourceExistsFilter');
expect(source).toContain('applyTraceResourceNotExistsFilter');
expect(source).toContain('replaceTraceResourceFilter');
expect(source).toContain('applyTraceSpanAttributeFilter');
expect(source).toContain('excludeTraceSpanAttributeFilter');
+ expect(source).toContain('applyTraceSpanAttributeContainsFilter');
expect(source).toContain('applyTraceSpanAttributeExistsFilter');
expect(source).toContain('applyTraceSpanAttributeNotExistsFilter');
expect(source).toContain('replaceTraceSpanAttributeFilter');
@@ -1827,6 +1831,8 @@ describe('trace manage page', () => {
expect(source).toContain('data-trace-manage-drawer-span-attribute-filter-action-owner="hertzbeat-ui-button"');
expect(source).toContain('data-trace-manage-drawer-span-attribute-filter-out-action="true"');
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-contains-action="true"');
+
expect(source).toContain('data-trace-manage-drawer-span-attribute-contains-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"');
@@ -1847,6 +1853,9 @@ describe('trace manage page', () => {
expect(source).toContain('data-trace-manage-drawer-resource-filter-out-action="true"');
expect(source).toContain('data-trace-manage-drawer-resource-filter-out-action-owner="hertzbeat-ui-button"');
expect(source).toContain("t('trace.manage.drawer.attributes.filter-out-action')");
+
expect(source).toContain('data-trace-manage-drawer-resource-contains-action="true"');
+
expect(source).toContain('data-trace-manage-drawer-resource-contains-action-owner="hertzbeat-ui-button"');
+
expect(source).toContain("t('trace.manage.drawer.attributes.contains-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 f9b934b718..e05a6b6e14 100644
--- a/web-next/app/trace/manage/trace-manage-page.tsx
+++ b/web-next/app/trace/manage/trace-manage-page.tsx
@@ -515,6 +515,15 @@ function buildTraceResourceExcludeFilterExpression(name:
React.ReactNode, value:
return `${key}!=${filterValue}`;
}
+function buildTraceResourceContainsFilterExpression(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} CONTAINS ${filterValue}`;
+}
+
function buildTraceResourceExistsFilterExpression(name: React.ReactNode) {
const key = String(name ?? '').trim();
if (!isSafeTraceResourceFilterKey(key)) {
@@ -539,6 +548,10 @@ function
buildTraceSpanAttributeExcludeFilterExpression(name: React.ReactNode, v
return buildTraceResourceExcludeFilterExpression(name, value);
}
+function buildTraceSpanAttributeContainsFilterExpression(name:
React.ReactNode, value: React.ReactNode) {
+ return buildTraceResourceContainsFilterExpression(name, value);
+}
+
function buildTraceSpanAttributeExistsFilterExpression(name: React.ReactNode) {
return buildTraceResourceExistsFilterExpression(name);
}
@@ -801,12 +814,14 @@ function TraceWaterfallDrawer({
onApplyOperationFilter,
onApplySpanAttributeFilter,
onExcludeSpanAttributeFilter,
+ onApplySpanAttributeContainsFilter,
onApplySpanAttributeExistsFilter,
onApplySpanAttributeNotExistsFilter,
onReplaceSpanAttributeFilter,
onApplySpanAttributeGroupBy,
onApplyResourceFilter,
onExcludeResourceFilter,
+ onApplyResourceContainsFilter,
onApplyResourceExistsFilter,
onApplyResourceNotExistsFilter,
onReplaceResourceFilter,
@@ -823,12 +838,14 @@ function TraceWaterfallDrawer({
onApplyOperationFilter: (operationName: string) => void;
onApplySpanAttributeFilter: (name: string, value: string) => void;
onExcludeSpanAttributeFilter: (name: string, value: string) => void;
+ onApplySpanAttributeContainsFilter: (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;
+ onApplyResourceContainsFilter: (name: string, value: string) => void;
onApplyResourceExistsFilter: (name: string) => void;
onApplyResourceNotExistsFilter: (name: string) => void;
onReplaceResourceFilter: (name: string, value: string) => void;
@@ -1266,7 +1283,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) ||
buildTraceSpanAttributeNotExistsFilterExpression(row.title) ||
buildTraceSpanAttributeGroupBy(row.title) ? (
+ action: buildTraceSpanAttributeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeExcludeFilterExpression(row.title,
row.copy) || buildTraceSpanAttributeContainsFilterExpression(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"
@@ -1300,6 +1317,19 @@ function TraceWaterfallDrawer({
<HzButtonIcon icon={X}
data-trace-manage-drawer-span-attribute-filter-out-action-icon="exclude"
data-trace-manage-drawer-span-attribute-filter-out-action-icon-owner="hertzbeat-ui-button-icon"
/>
{t('trace.manage.drawer.attributes.filter-out-action')}
</HzButton>
+ <HzButton
+
data-trace-manage-drawer-span-attribute-contains-action="true"
+
data-trace-manage-drawer-span-attribute-contains-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={() =>
onApplySpanAttributeContainsFilter(row.title, row.copy)}
+
aria-label={t('trace.manage.drawer.attributes.contains-action.aria', { name:
row.title, value: row.copy })}
+ >
+ <HzButtonIcon icon={Search}
data-trace-manage-drawer-span-attribute-contains-action-icon="contains"
data-trace-manage-drawer-span-attribute-contains-action-icon-owner="hertzbeat-ui-button-icon"
/>
+
{t('trace.manage.drawer.attributes.contains-action')}
+ </HzButton>
<HzButton
data-trace-manage-drawer-span-attribute-exists-action="true"
data-trace-manage-drawer-span-attribute-exists-action-owner="hertzbeat-ui-button"
@@ -1367,7 +1397,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) ||
buildTraceResourceNotExistsFilterExpression(row.title) ||
buildTraceResourceGroupBy(row.title) ? (
+ action: buildTraceResourceFilterExpression(row.title,
row.copy) || buildTraceResourceExcludeFilterExpression(row.title, row.copy) ||
buildTraceResourceContainsFilterExpression(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"
@@ -1401,6 +1431,19 @@ function TraceWaterfallDrawer({
<HzButtonIcon icon={X}
data-trace-manage-drawer-resource-filter-out-action-icon="exclude"
data-trace-manage-drawer-resource-filter-out-action-icon-owner="hertzbeat-ui-button-icon"
/>
{t('trace.manage.drawer.attributes.filter-out-action')}
</HzButton>
+ <HzButton
+
data-trace-manage-drawer-resource-contains-action="true"
+
data-trace-manage-drawer-resource-contains-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={() =>
onApplyResourceContainsFilter(row.title, row.copy)}
+
aria-label={t('trace.manage.drawer.attributes.contains-action.aria', { name:
row.title, value: row.copy })}
+ >
+ <HzButtonIcon icon={Search}
data-trace-manage-drawer-resource-contains-action-icon="contains"
data-trace-manage-drawer-resource-contains-action-icon-owner="hertzbeat-ui-button-icon"
/>
+
{t('trace.manage.drawer.attributes.contains-action')}
+ </HzButton>
<HzButton
data-trace-manage-drawer-resource-exists-action="true"
data-trace-manage-drawer-resource-exists-action-owner="hertzbeat-ui-button"
@@ -1955,6 +1998,17 @@ function TraceExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft]);
+ const applyTraceResourceContainsFilter = useCallback((name: string, value:
string) => {
+ const expression = buildTraceResourceContainsFilterExpression(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;
@@ -2010,6 +2064,17 @@ function TraceExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft]);
+ const applyTraceSpanAttributeContainsFilter = useCallback((name: string,
value: string) => {
+ const expression = buildTraceSpanAttributeContainsFilterExpression(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;
@@ -3590,12 +3655,14 @@ function TraceExplorer({
onApplyOperationFilter={operationName =>
applyTraceQuickFilter('operationName', operationName)}
onApplySpanAttributeFilter={applyTraceSpanAttributeFilter}
onExcludeSpanAttributeFilter={excludeTraceSpanAttributeFilter}
+
onApplySpanAttributeContainsFilter={applyTraceSpanAttributeContainsFilter}
onApplySpanAttributeExistsFilter={applyTraceSpanAttributeExistsFilter}
onApplySpanAttributeNotExistsFilter={applyTraceSpanAttributeNotExistsFilter}
onReplaceSpanAttributeFilter={replaceTraceSpanAttributeFilter}
onApplySpanAttributeGroupBy={applyTraceSpanAttributeGroupBy}
onApplyResourceFilter={applyTraceResourceFilter}
onExcludeResourceFilter={excludeTraceResourceFilter}
+ onApplyResourceContainsFilter={applyTraceResourceContainsFilter}
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 07f90978ce..21abc762c5 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -2739,6 +2739,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'trace.manage.drawer.attributes.resource.meta': 'resourceAttributes',
'trace.manage.drawer.attributes.filter-action': 'Filter',
'trace.manage.drawer.attributes.filter-action.aria': 'Filter {{name}}
equals {{value}}',
+ 'trace.manage.drawer.attributes.contains-action': 'Contains',
+ 'trace.manage.drawer.attributes.contains-action.aria': 'Filter traces
where {{name}} contains {{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',
@@ -3208,6 +3210,7 @@ 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.contains': 'Contains',
'otlp.metrics.attributes.column.exclude': 'Exclude',
'otlp.metrics.attributes.column.exists': 'Exists',
'otlp.metrics.attributes.column.not-exists': 'Not exists',
@@ -3215,6 +3218,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'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.contains-action': 'Contains',
+ 'otlp.metrics.attributes.contains-action.aria': 'Filter metrics where
{{name}} contains {{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',
@@ -3996,6 +4001,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'log.manage.attributes.column.actions': 'Actions',
'log.manage.attributes.filter-action': 'Filter',
'log.manage.attributes.filter-action.aria': 'Filter {{name}} equals
{{value}}',
+ 'log.manage.attributes.contains-action': 'Contains',
+ 'log.manage.attributes.contains-action.aria': 'Filter logs where {{name}}
contains {{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',
@@ -7222,6 +7229,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'trace.manage.drawer.attributes.resource.meta': 'resourceAttributes',
'trace.manage.drawer.attributes.filter-action': '过滤',
'trace.manage.drawer.attributes.filter-action.aria': '过滤 {{name}} 等于
{{value}}',
+ 'trace.manage.drawer.attributes.contains-action': '包含',
+ 'trace.manage.drawer.attributes.contains-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': '存在',
@@ -7691,6 +7700,7 @@ 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.contains': '包含',
'otlp.metrics.attributes.column.exclude': '排除',
'otlp.metrics.attributes.column.exists': '存在',
'otlp.metrics.attributes.column.not-exists': '不存在',
@@ -7698,6 +7708,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.column.group': '分组',
'otlp.metrics.attributes.filter-action': '过滤',
'otlp.metrics.attributes.filter-action.aria': '过滤 {{name}} 等于 {{value}}',
+ 'otlp.metrics.attributes.contains-action': '包含',
+ 'otlp.metrics.attributes.contains-action.aria': '过滤 {{name}} 包含 {{value}}
的指标',
'otlp.metrics.attributes.filter-out-action': '排除',
'otlp.metrics.attributes.filter-out-action.aria': '排除 {{name}} 等于
{{value}} 的指标',
'otlp.metrics.attributes.exists-action': '存在',
@@ -8479,6 +8491,8 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'log.manage.attributes.column.actions': '操作',
'log.manage.attributes.filter-action': '过滤',
'log.manage.attributes.filter-action.aria': '过滤 {{name}} 等于 {{value}}',
+ 'log.manage.attributes.contains-action': '包含',
+ 'log.manage.attributes.contains-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]