Tracing: Full text search (#78628)

* Full text search

* Tests
pull/78744/head
Joey 2 years ago committed by GitHub
parent 630b8a30be
commit a49e1ded8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.test.tsx
  2. 20
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.tsx
  3. 11
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/TracePageSearchBar.tsx
  4. 1
      public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.test.tsx
  5. 17
      public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx
  6. 32
      public/app/features/explore/TraceView/components/common/SearchBarInput.tsx
  7. 37
      public/app/features/explore/TraceView/components/utils/filter-spans.test.ts
  8. 50
      public/app/features/explore/TraceView/components/utils/filter-spans.tsx
  9. 1
      public/app/features/explore/TraceView/useSearch.ts

@ -124,7 +124,7 @@ describe('<NextPrevResult>', () => {
jest.advanceTimersByTime(1000); jest.advanceTimersByTime(1000);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText(/Services: 2\/3/)).toBeDefined(); expect(screen.getByText(/Services: 2\/3/)).toBeDefined();
expect(screen.getByText(/Depth: 1\/1/)).toBeDefined(); expect(screen.getByText(/Depth: 1/)).toBeDefined();
}); });
}); });
}); });

@ -129,8 +129,6 @@ export default memo(function NextPrevResult(props: NextPrevResultProps) {
const getMatchesMetadata = useCallback( const getMatchesMetadata = useCallback(
(depth: number, services: number) => { (depth: number, services: number) => {
const matchedServices: string[] = [];
const matchedDepth: number[] = [];
let metadata = ( let metadata = (
<> <>
<span>{`${trace.spans.length} spans`}</span> <span>{`${trace.spans.length} spans`}</span>
@ -144,13 +142,6 @@ export default memo(function NextPrevResult(props: NextPrevResultProps) {
); );
if (spanFilterMatches) { if (spanFilterMatches) {
spanFilterMatches.forEach((spanID) => {
if (trace.processes[spanID]) {
matchedServices.push(trace.processes[spanID].serviceName);
matchedDepth.push(trace.spans.find((span) => span.spanID === spanID)?.depth || 0);
}
});
if (spanFilterMatches.size === 0) { if (spanFilterMatches.size === 0) {
metadata = ( metadata = (
<> <>
@ -167,6 +158,13 @@ export default memo(function NextPrevResult(props: NextPrevResultProps) {
? `${focusedSpanIndexForSearch + 1}/${spanFilterMatches.size} ${type}` ? `${focusedSpanIndexForSearch + 1}/${spanFilterMatches.size} ${type}`
: `${spanFilterMatches.size} ${type}`; : `${spanFilterMatches.size} ${type}`;
const matchedServices: string[] = [];
spanFilterMatches.forEach((spanID) => {
if (trace.processes[spanID]) {
matchedServices.push(trace.processes[spanID].serviceName);
}
});
metadata = ( metadata = (
<> <>
<span>{text}</span> <span>{text}</span>
@ -175,9 +173,7 @@ export default memo(function NextPrevResult(props: NextPrevResultProps) {
<div> <div>
Services: {new Set(matchedServices).size}/{services} Services: {new Set(matchedServices).size}/{services}
</div> </div>
<div> <div>Depth: {depth}</div>
Depth: {new Set(matchedDepth).size}/{depth}
</div>
</> </>
)} )}
</> </>

@ -69,9 +69,18 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps)
search.tags.some((tag) => { search.tags.some((tag) => {
return tag.key; return tag.key;
}) || }) ||
(search.query && search.query !== '') ||
showSpanFilterMatchesOnly showSpanFilterMatchesOnly
); );
}, [search.serviceName, search.spanName, search.from, search.to, search.tags, showSpanFilterMatchesOnly]); }, [
search.serviceName,
search.spanName,
search.from,
search.to,
search.tags,
search.query,
showSpanFilterMatchesOnly,
]);
return ( return (
<div className={styles.container}> <div className={styles.container}>

@ -144,6 +144,7 @@ describe('SpanFilters', () => {
expect(screen.getByText('ProcessKey1')).toBeInTheDocument(); expect(screen.getByText('ProcessKey1')).toBeInTheDocument();
expect(screen.getByText('LogKey0')).toBeInTheDocument(); expect(screen.getByText('LogKey0')).toBeInTheDocument();
expect(screen.getByText('LogKey1')).toBeInTheDocument(); expect(screen.getByText('LogKey1')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Find...')).toBeInTheDocument();
}); });
}); });

@ -23,6 +23,7 @@ import { Collapse, HorizontalGroup, Icon, InlineField, InlineFieldRow, Select, T
import { IntervalInput } from 'app/core/components/IntervalInput/IntervalInput'; import { IntervalInput } from 'app/core/components/IntervalInput/IntervalInput';
import { defaultFilters, randomId, SearchProps, Tag } from '../../../useSearch'; import { defaultFilters, randomId, SearchProps, Tag } from '../../../useSearch';
import SearchBarInput from '../../common/SearchBarInput';
import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE, ID } from '../../constants/span'; import { KIND, LIBRARY_NAME, LIBRARY_VERSION, STATUS, STATUS_MESSAGE, TRACE_STATE, ID } from '../../constants/span';
import { Trace } from '../../types'; import { Trace } from '../../types';
import NextPrevResult from '../SearchBar/NextPrevResult'; import NextPrevResult from '../SearchBar/NextPrevResult';
@ -298,7 +299,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Collapse label={collapseLabel} collapsible={true} isOpen={showSpanFilters} onToggle={setShowSpanFilters}> <Collapse label={collapseLabel} collapsible={true} isOpen={showSpanFilters} onToggle={setShowSpanFilters}>
<InlineFieldRow> <InlineFieldRow className={styles.flexContainer}>
<InlineField label="Service Name" labelWidth={16}> <InlineField label="Service Name" labelWidth={16}>
<HorizontalGroup spacing={'xs'}> <HorizontalGroup spacing={'xs'}>
<Select <Select
@ -318,6 +319,15 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
/> />
</HorizontalGroup> </HorizontalGroup>
</InlineField> </InlineField>
<SearchBarInput
onChange={(v) => {
setSpanFiltersSearch({ ...search, query: v });
if (v === '') {
setShowSpanFilterMatchesOnly(false);
}
}}
value={search.query || ''}
/>
</InlineFieldRow> </InlineFieldRow>
<InlineFieldRow> <InlineFieldRow>
<InlineField label="Span Name" labelWidth={16}> <InlineField label="Span Name" labelWidth={16}>
@ -479,6 +489,7 @@ SpanFilters.displayName = 'SpanFilters';
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
container: css` container: css`
label: SpanFilters;
margin: 0.5em 0 -${theme.spacing(1)} 0; margin: 0.5em 0 -${theme.spacing(1)} 0;
z-index: 5; z-index: 5;
@ -493,6 +504,10 @@ const getStyles = (theme: GrafanaTheme2) => {
margin: -2px 0 0 10px; margin: -2px 0 0 10px;
} }
`, `,
flexContainer: css({
display: 'flex',
justifyContent: 'space-between',
}),
addTag: css` addTag: css`
margin: 0 0 0 10px; margin: 0 0 0 10px;
`, `,

@ -16,21 +16,13 @@ import * as React from 'react';
import { IconButton, Input } from '@grafana/ui'; import { IconButton, Input } from '@grafana/ui';
import { TNil } from '../types';
type Props = { type Props = {
allowClear?: boolean;
inputProps: Record<string, unknown>;
location: Location;
trackFindFunction?: (str: string | TNil) => void;
value: string | undefined; value: string | undefined;
onChange: (value: string) => void; onChange: (value: string) => void;
}; };
export default class SearchBarInput extends React.PureComponent<Props> { export default class SearchBarInput extends React.PureComponent<Props> {
static defaultProps: Partial<Props> = { static defaultProps: Partial<Props> = {
inputProps: {},
trackFindFunction: undefined,
value: undefined, value: undefined,
}; };
@ -39,25 +31,21 @@ export default class SearchBarInput extends React.PureComponent<Props> {
}; };
render() { render() {
const { allowClear, inputProps, value } = this.props; const { value } = this.props;
const suffix = ( const suffix = (
<> <>{value && value.length && <IconButton name="times" onClick={this.clearUiFind} tooltip="Clear input" />}</>
{inputProps.suffix}
{allowClear && value && value.length && (
<IconButton name="times" onClick={this.clearUiFind} tooltip="Clear input" />
)}
</>
); );
return ( return (
<Input <div style={{ width: '200px' }}>
placeholder="Find..." <Input
{...inputProps} placeholder="Find..."
onChange={(e) => this.props.onChange(e.currentTarget.value)} onChange={(e) => this.props.onChange(e.currentTarget.value)}
suffix={suffix} suffix={suffix}
value={value} value={value}
/> />
</div>
); );
} }
} }

@ -465,6 +465,11 @@ describe('filterSpans', () => {
spans spans
) )
).toEqual(new Set()); ).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([]));
}); });
// Multiple // Multiple
@ -541,6 +546,38 @@ describe('filterSpans', () => {
) )
).toEqual(new Set([spanID2])); ).toEqual(new Set([spanID2]));
// query + other
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName0', query: 'tag' }, spans)).toEqual(
new Set([spanID0])
);
expect(filterSpans({ ...defaultFilters, serviceName: 'serviceName0', query: 'tagKey2' }, spans)).toEqual(
new Set([])
);
expect(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName2', spanName: 'operationName2', query: 'tagKey1' },
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpans(
{ ...defaultFilters, serviceName: 'serviceName2', spanName: 'operationName2', to: '6ms', query: 'kind2' },
spans
)
).toEqual(new Set([spanID2]));
expect(
filterSpans(
{
...defaultFilters,
serviceName: 'serviceName0',
spanName: 'operationName0',
from: '2ms',
query: 'logFieldKey1',
},
spans
)
).toEqual(new Set([spanID0]));
// all // all
expect( expect(
filterSpans( filterSpans(

@ -44,9 +44,59 @@ export function filterSpans(searchProps: SearchProps, spans: TraceSpan[] | TNil)
filteredSpans = true; filteredSpans = true;
} }
if (searchProps.query) {
const queryMatches = getQueryMatches(searchProps.query, spans);
if (queryMatches) {
spans = queryMatches;
filteredSpans = true;
}
}
return filteredSpans ? new Set(spans.map((span: TraceSpan) => span.spanID)) : undefined; return filteredSpans ? new Set(spans.map((span: TraceSpan) => span.spanID)) : undefined;
} }
export function getQueryMatches(query: string, spans: TraceSpan[] | TNil) {
if (!spans) {
return undefined;
}
const queryParts: string[] = [];
// split query by whitespace, remove empty strings, and extract filters
query
.split(/\s+/)
.filter(Boolean)
.forEach((w) => {
queryParts.push(w.toLowerCase());
});
const isTextInQuery = (queryParts: string[], text: string) =>
queryParts.some((queryPart) => text.toLowerCase().includes(queryPart));
const isTextInKeyValues = (kvs: TraceKeyValuePair[]) =>
kvs
? kvs.some((kv) => {
return isTextInQuery(queryParts, kv.key) || isTextInQuery(queryParts, kv.value.toString());
})
: false;
const isSpanAMatch = (span: TraceSpan) =>
isTextInQuery(queryParts, span.operationName) ||
isTextInQuery(queryParts, span.process.serviceName) ||
isTextInKeyValues(span.tags) ||
(span.kind && isTextInQuery(queryParts, span.kind)) ||
(span.statusCode !== undefined && isTextInQuery(queryParts, SpanStatusCode[span.statusCode])) ||
(span.statusMessage && isTextInQuery(queryParts, span.statusMessage)) ||
(span.instrumentationLibraryName && isTextInQuery(queryParts, span.instrumentationLibraryName)) ||
(span.instrumentationLibraryVersion && isTextInQuery(queryParts, span.instrumentationLibraryVersion)) ||
(span.traceState && isTextInQuery(queryParts, span.traceState)) ||
(span.logs !== null && span.logs.some((log) => isTextInKeyValues(log.fields))) ||
isTextInKeyValues(span.process.tags) ||
queryParts.some((queryPart) => queryPart === span.spanID);
return spans.filter(isSpanAMatch);
}
const getTagMatches = (spans: TraceSpan[], tags: Tag[]) => { const getTagMatches = (spans: TraceSpan[], tags: Tag[]) => {
// remove empty/default tags // remove empty/default tags
tags = tags.filter((tag) => { tags = tags.filter((tag) => {

@ -13,6 +13,7 @@ export interface SearchProps {
to?: string; to?: string;
toOperator: string; toOperator: string;
tags: Tag[]; tags: Tag[];
query?: string;
} }
export interface Tag { export interface Tag {

Loading…
Cancel
Save