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

@ -69,9 +69,18 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps)
search.tags.some((tag) => {
return tag.key;
}) ||
(search.query && search.query !== '') ||
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 (
<div className={styles.container}>

@ -144,6 +144,7 @@ describe('SpanFilters', () => {
expect(screen.getByText('ProcessKey1')).toBeInTheDocument();
expect(screen.getByText('LogKey0')).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 { 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 { Trace } from '../../types';
import NextPrevResult from '../SearchBar/NextPrevResult';
@ -298,7 +299,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
return (
<div className={styles.container}>
<Collapse label={collapseLabel} collapsible={true} isOpen={showSpanFilters} onToggle={setShowSpanFilters}>
<InlineFieldRow>
<InlineFieldRow className={styles.flexContainer}>
<InlineField label="Service Name" labelWidth={16}>
<HorizontalGroup spacing={'xs'}>
<Select
@ -318,6 +319,15 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
/>
</HorizontalGroup>
</InlineField>
<SearchBarInput
onChange={(v) => {
setSpanFiltersSearch({ ...search, query: v });
if (v === '') {
setShowSpanFilterMatchesOnly(false);
}
}}
value={search.query || ''}
/>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Span Name" labelWidth={16}>
@ -479,6 +489,7 @@ SpanFilters.displayName = 'SpanFilters';
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css`
label: SpanFilters;
margin: 0.5em 0 -${theme.spacing(1)} 0;
z-index: 5;
@ -493,6 +504,10 @@ const getStyles = (theme: GrafanaTheme2) => {
margin: -2px 0 0 10px;
}
`,
flexContainer: css({
display: 'flex',
justifyContent: 'space-between',
}),
addTag: css`
margin: 0 0 0 10px;
`,

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

@ -465,6 +465,11 @@ describe('filterSpans', () => {
spans
)
).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
@ -541,6 +546,38 @@ describe('filterSpans', () => {
)
).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
expect(
filterSpans(

@ -44,9 +44,59 @@ export function filterSpans(searchProps: SearchProps, spans: TraceSpan[] | TNil)
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;
}
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[]) => {
// remove empty/default tags
tags = tags.filter((tag) => {

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

Loading…
Cancel
Save