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} /> - { + setSpanFiltersSearch({ + ...search, + tags: search.tags?.map((x) => { + return x.id === tag.id ? { ...x, value: v?.value || '' } : x; + }), + }); + }} + options={tagValues[tag.id] ? tagValues[tag.id] : []} + placeholder="Select value" + value={tag.value} + /> + )} + {(tag.operator === '=~' || tag.operator === '!~') && ( + { + setSpanFiltersSearch({ + ...search, + tags: search.tags?.map((x) => { + return x.id === tag.id ? { ...x, value: v?.currentTarget?.value || '' } : x; + }), + }); + }} + placeholder="Tag value" + width={18} + value={tag.value || ''} + /> + )} {(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) => {