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>
This commit is contained in:
co-authored by
André Pereira
parent
433a5fd464
commit
208df33e04
@@ -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
|
||||
|
||||
+2
-2
@@ -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(),
|
||||
|
||||
+3
-2
@@ -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(),
|
||||
|
||||
+2
-3
@@ -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;
|
||||
|
||||
+3
-2
@@ -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 = {
|
||||
|
||||
+18
-8
@@ -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);
|
||||
|
||||
+5
-5
@@ -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) => {
|
||||
|
||||
+2
-3
@@ -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(),
|
||||
|
||||
+3
-4
@@ -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,244 +124,273 @@ 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([]));
|
||||
});
|
||||
|
||||
it("should return spans whose tags' kv.key and kv.value match a filter", () => {
|
||||
expect(
|
||||
filterSpans({ ...defaultFilters, tags: [{ ...defaultTagFilter, 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' }] },
|
||||
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(
|
||||
{
|
||||
...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;
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
key?: string;
|
||||
operator: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export const randomId = () => uuidv4().slice(0, 12);
|
||||
|
||||
export const defaultTagFilter = {
|
||||
id: randomId(),
|
||||
operator: '=',
|
||||
};
|
||||
|
||||
export const defaultFilters = {
|
||||
spanNameOperator: '=',
|
||||
serviceNameOperator: '=',
|
||||
fromOperator: '>',
|
||||
toOperator: '<',
|
||||
tags: [defaultTagFilter],
|
||||
matchesOnly: false,
|
||||
criticalPathOnly: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Controls the state of search input that highlights spans if they match the search string.
|
||||
* @param spans
|
||||
* 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(spans?: TraceSpan[], initialFilters?: SearchProps) {
|
||||
const [search, setSearch] = useState<SearchProps>(merge(cloneDeep(defaultFilters), initialFilters ?? {}));
|
||||
export function useSearch(exploreId?: string, spans?: TraceSpan[], initialFilters?: TraceSearchProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Global state logic (for Explore)
|
||||
const panelState = useSelector((state) => state.explore.panes[exploreId ?? '']?.panelsState.trace);
|
||||
const { spanFilters: globalFilters } = panelState || {};
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
// Determine which state to use based on exploreId presence
|
||||
const search = exploreId
|
||||
? globalFilters || merge(cloneDeep(DEFAULT_SPAN_FILTERS), initialFilters ?? {})
|
||||
: localSearch;
|
||||
|
||||
// Ensure tags is always an array for safety
|
||||
if (search && (!search.tags || !Array.isArray(search.tags))) {
|
||||
search.tags = [{ id: randomId(), operator: '=' }];
|
||||
}
|
||||
|
||||
// Global state initialization (only when exploreId exists)
|
||||
useEffect(() => {
|
||||
if (initialFilters) {
|
||||
setSearch((prev) => {
|
||||
return merge(cloneDeep(prev), initialFilters);
|
||||
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 (!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>) => {
|
||||
|
||||
Reference in New Issue
Block a user