diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx
index 9a465d8a389..cedc85d40a9 100644
--- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx
+++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx
@@ -99,7 +99,7 @@ describe('SpanFilters', () => {
const toValue = screen.getByLabelText('Select max span duration');
const tagKey = screen.getByLabelText('Select tag key');
const tagOperator = screen.getByLabelText('Select tag operator');
- const tagValue = screen.getByLabelText('Select tag value');
+ const tagSelectValue = screen.getByLabelText('Select tag value');
expect(serviceOperator).toBeInTheDocument();
expect(getElemText(serviceOperator)).toBe('=');
@@ -116,7 +116,7 @@ describe('SpanFilters', () => {
expect(tagKey).toBeInTheDocument();
expect(tagOperator).toBeInTheDocument();
expect(getElemText(tagOperator)).toBe('=');
- expect(tagValue).toBeInTheDocument();
+ expect(tagSelectValue).toBeInTheDocument();
await user.click(serviceValue);
jest.advanceTimersByTime(1000);
@@ -130,6 +130,13 @@ describe('SpanFilters', () => {
expect(screen.getByText('Span0')).toBeInTheDocument();
expect(screen.getByText('Span1')).toBeInTheDocument();
});
+ await user.click(tagOperator);
+ jest.advanceTimersByTime(1000);
+ await waitFor(() => {
+ expect(screen.getByText('!~')).toBeInTheDocument();
+ expect(screen.getByText('=~')).toBeInTheDocument();
+ expect(screen.getByText('!~')).toBeInTheDocument();
+ });
await user.click(tagKey);
jest.advanceTimersByTime(1000);
await waitFor(() => {
@@ -149,6 +156,7 @@ describe('SpanFilters', () => {
const serviceValue = screen.getByLabelText('Select service name');
const spanValue = screen.getByLabelText('Select span name');
const tagKey = screen.getByLabelText('Select tag key');
+ const tagOperator = screen.getByLabelText('Select tag operator');
const tagValue = screen.getByLabelText('Select tag value');
expect(getElemText(serviceValue)).toBe('All service names');
@@ -164,6 +172,9 @@ describe('SpanFilters', () => {
await selectAndCheckValue(user, tagKey, 'TagKey0');
expect(getElemText(tagValue)).toBe('Select value');
await selectAndCheckValue(user, tagValue, 'TagValue0');
+ expect(screen.queryByLabelText('Input tag value')).toBeNull();
+ await selectAndCheckValue(user, tagOperator, '=~');
+ expect(screen.getByLabelText('Input tag value')).toBeInTheDocument();
});
it('should order tag filters', async () => {
diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx
index 6345ebee034..efed2c92f06 100644
--- a/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx
+++ b/public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx
@@ -15,13 +15,22 @@
import { css } from '@emotion/css';
import { SpanStatusCode } from '@opentelemetry/api';
import { uniq } from 'lodash';
-import { useState, useEffect, memo, useCallback } from 'react';
-import * as React from 'react';
+import React, { useState, useEffect, memo, useCallback } from 'react';
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
import { AccessoryButton } from '@grafana/experimental';
import { IntervalInput } from '@grafana/o11y-ds-frontend';
-import { Collapse, HorizontalGroup, Icon, InlineField, InlineFieldRow, Select, Tooltip, useStyles2 } from '@grafana/ui';
+import {
+ Collapse,
+ HorizontalGroup,
+ Icon,
+ InlineField,
+ InlineFieldRow,
+ Select,
+ Tooltip,
+ useStyles2,
+ Input,
+} from '@grafana/ui';
import { defaultFilters, randomId, SearchProps, Tag } from '../../../useSearch';
import SearchBarInput from '../../common/SearchBarInput';
@@ -419,26 +428,44 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
}),
});
}}
- options={[toOption('='), toOption('!=')]}
+ options={[toOption('='), toOption('!='), toOption('=~'), toOption('!~')]}
value={tag.operator}
/>
-
{(tag.key || tag.value || search.tags.length > 1) && (
{
spans
)
).toEqual(new Set([spanID2]));
+
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
@@ -328,12 +329,36 @@ describe('filterSpans', () => {
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1' }] }, spans)
).toEqual(new Set([spanID0]));
+ expect(
+ filterSpans(
+ { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '=' }] },
+ spans
+ )
+ ).toEqual(new Set([spanID0]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '!=' }] },
spans
)
).toEqual(new Set([spanID2]));
+ expect(
+ filterSpans(
+ { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '=~', value: 'tagValue' }] },
+ spans
+ )
+ ).toEqual(new Set([spanID0, spanID2]));
+ expect(
+ filterSpans(
+ { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '!~', value: 'tagValue1' }] },
+ spans
+ )
+ ).toEqual(new Set([spanID2]));
+ expect(
+ filterSpans(
+ { ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '!~', value: 'tag' }] },
+ spans
+ )
+ ).toEqual(new Set([]));
});
it("should not return spans whose tags' kv.key match a filter but kv.value/operator does not match", () => {
diff --git a/public/app/features/explore/TraceView/components/utils/filter-spans.tsx b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx
index c141ca43ce7..44845220f38 100644
--- a/public/app/features/explore/TraceView/components/utils/filter-spans.tsx
+++ b/public/app/features/explore/TraceView/components/utils/filter-spans.tsx
@@ -109,25 +109,16 @@ const getTagMatches = (spans: TraceSpan[], tags: Tag[]) => {
// match against every tag filter
return tags.every((tag: Tag) => {
if (tag.key && tag.value) {
- if (
- span.tags.some((kv) => checkKeyAndValueForMatch(tag, kv)) ||
- span.process.tags.some((kv) => checkKeyAndValueForMatch(tag, kv)) ||
- (span.logs && span.logs.some((log) => log.fields.some((kv) => checkKeyAndValueForMatch(tag, kv)))) ||
- (span.kind && tag.key === KIND && tag.value === span.kind) ||
- (span.statusCode !== undefined &&
- tag.key === STATUS &&
- tag.value === SpanStatusCode[span.statusCode].toLowerCase()) ||
- (span.statusMessage && tag.key === STATUS_MESSAGE && tag.value === span.statusMessage) ||
- (span.instrumentationLibraryName &&
- tag.key === LIBRARY_NAME &&
- tag.value === span.instrumentationLibraryName) ||
- (span.instrumentationLibraryVersion &&
- tag.key === LIBRARY_VERSION &&
- tag.value === span.instrumentationLibraryVersion) ||
- (span.traceState && tag.key === TRACE_STATE && tag.value === span.traceState) ||
- (tag.key === ID && tag.value === span.spanID)
- ) {
+ if (tag.operator === '=' && checkKeyValConditionForMatch(tag, span)) {
return getReturnValue(tag.operator, true);
+ } else if (tag.operator === '=~' && checkKeyValConditionForRegex(tag, span)) {
+ return getReturnValue(tag.operator, false);
+ } else if (tag.operator === '!=' && !checkKeyValConditionForMatch(tag, span)) {
+ return getReturnValue(tag.operator, false);
+ } else if (tag.operator === '!~' && !checkKeyValConditionForRegex(tag, span)) {
+ return getReturnValue(tag.operator, false);
+ } else {
+ return false;
}
} else if (tag.key) {
if (
@@ -152,6 +143,46 @@ const getTagMatches = (spans: TraceSpan[], tags: Tag[]) => {
return undefined;
};
+const checkKeyValConditionForRegex = (tag: Tag, span: TraceSpan) => {
+ return (
+ span.tags.some((kv) => checkKeyAndValueForRegex(tag, kv)) ||
+ span.process.tags.some((kv) => checkKeyAndValueForRegex(tag, kv)) ||
+ (span.logs && span.logs.some((log) => log.fields.some((kv) => checkKeyAndValueForRegex(tag, kv)))) ||
+ (span.kind && tag.key === KIND && tag.value?.includes(span.kind)) ||
+ (span.statusCode !== undefined &&
+ tag.key === STATUS &&
+ tag.value?.includes(SpanStatusCode[span.statusCode].toLowerCase())) ||
+ (span.statusMessage && tag.key === STATUS_MESSAGE && tag.value?.includes(span.statusMessage)) ||
+ (span.instrumentationLibraryName &&
+ tag.key === LIBRARY_NAME &&
+ tag.value?.includes(span.instrumentationLibraryName)) ||
+ (span.instrumentationLibraryVersion &&
+ tag.key === LIBRARY_VERSION &&
+ tag.value?.includes(span.instrumentationLibraryVersion)) ||
+ (span.traceState && tag.key === TRACE_STATE && tag.value?.includes(span.traceState)) ||
+ (tag.key === ID && tag.value?.includes(span.spanID))
+ );
+};
+
+const checkKeyValConditionForMatch = (tag: Tag, span: TraceSpan) => {
+ return (
+ span.tags.some((kv) => checkKeyAndValueForMatch(tag, kv)) ||
+ span.process.tags.some((kv) => checkKeyAndValueForMatch(tag, kv)) ||
+ (span.logs && span.logs.some((log) => log.fields.some((kv) => checkKeyAndValueForMatch(tag, kv)))) ||
+ (span.kind && tag.key === KIND && tag.value === span.kind) ||
+ (span.statusCode !== undefined &&
+ tag.key === STATUS &&
+ tag.value === SpanStatusCode[span.statusCode].toLowerCase()) ||
+ (span.statusMessage && tag.key === STATUS_MESSAGE && tag.value === span.statusMessage) ||
+ (span.instrumentationLibraryName && tag.key === LIBRARY_NAME && tag.value === span.instrumentationLibraryName) ||
+ (span.instrumentationLibraryVersion &&
+ tag.key === LIBRARY_VERSION &&
+ tag.value === span.instrumentationLibraryVersion) ||
+ (span.traceState && tag.key === TRACE_STATE && tag.value === span.traceState) ||
+ (tag.key === ID && tag.value === span.spanID)
+ );
+};
+
const checkKeyForMatch = (tagKey: string, key: string) => {
return tagKey === key.toString() ? true : false;
};
@@ -160,8 +191,23 @@ const checkKeyAndValueForMatch = (tag: Tag, kv: TraceKeyValuePair) => {
return tag.key === kv.key.toString() && tag.value === kv.value.toString() ? true : false;
};
+const checkKeyAndValueForRegex = (tag: Tag, kv: TraceKeyValuePair) => {
+ return kv.key.toString().includes(tag.key || '') && kv.value.toString().includes(tag.value || '') ? true : false;
+};
+
const getReturnValue = (operator: string, found: boolean) => {
- return operator === '=' ? found : !found;
+ switch (operator) {
+ case '=':
+ return found;
+ case '!=':
+ return !found;
+ case '~=':
+ return !found;
+ case '!~':
+ return !found;
+ default:
+ return !found;
+ }
};
const getServiceNameMatches = (spans: TraceSpan[], searchProps: SearchProps) => {