mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 07:35:45 -06:00
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:
parent
80f201026f
commit
4779d8417d
@ -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 () => {
|
||||
|
@ -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,10 +428,11 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
}),
|
||||
});
|
||||
}}
|
||||
options={[toOption('='), toOption('!=')]}
|
||||
options={[toOption('='), toOption('!='), toOption('=~'), toOption('!~')]}
|
||||
value={tag.operator}
|
||||
/>
|
||||
<span className={styles.tagValues}>
|
||||
{(tag.operator === '=' || tag.operator === '!=') && (
|
||||
<Select
|
||||
aria-label="Select tag value"
|
||||
isClearable
|
||||
@ -439,6 +449,23 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
|
||||
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
|
||||
|
@ -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", () => {
|
||||
|
@ -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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user