Trace View: Add span filters to URL (#107363)

* refactor(explore/spanFilters): move default filters and search props to shared folders

chore: ammend with refactor commit

chore: ammend, fix type errors

* feat(explore): add spanFilters to global state on explore

* feat(explore/spanFilters): sync span filters with global redux state

* feat(explore): sync spanFilters with URL state

* Moved span filters to panel state

* Fix types

* Fix tests in useSearch

* Fix TraceView tests

* Remove console.warn

* fix(test/trace-view-container): add span filter context to intial state

* refactor(traceview): use generic redux action

* fix(traceview): prune array objects correctly to preserve tag state in URL

---------

Co-authored-by: André Pereira <adrapereira@gmail.com>
pull/108200/head
Hasith De Alwis 4 days ago committed by GitHub
parent 433a5fd464
commit 208df33e04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      packages/grafana-data/src/index.ts
  2. 26
      packages/grafana-data/src/types/explore.ts
  3. 1
      public/app/features/explore/TraceView/TraceView.test.tsx
  4. 7
      public/app/features/explore/TraceView/TraceView.tsx
  5. 17
      public/app/features/explore/TraceView/TraceViewContainer.test.tsx
  6. 4
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.test.tsx
  7. 5
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.test.tsx
  8. 5
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx
  9. 5
      public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx
  10. 26
      public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx
  11. 10
      public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags.tsx
  12. 5
      public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx
  13. 7
      public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx
  14. 330
      public/app/features/explore/TraceView/components/utils/filter-spans.test.ts
  15. 23
      public/app/features/explore/TraceView/components/utils/filter-spans.tsx
  16. 51
      public/app/features/explore/TraceView/useSearch.test.ts
  17. 124
      public/app/features/explore/TraceView/useSearch.ts
  18. 14
      public/app/features/explore/hooks/useStateSync/external.utils.ts
  19. 19
      public/app/features/explore/state/constants.ts
  20. 8
      public/app/plugins/panel/traces/TagsEditor.tsx
  21. 6
      public/app/plugins/panel/traces/TracesPanel.tsx

@ -492,6 +492,8 @@ export type {
ExploreLogsPanelState,
SplitOpenOptions,
SplitOpen,
TraceSearchProps,
TraceSearchTag,
} from './types/explore';
export type { TraceKeyValuePair, TraceLog, TraceSpanReference, TraceSpanRow } from './types/trace';
export type { FlotDataPoint } from './types/flot';

@ -18,6 +18,31 @@ export type URLRange = {
to: URLRangeValue;
};
/**
* @internal
*/
export interface TraceSearchProps {
serviceName?: string;
serviceNameOperator: string;
spanName?: string;
spanNameOperator: string;
from?: string;
fromOperator: string;
to?: string;
toOperator: string;
tags: TraceSearchTag[];
query?: string;
matchesOnly: boolean;
criticalPathOnly: boolean;
}
export interface TraceSearchTag {
id: string;
key?: string;
operator: string;
value?: string;
}
/** @internal */
export interface ExploreUrlState<T extends DataQuery = AnyQuery> {
datasource: string | null;
@ -45,6 +70,7 @@ export interface ExploreCorrelationHelperData {
export interface ExploreTracePanelState {
spanId?: string;
spanFilters?: TraceSearchProps;
}
export interface ExploreLogsPanelState {

@ -19,7 +19,6 @@ function getTraceView(frames: DataFrame[]) {
return (
<Provider store={store}>
<TraceView
exploreId="left"
dataFrames={frames}
splitOpenFn={() => {}}
traceProp={transformDataFrames(frames[0])!}

@ -14,6 +14,7 @@ import {
mapInternalLinkToExplore,
SplitOpen,
TimeRange,
TraceSearchProps,
} from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { getTraceToLogsOptions, TraceToMetricsData, TraceToProfilesData } from '@grafana/o11y-ds-frontend';
@ -40,7 +41,7 @@ import { createSpanLinkFactory } from './createSpanLink';
import { useChildrenState } from './useChildrenState';
import { useDetailState } from './useDetailState';
import { useHoverIndentGuide } from './useHoverIndentGuide';
import { SearchProps, useSearch } from './useSearch';
import { useSearch } from './useSearch';
import { useViewRange } from './useViewRange';
const getStyles = (theme: GrafanaTheme2) => ({
@ -66,7 +67,7 @@ type Props = {
createSpanLink?: SpanLinkFunc;
focusedSpanId?: string;
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
spanFilters?: SearchProps;
spanFilters?: TraceSearchProps;
timeRange: TimeRange;
};
@ -98,7 +99,7 @@ export function TraceView(props: Props) {
const { removeHoverIndentGuideId, addHoverIndentGuideId, hoverIndentGuideIds } = useHoverIndentGuide();
const { viewRange, updateViewRangeTime, updateNextViewRangeTime } = useViewRange();
const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState();
const { search, setSearch, spanFilterMatches } = useSearch(traceProp?.spans, spanFilters);
const { search, setSearch, spanFilterMatches } = useSearch(exploreId, traceProp?.spans, spanFilters);
const [focusedSpanIdForSearch, setFocusedSpanIdForSearch] = useState('');
const [showSpanFilters, setShowSpanFilters] = useToggle(false);
const [headerHeight, setHeaderHeight] = useState(100);

@ -5,6 +5,8 @@ import { Provider } from 'react-redux';
import { TimeRange } from '@grafana/data';
import { configureStore } from '../../../store/configureStore';
import { initialExploreState } from '../state/main';
import { makeExplorePaneState } from '../state/utils';
// TODO: rebase after https://github.com/grafana/grafana/pull/105711, as this is already fixed
// eslint-disable-next-line no-restricted-imports
@ -20,7 +22,19 @@ jest.mock('@grafana/runtime', () => {
});
function renderTraceViewContainer(frames = [frameOld]) {
const store = configureStore();
const initialState = {
explore: {
...initialExploreState,
panes: {
left: makeExplorePaneState({
initialized: true,
datasourceInstance: null,
}),
},
},
};
const store = configureStore(initialState);
const { container, baseElement } = render(
<Provider store={store}>
@ -84,7 +98,6 @@ describe('TraceViewContainer', () => {
const tagOption = screen.getByText('component');
await waitFor(() => expect(tagOption).toBeInTheDocument());
await user.click(tagOption);
await waitFor(() => {
expect(
screen.queryAllByText('', { selector: 'div[data-testid="span-view"]' })[0].parentElement!.className

@ -17,8 +17,8 @@ import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { createTheme } from '@grafana/data';
import { DEFAULT_SPAN_FILTERS } from 'app/features/explore/state/constants';
import { defaultFilters } from '../../../useSearch';
import { trace } from '../mocks';
import NextPrevResult, { getStyles } from './NextPrevResult';
@ -39,7 +39,7 @@ describe('<NextPrevResult>', () => {
const [focusedSpanIndexForSearch, setFocusedSpanIndexForSearch] = useState(-1);
const searchBarProps = {
trace: trace,
search: defaultFilters,
search: DEFAULT_SPAN_FILTERS,
spanFilterMatches: props.matches ? new Set(props.matches) : undefined,
showSpanFilterMatchesOnly: false,
setShowSpanFilterMatchesOnly: jest.fn(),

@ -14,7 +14,8 @@
import { render, screen } from '@testing-library/react';
import { defaultFilters } from '../../../useSearch';
import { DEFAULT_SPAN_FILTERS } from 'app/features/explore/state/constants';
import { trace } from '../mocks';
import TracePageSearchBar from './TracePageSearchBar';
@ -23,7 +24,7 @@ describe('<TracePageSearchBar>', () => {
const TracePageSearchBarWithProps = (props: { matches: string[] | undefined }) => {
const searchBarProps = {
trace: trace,
search: defaultFilters,
search: DEFAULT_SPAN_FILTERS,
spanFilterMatches: props.matches ? new Set(props.matches) : undefined,
showSpanFilterMatchesOnly: false,
setShowSpanFilterMatchesOnly: jest.fn(),

@ -15,12 +15,11 @@
import { css } from '@emotion/css';
import { memo, Dispatch, SetStateAction, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2, TraceSearchProps } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { Button, Switch, useStyles2 } from '@grafana/ui';
import { getButtonStyles } from '@grafana/ui/internal';
import { SearchProps } from '../../../useSearch';
import { Trace } from '../../types/trace';
import { convertTimeFilter } from '../../utils/filter-spans';
@ -28,7 +27,7 @@ import NextPrevResult from './NextPrevResult';
export type TracePageSearchBarProps = {
trace: Trace;
search: SearchProps;
search: TraceSearchProps;
spanFilterMatches: Set<string> | undefined;
setShowSpanFilterMatchesOnly: (showMatchesOnly: boolean) => void;
setShowCriticalPathSpansOnly: (showCriticalPath: boolean) => void;

@ -2,7 +2,8 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { defaultFilters } from '../../../useSearch';
import { DEFAULT_SPAN_FILTERS } from 'app/features/explore/state/constants';
import { Trace } from '../../types/trace';
import { SpanFilters } from './SpanFilters';
@ -50,7 +51,7 @@ const trace: Trace = {
describe('SpanFilters', () => {
let user: ReturnType<typeof userEvent.setup>;
const SpanFiltersWithProps = ({ showFilters = true, matches }: { showFilters?: boolean; matches?: Set<string> }) => {
const [search, setSearch] = useState(defaultFilters);
const [search, setSearch] = useState(DEFAULT_SPAN_FILTERS);
const [showSpanFilterMatchesOnly, setShowSpanFilterMatchesOnly] = useState(false);
const [showCriticalPathSpansOnly, setShowCriticalPathSpansOnly] = useState(false);
const props = {

@ -13,14 +13,14 @@
// limitations under the License.
import { css } from '@emotion/css';
import React, { useState, useEffect, memo, useCallback } from 'react';
import React, { useState, useEffect, memo, useCallback, useRef } from 'react';
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
import { GrafanaTheme2, TraceSearchProps, SelectableValue, toOption } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { IntervalInput } from '@grafana/o11y-ds-frontend';
import { Collapse, Icon, InlineField, InlineFieldRow, Select, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { defaultFilters, SearchProps } from '../../../useSearch';
import { DEFAULT_SPAN_FILTERS } from '../../../../state/constants';
import { getTraceServiceNames, getTraceSpanNames } from '../../../utils/tags';
import SearchBarInput from '../../common/SearchBarInput';
import { Trace } from '../../types/trace';
@ -31,8 +31,8 @@ import { SpanFiltersTags } from './SpanFiltersTags';
export type SpanFilterProps = {
trace: Trace;
search: SearchProps;
setSearch: React.Dispatch<React.SetStateAction<SearchProps>>;
search: TraceSearchProps;
setSearch: (newSearch: TraceSearchProps) => void;
showSpanFilters: boolean;
setShowSpanFilters: (isOpen: boolean) => void;
setFocusedSpanIdForSearch: React.Dispatch<React.SetStateAction<string>>;
@ -57,6 +57,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
const [focusedSpanIndexForSearch, setFocusedSpanIndexForSearch] = useState(-1);
const [tagKeys, setTagKeys] = useState<Array<SelectableValue<string>>>();
const [tagValues, setTagValues] = useState<{ [key: string]: Array<SelectableValue<string>> }>({});
const prevTraceIdRef = useRef<string>();
const durationRegex = /^\d+(?:\.\d)?\d*(?:ns|us|µs|ms|s|m|h)$/;
@ -65,11 +66,20 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
setSpanNames(undefined);
setTagKeys(undefined);
setTagValues({});
setSearch(defaultFilters);
setSearch(DEFAULT_SPAN_FILTERS);
}, [setSearch]);
useEffect(() => {
clear();
// Only clear filters when trace ID actually changes (not on initial mount)
const currentTraceId = trace?.traceID;
const traceHasChanged = prevTraceIdRef.current && prevTraceIdRef.current !== currentTraceId;
if (traceHasChanged) {
clear();
}
prevTraceIdRef.current = currentTraceId;
}, [clear, trace]);
const setShowSpanFilterMatchesOnly = useCallback(
@ -90,7 +100,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
return null;
}
const setSpanFiltersSearch = (spanSearch: SearchProps) => {
const setSpanFiltersSearch = (spanSearch: TraceSearchProps) => {
setFocusedSpanIndexForSearch(-1);
setFocusedSpanIdForSearch('');
setSearch(spanSearch);

@ -2,18 +2,18 @@ import { css } from '@emotion/css';
import React from 'react';
import { useMount } from 'react-use';
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
import { GrafanaTheme2, SelectableValue, toOption, TraceSearchProps, TraceSearchTag } from '@grafana/data';
import { t } from '@grafana/i18n';
import { AccessoryButton } from '@grafana/plugin-ui';
import { Input, Select, Stack, useStyles2 } from '@grafana/ui';
import { randomId, SearchProps, Tag } from '../../../useSearch';
import { randomId } from '../../../../state/constants';
import { getTraceTagKeys, getTraceTagValues } from '../../../utils/tags';
import { Trace } from '../../types/trace';
interface Props {
search: SearchProps;
setSearch: (search: SearchProps) => void;
search: TraceSearchProps;
setSearch: (search: TraceSearchProps) => void;
trace: Trace;
tagKeys?: Array<SelectableValue<string>>;
setTagKeys: React.Dispatch<React.SetStateAction<Array<SelectableValue<string>> | undefined>>;
@ -47,7 +47,7 @@ export const SpanFiltersTags = ({ search, trace, setSearch, tagKeys, setTagKeys,
}
});
const onTagChange = (tag: Tag, v: SelectableValue<string>) => {
const onTagChange = (tag: TraceSearchTag, v: SelectableValue<string>) => {
setSearch({
...search,
tags: search.tags?.map((x) => {

@ -15,8 +15,7 @@
import { getByText, render } from '@testing-library/react';
import { MutableDataFrame } from '@grafana/data';
import { defaultFilters } from '../../useSearch';
import { DEFAULT_SPAN_FILTERS } from 'app/features/explore/state/constants';
import { TracePageHeader } from './TracePageHeader';
import { trace } from './mocks';
@ -25,7 +24,7 @@ const setup = () => {
const defaultProps = {
trace,
timeZone: '',
search: defaultFilters,
search: DEFAULT_SPAN_FILTERS,
setSearch: jest.fn(),
showSpanFilters: true,
setShowSpanFilters: jest.fn(),

@ -17,12 +17,11 @@ import cx from 'classnames';
import { memo, useEffect, useMemo } from 'react';
import * as React from 'react';
import { CoreApp, DataFrame, dateTimeFormat, GrafanaTheme2 } from '@grafana/data';
import { TraceSearchProps, CoreApp, DataFrame, dateTimeFormat, GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { TimeZone } from '@grafana/schema';
import { Badge, BadgeColor, Tooltip, useStyles2 } from '@grafana/ui';
import { SearchProps } from '../../useSearch';
import ExternalLinks from '../common/ExternalLinks';
import TraceName from '../common/TraceName';
import { getTraceLinks } from '../model/link-patterns';
@ -38,8 +37,8 @@ export type TracePageHeaderProps = {
data: DataFrame;
app?: CoreApp;
timeZone: TimeZone;
search: SearchProps;
setSearch: React.Dispatch<React.SetStateAction<SearchProps>>;
search: TraceSearchProps;
setSearch: (newSearch: TraceSearchProps) => void;
showSpanFilters: boolean;
setShowSpanFilters: (isOpen: boolean) => void;
setFocusedSpanIdForSearch: React.Dispatch<React.SetStateAction<string>>;

@ -12,7 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { defaultFilters, defaultTagFilter } from '../../useSearch';
import { DEFAULT_SPAN_FILTERS, DEFAULT_TAG_FILTERS } from 'app/features/explore/state/constants';
import { TraceSpan } from '../types/trace';
import { filterSpans } from './filter-spans';
@ -123,208 +124,222 @@ describe('filterSpans', () => {
const spans = [span0, span2] as TraceSpan[];
it('should return `undefined` if spans is falsy', () => {
expect(filterSpans({ ...defaultFilters, spanName: 'operationName' }, null)).toBe(undefined);
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, spanName: 'operationName' }, null)).toBe(undefined);
});
// Service / span name
it('should return spans whose serviceName match a filter', () => {
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName0' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName2' }, spans)).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName2', serviceNameOperator: '!=' }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName0' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName2' }, spans)).toEqual(new Set([spanID2]));
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName2', serviceNameOperator: '!=' }, spans)
).toEqual(new Set([spanID0]));
});
it('should return spans whose operationName match a filter', () => {
expect(filterSpans({ ...defaultFilters, spanName: 'operationName0' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...defaultFilters, spanName: 'operationName2' }, spans)).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, spanName: 'operationName2', spanNameOperator: '!=' }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, spanName: 'operationName0' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, spanName: 'operationName2' }, spans)).toEqual(new Set([spanID2]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, spanName: 'operationName2', spanNameOperator: '!=' }, spans)).toEqual(
new Set([spanID0])
);
});
// Durations
it('should return spans whose duration match a filter', () => {
expect(filterSpans({ ...defaultFilters, from: '2ns' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...defaultFilters, from: '2us' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...defaultFilters, from: '2ms' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...defaultFilters, from: '3.05ms' }, spans)).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, from: '3.05ms', fromOperator: '>=' }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, from: '2ns' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, from: '2us' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, from: '2ms' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, from: '3.05ms' }, spans)).toEqual(new Set([spanID2]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, from: '3.05ms', fromOperator: '>=' }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(filterSpans({ ...defaultFilters, from: '3.05ms', fromOperator: '>=', to: '4ms' }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, from: '3.05ms', fromOperator: '>=', to: '4ms' }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, to: '4ms' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...defaultFilters, to: '5ms', toOperator: '<=' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, to: '4ms' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, to: '5ms', toOperator: '<=' }, spans)).toEqual(
new Set([spanID0, spanID2])
);
});
// Tags
it('should return spans whose tags kv.key match a filter', () => {
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1' }] }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey0' }] }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey0' }] }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2' }] }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2' }] }, spans)).toEqual(
new Set([spanID2])
);
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2', operator: '!=' }] }, spans)
filterSpans(
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2', operator: '!=' }] },
spans
)
).toEqual(new Set([spanID0]));
});
it('should return spans whose kind, statusCode, statusMessage, libraryName, libraryVersion, traceState, or id match a filter', () => {
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind' }] }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'kind' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', value: 'kind0' }] }, spans)
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'kind', value: 'kind0' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'kind', operator: '!=', value: 'kind0' }] },
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'kind', operator: '!=', value: 'kind0' }] },
spans
)
).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status' }] }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'status' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', value: 'unset' }] }, spans)
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'status', value: 'unset' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status', operator: '!=', value: 'unset' }] },
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'status', operator: '!=', value: 'unset' }] },
spans
)
).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'status.message' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'status.message', value: 'statusMessage0' }] },
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'status.message', value: 'statusMessage0' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{
...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'status.message', operator: '!=', value: 'statusMessage0' }],
...DEFAULT_SPAN_FILTERS,
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'status.message', operator: '!=', value: 'statusMessage0' }],
},
spans
)
).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'library.name' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.name', value: 'libraryName' }] },
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'library.name', value: 'libraryName' }] },
spans
)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpans(
{
...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'library.name', operator: '!=', value: 'libraryName' }],
...DEFAULT_SPAN_FILTERS,
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'library.name', operator: '!=', value: 'libraryName' }],
},
spans
)
).toEqual(new Set([]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'library.version' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'library.version', value: 'libraryVersion0' }] },
{
...DEFAULT_SPAN_FILTERS,
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'library.version', value: 'libraryVersion0' }],
},
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{
...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'library.version', operator: '!=', value: 'libraryVersion0' }],
...DEFAULT_SPAN_FILTERS,
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'library.version', operator: '!=', value: 'libraryVersion0' }],
},
spans
)
).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'trace.state' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'trace.state', value: 'traceState0' }] },
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'trace.state', value: 'traceState0' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{
...defaultFilters,
tags: [{ ...defaultTagFilter, key: 'trace.state', operator: '!=', value: 'traceState0' }],
...DEFAULT_SPAN_FILTERS,
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'trace.state', operator: '!=', value: 'traceState0' }],
},
spans
)
).toEqual(new Set([spanID2]));
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id' }] }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'id' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id', value: 'span-id-0' }] }, spans)
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'id', value: 'span-id-0' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'id', operator: '!=', value: 'span-id-0' }] },
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'id', operator: '!=', value: 'span-id-0' }] },
spans
)
).toEqual(new Set([spanID2]));
});
it('should return spans whose process.tags kv.key match a filter', () => {
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey1' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey0' }] }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey2' }] }, spans)).toEqual(
new Set([spanID2])
);
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'processTagKey2', operator: '!=' }] }, spans)
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'processTagKey1' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'processTagKey0' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'processTagKey2' }] }, spans)
).toEqual(new Set([spanID2]));
expect(
filterSpans(
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'processTagKey2', operator: '!=' }] },
spans
)
).toEqual(new Set([spanID0]));
});
it('should return spans whose logs have a field whose kv.key match a filter', () => {
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey1' }] }, spans)).toEqual(
new Set([spanID0, spanID2])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey0' }] }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey2' }] }, spans)).toEqual(
new Set([spanID2])
);
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey2', operator: '!=' }] }, spans)
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'logFieldKey1' }] }, spans)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'logFieldKey0' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'logFieldKey2' }] }, spans)
).toEqual(new Set([spanID2]));
expect(
filterSpans(
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'logFieldKey2', operator: '!=' }] },
spans
)
).toEqual(new Set([spanID0]));
});
it('it should return logs have a name which matches the filter', () => {
expect(filterSpans({ ...defaultFilters, query: 'logName0' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, query: 'logName0' }, spans)).toEqual(new Set([spanID0]));
});
it('should return no spans when logs is null', () => {
const nullSpan = { ...span0, logs: null };
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'logFieldKey1' }] }, [
filterSpans({ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'logFieldKey1' }] }, [
nullSpan,
] as unknown as TraceSpan[])
).toEqual(new Set([]));
@ -332,35 +347,50 @@ describe('filterSpans', () => {
it("should return spans whose tags' kv.key and kv.value match a filter", () => {
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1' }] }, spans)
filterSpans(
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', value: 'tagValue1' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '=' }] },
{
...DEFAULT_SPAN_FILTERS,
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', value: 'tagValue1', operator: '=' }],
},
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '!=' }] },
{
...DEFAULT_SPAN_FILTERS,
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', value: 'tagValue1', operator: '!=' }],
},
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '=~', value: 'tagValue' }] },
{
...DEFAULT_SPAN_FILTERS,
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', operator: '=~', value: 'tagValue' }],
},
spans
)
).toEqual(new Set([spanID0, spanID2]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '!~', value: 'tagValue1' }] },
{
...DEFAULT_SPAN_FILTERS,
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', operator: '!~', value: 'tagValue1' }],
},
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '!~', value: 'tag' }] },
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', operator: '!~', value: 'tag' }] },
spans
)
).toEqual(new Set([]));
@ -368,14 +398,23 @@ describe('filterSpans', () => {
it("should not return spans whose tags' kv.key match a filter but kv.value/operator does not match", () => {
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '!=' }] }, spans)
filterSpans(
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', operator: '!=' }] },
spans
)
).toEqual(new Set());
expect(
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey2', operator: '!=' }] }, spans)
filterSpans(
{ ...DEFAULT_SPAN_FILTERS, tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2', operator: '!=' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{ ...defaultFilters, tags: [{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1', operator: '!=' }] },
{
...DEFAULT_SPAN_FILTERS,
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', value: 'tagValue1', operator: '!=' }],
},
spans
)
).toEqual(new Set([spanID2]));
@ -386,10 +425,10 @@ describe('filterSpans', () => {
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
tags: [
{ ...defaultTagFilter, key: 'tagKey1' },
{ ...defaultTagFilter, key: 'tagKey0' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey0' },
],
},
spans
@ -398,10 +437,10 @@ describe('filterSpans', () => {
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
tags: [
{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1' },
{ ...defaultTagFilter, key: 'tagKey0' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', value: 'tagValue1' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey0' },
],
},
spans
@ -410,10 +449,10 @@ describe('filterSpans', () => {
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
tags: [
{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1' },
{ ...defaultTagFilter, key: 'tagKey0', value: 'tagValue0' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', value: 'tagValue1' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey0', value: 'tagValue0' },
],
},
spans
@ -424,10 +463,10 @@ describe('filterSpans', () => {
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
tags: [
{ ...defaultTagFilter, key: 'tagKey0' },
{ ...defaultTagFilter, key: 'tagKey2' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey0' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2' },
],
},
spans
@ -436,10 +475,10 @@ describe('filterSpans', () => {
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
tags: [
{ ...defaultTagFilter, key: 'tagKey0', value: '' },
{ ...defaultTagFilter, key: 'tagKey2' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey0', value: '' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2' },
],
},
spans
@ -450,10 +489,10 @@ describe('filterSpans', () => {
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
tags: [
{ ...defaultTagFilter, key: 'tagKey0', value: 'tagValue0' },
{ ...defaultTagFilter, key: 'tagKey2' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey0', value: 'tagValue0' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2' },
],
},
spans
@ -462,10 +501,10 @@ describe('filterSpans', () => {
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
tags: [
{ ...defaultTagFilter, key: 'tagKey0', value: 'tagValue0' },
{ ...defaultTagFilter, key: 'tagKey2', value: 'tagValue2' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey0', value: 'tagValue0' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2', value: 'tagValue2' },
],
},
spans
@ -474,10 +513,10 @@ describe('filterSpans', () => {
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
tags: [
{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1' },
{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue2' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', value: 'tagValue1' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', value: 'tagValue2' },
],
},
spans
@ -486,10 +525,10 @@ describe('filterSpans', () => {
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
tags: [
{ ...defaultTagFilter, key: 'tagKey1', value: 'tagValue1' },
{ ...defaultTagFilter, key: 'tagKey2', value: 'tagValue2' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', value: 'tagValue1' },
{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2', value: 'tagValue2' },
],
},
spans
@ -497,69 +536,78 @@ describe('filterSpans', () => {
).toEqual(new Set());
// query
expect(filterSpans({ ...defaultFilters, query: 'serviceName0' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...defaultFilters, query: 'tagKey1' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...defaultFilters, query: 'does_not_exist' }, spans)).toEqual(new Set([]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, query: 'serviceName0' }, spans)).toEqual(new Set([spanID0]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, query: 'tagKey1' }, spans)).toEqual(new Set([spanID0, spanID2]));
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, query: 'does_not_exist' }, spans)).toEqual(new Set([]));
});
// Multiple
it('should return spans with multiple filters', () => {
// service name + span name
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0' }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName2' }, spans)).toEqual(
new Set([])
);
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName0', spanName: 'operationName0' }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpans({ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName0', spanName: 'operationName2' }, spans)
).toEqual(new Set([]));
expect(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName2', spanNameOperator: '!=' },
{ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName0', spanName: 'operationName2', spanNameOperator: '!=' },
spans
)
).toEqual(new Set([spanID0]));
// service name + span name + duration
expect(
filterSpans({ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0', from: '2ms' }, spans)
filterSpans(
{ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName0', spanName: 'operationName0', from: '2ms' },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpans({ ...defaultFilters, serviceName: 'serviceName0', spanName: 'operationName0', to: '2ms' }, spans)
filterSpans(
{ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName0', spanName: 'operationName0', to: '2ms' },
spans
)
).toEqual(new Set([]));
expect(
filterSpans({ ...defaultFilters, serviceName: 'serviceName2', spanName: 'operationName2', to: '6ms' }, spans)
filterSpans(
{ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName2', spanName: 'operationName2', to: '6ms' },
spans
)
).toEqual(new Set([spanID2]));
// service name + tag key
expect(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName0', tags: [{ ...defaultTagFilter, key: 'tagKey0' }] },
{ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName0', tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey0' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName0', tags: [{ ...defaultTagFilter, key: 'tagKey1' }] },
{ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName0', tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1' }] },
spans
)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName2', tags: [{ ...defaultTagFilter, key: 'tagKey1' }] },
{ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName2', tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1' }] },
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName2', tags: [{ ...defaultTagFilter, key: 'tagKey2' }] },
{ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName2', tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2' }] },
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
serviceName: 'serviceName0',
tags: [{ ...defaultTagFilter, key: 'tagKey1', operator: '!=' }],
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey1', operator: '!=' }],
},
spans
)
@ -567,38 +615,38 @@ describe('filterSpans', () => {
// duration + tag
expect(
filterSpans({ ...defaultFilters, from: '2ms', tags: [{ ...defaultTagFilter, key: 'tagKey0' }] }, spans)
filterSpans({ ...DEFAULT_SPAN_FILTERS, from: '2ms', tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey0' }] }, spans)
).toEqual(new Set([spanID0]));
expect(
filterSpans(
{ ...defaultFilters, to: '5ms', toOperator: '<=', tags: [{ ...defaultTagFilter, key: 'tagKey2' }] },
{ ...DEFAULT_SPAN_FILTERS, to: '5ms', toOperator: '<=', tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2' }] },
spans
)
).toEqual(new Set([spanID2]));
// query + other
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName0', query: 'tag' }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName0', query: 'tag' }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName0', query: 'tagKey2' }, spans)).toEqual(
expect(filterSpans({ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName0', query: 'tagKey2' }, spans)).toEqual(
new Set([])
);
expect(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName2', spanName: 'operationName2', query: 'tagKey1' },
{ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName2', spanName: 'operationName2', query: 'tagKey1' },
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName2', spanName: 'operationName2', to: '6ms', query: 'kind2' },
{ ...DEFAULT_SPAN_FILTERS, serviceName: 'serviceName2', spanName: 'operationName2', to: '6ms', query: 'kind2' },
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
serviceName: 'serviceName0',
spanName: 'operationName0',
from: '2ms',
@ -612,14 +660,14 @@ describe('filterSpans', () => {
expect(
filterSpans(
{
...defaultFilters,
...DEFAULT_SPAN_FILTERS,
serviceName: 'serviceName0',
spanName: 'operationName2',
spanNameOperator: '!=',
from: '3.05ms',
fromOperator: '>=',
to: '3.5ms',
tags: [{ ...defaultTagFilter, key: 'tagKey2', operator: '!=' }],
tags: [{ ...DEFAULT_TAG_FILTERS, key: 'tagKey2', operator: '!=' }],
},
spans
)

@ -14,16 +14,15 @@
import { SpanStatusCode } from '@opentelemetry/api';
import { TraceKeyValuePair } from '@grafana/data';
import { TraceKeyValuePair, TraceSearchProps, TraceSearchTag } from '@grafana/data';
import { SearchProps, Tag } from '../../useSearch';
import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE, ID } from '../constants/span';
import TNil from '../types/TNil';
import { TraceSpan } from '../types/trace';
// filter spans where all filters added need to be true for each individual span that is returned
// i.e. the more filters added -> the more specific that the returned results are
export function filterSpans(searchProps: SearchProps, spans: TraceSpan[] | TNil) {
export function filterSpans(searchProps: TraceSearchProps, spans: TraceSpan[] | TNil) {
if (!spans) {
return undefined;
}
@ -101,7 +100,7 @@ export function getQueryMatches(query: string, spans: TraceSpan[] | TNil) {
return spans.filter(isSpanAMatch);
}
const getTagMatches = (spans: TraceSpan[], tags: Tag[]) => {
const getTagMatches = (spans: TraceSpan[], tags: TraceSearchTag[]) => {
// remove empty/default tags
tags = tags.filter((tag) => {
// tag.key === '' when it is cleared via pressing x icon in select field
@ -111,7 +110,7 @@ const getTagMatches = (spans: TraceSpan[], tags: Tag[]) => {
if (tags.length > 0) {
return spans.filter((span: TraceSpan) => {
// match against every tag filter
return tags.every((tag: Tag) => {
return tags.every((tag: TraceSearchTag) => {
if (tag.key && tag.value) {
if (
(tag.operator === '=' && checkKeyValConditionForMatch(tag, span)) ||
@ -146,7 +145,7 @@ const getTagMatches = (spans: TraceSpan[], tags: Tag[]) => {
return undefined;
};
const checkKeyValConditionForRegex = (tag: Tag, span: TraceSpan) => {
const checkKeyValConditionForRegex = (tag: TraceSearchTag, span: TraceSpan) => {
return (
span.tags.some((kv) => checkKeyAndValueForRegex(tag, kv)) ||
span.process.tags.some((kv) => checkKeyAndValueForRegex(tag, kv)) ||
@ -167,7 +166,7 @@ const checkKeyValConditionForRegex = (tag: Tag, span: TraceSpan) => {
);
};
const checkKeyValConditionForMatch = (tag: Tag, span: TraceSpan) => {
const checkKeyValConditionForMatch = (tag: TraceSearchTag, span: TraceSpan) => {
return (
span.tags.some((kv) => checkKeyAndValueForMatch(tag, kv)) ||
span.process.tags.some((kv) => checkKeyAndValueForMatch(tag, kv)) ||
@ -190,11 +189,11 @@ const checkKeyForMatch = (tagKey: string, key: string) => {
return tagKey === key.toString();
};
const checkKeyAndValueForMatch = (tag: Tag, kv: TraceKeyValuePair) => {
const checkKeyAndValueForMatch = (tag: TraceSearchTag, kv: TraceKeyValuePair) => {
return tag.key === kv.key && tag.value === getStringValue(kv.value);
};
const checkKeyAndValueForRegex = (tag: Tag, kv: TraceKeyValuePair) => {
const checkKeyAndValueForRegex = (tag: TraceSearchTag, kv: TraceKeyValuePair) => {
return kv.key.includes(tag.key || '') && getStringValue(kv.value).includes(tag.value || '');
};
@ -202,7 +201,7 @@ const getStringValue = (value: string | number | boolean | undefined) => {
return value ? value.toString() : '';
};
const getServiceNameMatches = (spans: TraceSpan[], searchProps: SearchProps) => {
const getServiceNameMatches = (spans: TraceSpan[], searchProps: TraceSearchProps) => {
return spans.filter((span: TraceSpan) => {
return searchProps.serviceNameOperator === '='
? span.process.serviceName === searchProps.serviceName
@ -210,7 +209,7 @@ const getServiceNameMatches = (spans: TraceSpan[], searchProps: SearchProps) =>
});
};
const getSpanNameMatches = (spans: TraceSpan[], searchProps: SearchProps) => {
const getSpanNameMatches = (spans: TraceSpan[], searchProps: TraceSearchProps) => {
return spans.filter((span: TraceSpan) => {
return searchProps.spanNameOperator === '='
? span.operationName === searchProps.spanName
@ -218,7 +217,7 @@ const getSpanNameMatches = (spans: TraceSpan[], searchProps: SearchProps) => {
});
};
const getDurationMatches = (spans: TraceSpan[], searchProps: SearchProps) => {
const getDurationMatches = (spans: TraceSpan[], searchProps: TraceSearchProps) => {
const from = convertTimeFilter(searchProps?.from || '');
const to = convertTimeFilter(searchProps?.to || '');
let filteredSpans: TraceSpan[] = [];

@ -1,7 +1,40 @@
import { configureStore } from '@reduxjs/toolkit';
import { act, renderHook } from '@testing-library/react';
import React, { ReactNode } from 'react';
import { Provider } from 'react-redux';
import { DEFAULT_SPAN_FILTERS } from '../state/constants';
import { TraceSpan } from './components/types/trace';
import { defaultFilters, useSearch } from './useSearch';
import { useSearch } from './useSearch';
// Create a mock store with the necessary structure
const createMockStore = (initialState = {}) => {
return configureStore({
reducer: {
explore: (state = { panes: {} }) => state,
},
preloadedState: {
explore: {
panes: {},
...initialState,
},
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
thunk: true,
serializableCheck: false,
immutableCheck: false,
}),
});
};
// Create a wrapper component that provides the Redux store
const createWrapper = (store: ReturnType<typeof createMockStore>) => {
return ({ children }: { children: ReactNode }) => {
return React.createElement(Provider, { store, children });
};
};
describe('useSearch', () => {
const spans = [
@ -28,15 +61,23 @@ describe('useSearch', () => {
];
it('returns matching span IDs', async () => {
const { result } = renderHook(() => useSearch(spans));
act(() => result.current.setSearch({ ...defaultFilters, serviceName: 'service1' }));
const store = createMockStore();
const wrapper = createWrapper(store);
// Use local state by not providing exploreId
const { result } = renderHook(() => useSearch(undefined, spans), { wrapper });
act(() => result.current.setSearch({ ...DEFAULT_SPAN_FILTERS, serviceName: 'service1' }));
expect(result.current.spanFilterMatches?.size).toBe(1);
expect(result.current.spanFilterMatches?.has('span1')).toBe(true);
});
it('works without spans', async () => {
const { result } = renderHook(() => useSearch());
act(() => result.current.setSearch({ ...defaultFilters, serviceName: 'service1' }));
const store = createMockStore();
const wrapper = createWrapper(store);
// Use local state by not providing exploreId
const { result } = renderHook(() => useSearch(), { wrapper });
act(() => result.current.setSearch({ ...DEFAULT_SPAN_FILTERS, serviceName: 'service1' }));
expect(result.current.spanFilterMatches).toBe(undefined);
});
});

@ -1,65 +1,87 @@
import { cloneDeep, merge } from 'lodash';
import { useEffect, useMemo, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useEffect, useMemo, useCallback, useState } from 'react';
import { InterpolateFunction } from '@grafana/data';
import { InterpolateFunction, TraceSearchProps } from '@grafana/data';
import { useDispatch, useSelector } from 'app/types/store';
import { DEFAULT_SPAN_FILTERS, randomId } from '../state/constants';
import { changePanelState } from '../state/explorePane';
import { TraceSpan } from './components/types/trace';
import { filterSpans } from './components/utils/filter-spans';
export interface SearchProps {
serviceName?: string;
serviceNameOperator: string;
spanName?: string;
spanNameOperator: string;
from?: string;
fromOperator: string;
to?: string;
toOperator: string;
tags: Tag[];
query?: string;
matchesOnly: boolean;
criticalPathOnly: boolean;
}
/**
* Controls the state of search input that highlights spans if they match the search string.
* Uses global state for Explore (when exploreId is provided) or local state for panels (when no exploreId).
* @param exploreId - The explore pane ID (optional, for global state management)
* @param spans - The trace spans to filter
* @param initialFilters - Initial filters to set
*/
export function useSearch(exploreId?: string, spans?: TraceSpan[], initialFilters?: TraceSearchProps) {
const dispatch = useDispatch();
export interface Tag {
id: string;
key?: string;
operator: string;
value?: string;
}
// Global state logic (for Explore)
const panelState = useSelector((state) => state.explore.panes[exploreId ?? '']?.panelsState.trace);
const { spanFilters: globalFilters } = panelState || {};
export const randomId = () => uuidv4().slice(0, 12);
// Local state logic (for TracesPanel and other non-Explore usage)
const [localSearch, setLocalSearch] = useState<TraceSearchProps>(() => {
const merged = merge(cloneDeep(DEFAULT_SPAN_FILTERS), initialFilters ?? {});
// Ensure tags is always an array
if (!merged.tags || !Array.isArray(merged.tags)) {
merged.tags = [{ id: randomId(), operator: '=' }];
}
return merged;
});
export const defaultTagFilter = {
id: randomId(),
operator: '=',
};
// Determine which state to use based on exploreId presence
const search = exploreId
? globalFilters || merge(cloneDeep(DEFAULT_SPAN_FILTERS), initialFilters ?? {})
: localSearch;
export const defaultFilters = {
spanNameOperator: '=',
serviceNameOperator: '=',
fromOperator: '>',
toOperator: '<',
tags: [defaultTagFilter],
matchesOnly: false,
criticalPathOnly: false,
};
// Ensure tags is always an array for safety
if (search && (!search.tags || !Array.isArray(search.tags))) {
search.tags = [{ id: randomId(), operator: '=' }];
}
/**
* Controls the state of search input that highlights spans if they match the search string.
* @param spans
*/
export function useSearch(spans?: TraceSpan[], initialFilters?: SearchProps) {
const [search, setSearch] = useState<SearchProps>(merge(cloneDeep(defaultFilters), initialFilters ?? {}));
// Global state initialization (only when exploreId exists)
useEffect(() => {
if (exploreId && !globalFilters) {
const mergedFilters = merge(cloneDeep(DEFAULT_SPAN_FILTERS), initialFilters ?? {});
// Ensure tags is always an array
if (!mergedFilters.tags || !Array.isArray(mergedFilters.tags)) {
mergedFilters.tags = [{ id: randomId(), operator: '=' }];
}
dispatch(changePanelState(exploreId, 'trace', { ...panelState, spanFilters: mergedFilters }));
}
}, [exploreId, initialFilters, globalFilters, dispatch, panelState]);
// Local state updates (only when no exploreId)
useEffect(() => {
if (initialFilters) {
setSearch((prev) => {
return merge(cloneDeep(prev), initialFilters);
if (!exploreId && initialFilters) {
setLocalSearch((prev) => {
const merged = merge(cloneDeep(prev), initialFilters);
// Ensure tags is always an array
if (!merged.tags || !Array.isArray(merged.tags)) {
merged.tags = [{ id: randomId(), operator: '=' }];
}
return merged;
});
}
}, [initialFilters]);
}, [exploreId, initialFilters]);
// Function to update span filters (global or local based on exploreId)
const setSearch = useCallback(
(newSearch: TraceSearchProps) => {
if (exploreId) {
dispatch(changePanelState(exploreId, 'trace', { ...panelState, spanFilters: newSearch }));
} else {
setLocalSearch(newSearch);
}
},
[exploreId, dispatch, panelState]
);
const spanFilterMatches: Set<string> | undefined = useMemo(() => {
return spans && filterSpans(search, spans);
@ -68,12 +90,18 @@ export function useSearch(spans?: TraceSpan[], initialFilters?: SearchProps) {
return { search, setSearch, spanFilterMatches };
}
export function replaceSearchVariables(replaceVariables: InterpolateFunction, search?: SearchProps) {
export function replaceSearchVariables(replaceVariables: InterpolateFunction, search?: TraceSearchProps) {
if (!search) {
return search;
}
const newSearch = { ...search };
// Ensure tags is always an array
if (!newSearch.tags || !Array.isArray(newSearch.tags)) {
newSearch.tags = [{ id: randomId(), operator: '=' }];
}
if (newSearch.query) {
newSearch.query = replaceVariables(newSearch.query);
}

@ -21,7 +21,19 @@ export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlStat
* if the resulting object is empty, returns undefined
**/
function pruneObject(obj: object): object | undefined {
let pruned = mapValues(obj, (value) => (isObject(value) ? pruneObject(value) : value));
let pruned = mapValues(obj, (value: unknown) => {
if (isObject(value)) {
if (Array.isArray(value)) {
// For arrays, recursively prune each item and filter out empty results
const prunedArray = value
.map((item: unknown) => (isObject(item) ? pruneObject(item) : item))
.filter((item: unknown) => !isEmpty(item));
return prunedArray.length > 0 ? prunedArray : undefined;
}
return pruneObject(value);
}
return value;
});
pruned = omitBy<typeof pruned>(pruned, isEmpty);
if (isEmpty(pruned)) {
return undefined;

@ -1,6 +1,25 @@
import { v4 as uuidv4 } from 'uuid';
import { config } from '@grafana/runtime';
export const DEFAULT_RANGE = {
from: `now-${config.exploreDefaultTimeOffset}`,
to: 'now',
};
export const randomId = () => uuidv4().slice(0, 12);
export const DEFAULT_TAG_FILTERS = {
id: randomId(),
operator: '=',
};
export const DEFAULT_SPAN_FILTERS = {
spanNameOperator: '=',
serviceNameOperator: '=',
fromOperator: '>',
toOperator: '<',
tags: [DEFAULT_TAG_FILTERS],
matchesOnly: false,
criticalPathOnly: false,
};

@ -1,12 +1,12 @@
import { useEffect, useMemo, useState } from 'react';
import { SelectableValue, StandardEditorProps } from '@grafana/data';
import { SelectableValue, StandardEditorProps, TraceSearchProps } from '@grafana/data';
import { SpanFiltersTags } from '../../../features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFiltersTags';
import { defaultTagFilter, SearchProps } from '../../../features/explore/TraceView/useSearch';
import { transformDataFrames } from '../../../features/explore/TraceView/utils/transform';
import { DEFAULT_TAG_FILTERS } from '../../../features/explore/state/constants';
type Props = StandardEditorProps<SearchProps, unknown, SearchProps>;
type Props = StandardEditorProps<TraceSearchProps, unknown, TraceSearchProps>;
export const TagsEditor = ({ value, onChange, context }: Props) => {
const trace = useMemo(() => transformDataFrames(context.data[0]), [context.data]);
@ -15,7 +15,7 @@ export const TagsEditor = ({ value, onChange, context }: Props) => {
useEffect(() => {
if (!value.tags) {
onChange({ ...value, tags: [defaultTagFilter] });
onChange({ ...value, tags: [DEFAULT_TAG_FILTERS] });
}
}, [onChange, value]);

@ -2,14 +2,14 @@ import { css } from '@emotion/css';
import { useMemo, createRef } from 'react';
import { useAsync } from 'react-use';
import { Field, LinkModel, PanelProps } from '@grafana/data';
import { TraceSearchProps, Field, LinkModel, PanelProps } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { TraceView } from 'app/features/explore/TraceView/TraceView';
import { SpanLinkFunc } from 'app/features/explore/TraceView/components/types/links';
import { transformDataFrames } from 'app/features/explore/TraceView/utils/transform';
import { replaceSearchVariables, SearchProps } from '../../../features/explore/TraceView/useSearch';
import { replaceSearchVariables } from '../../../features/explore/TraceView/useSearch';
const styles = {
wrapper: css({
@ -22,7 +22,7 @@ export interface TracesPanelOptions {
createSpanLink?: SpanLinkFunc;
focusedSpanId?: string;
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
spanFilters?: SearchProps;
spanFilters?: TraceSearchProps;
}
export const TracesPanel = ({ data, options, replaceVariables }: PanelProps<TracesPanelOptions>) => {

Loading…
Cancel
Save