Tracing: Add regex support for span filters (#89885)

* Tracing: Add regex support for span filters

* Update SpanFilters test

* Modify placeholder

---------

Co-authored-by: Ekta Sorathia <esorathia@ebay.com>
Co-authored-by: Joey Tawadrous <joey.tawadrous@grafana.com>
This commit is contained in:
ektasorathia 2024-07-23 03:10:54 -07:00 committed by GitHub
parent 80f201026f
commit 4779d8417d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 150 additions and 41 deletions

View File

@ -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 () => {

View File

@ -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}
/>
<span className={styles.tagValues}>
<Select
aria-label="Select tag value"
isClearable
key={tag.value}
onChange={(v) => {
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 === '!=') && (
<Select
aria-label="Select tag value"
isClearable
key={tag.value}
onChange={(v) => {
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 === '!~') && (
<Input
aria-label="Input tag value"
onChange={(v) => {
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 || ''}
/>
)}
</span>
{(tag.key || tag.value || search.tags.length > 1) && (
<AccessoryButton

View File

@ -199,6 +199,7 @@ describe('filterSpans', () => {
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", () => {

View File

@ -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) => {