Tracing: Add services, depth to span filters metadata (#71084)

* Add services, depth to span filters metadata

* Add default metadata to method
pull/71631/head
Joey 2 years ago committed by GitHub
parent 9e999b455b
commit 76224c88bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NewTracePageSearchBar.test.tsx
  2. 7
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NewTracePageSearchBar.tsx
  3. 23
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.test.tsx
  4. 107
      public/app/features/explore/TraceView/components/TracePageHeader/SearchBar/NextPrevResult.tsx
  5. 4
      public/app/features/explore/TraceView/components/TracePageHeader/SpanFilters/SpanFilters.tsx
  6. 62
      public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx

@ -16,12 +16,14 @@ import { render, screen } from '@testing-library/react';
import React from 'react';
import { defaultFilters } from '../../../useSearch';
import { trace } from '../TracePageHeader.test';
import NewTracePageSearchBar from './NewTracePageSearchBar';
describe('<NewTracePageSearchBar>', () => {
const NewTracePageSearchBarWithProps = (props: { matches: string[] | undefined }) => {
const searchBarProps = {
trace: trace,
search: defaultFilters,
spanFilterMatches: props.matches ? new Set(props.matches) : undefined,
showSpanFilterMatchesOnly: false,

@ -20,11 +20,13 @@ import { Button, Switch, useStyles2 } from '@grafana/ui';
import { getButtonStyles } from '@grafana/ui/src/components/Button';
import { SearchProps } from '../../../useSearch';
import { Trace } from '../../types';
import { convertTimeFilter } from '../../utils/filter-spans';
import NextPrevResult from './NextPrevResult';
export type TracePageSearchBarProps = {
trace: Trace;
search: SearchProps;
spanFilterMatches: Set<string> | undefined;
showSpanFilterMatchesOnly: boolean;
@ -33,13 +35,13 @@ export type TracePageSearchBarProps = {
setFocusedSpanIndexForSearch: Dispatch<SetStateAction<number>>;
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
datasourceType: string;
totalSpans: number;
clear: () => void;
showSpanFilters: boolean;
};
export default memo(function NewTracePageSearchBar(props: TracePageSearchBarProps) {
const {
trace,
search,
spanFilterMatches,
showSpanFilterMatchesOnly,
@ -48,7 +50,6 @@ export default memo(function NewTracePageSearchBar(props: TracePageSearchBarProp
setFocusedSpanIndexForSearch,
setFocusedSpanIdForSearch,
datasourceType,
totalSpans,
clear,
showSpanFilters,
} = props;
@ -100,12 +101,12 @@ export default memo(function NewTracePageSearchBar(props: TracePageSearchBarProp
</div>
<div className={styles.nextPrevResult}>
<NextPrevResult
trace={trace}
spanFilterMatches={spanFilterMatches}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
focusedSpanIndexForSearch={focusedSpanIndexForSearch}
setFocusedSpanIndexForSearch={setFocusedSpanIndexForSearch}
datasourceType={datasourceType}
totalSpans={totalSpans}
showSpanFilters={showSpanFilters}
/>
</div>

@ -19,6 +19,7 @@ import React, { useState } from 'react';
import { createTheme } from '@grafana/data';
import { defaultFilters } from '../../../useSearch';
import { trace } from '../TracePageHeader.test';
import NextPrevResult, { getStyles } from './NextPrevResult';
@ -37,6 +38,7 @@ describe('<NextPrevResult>', () => {
const NextPrevResultWithProps = (props: { matches: string[] | undefined }) => {
const [focusedSpanIndexForSearch, setFocusedSpanIndexForSearch] = useState(-1);
const searchBarProps = {
trace: trace,
search: defaultFilters,
spanFilterMatches: props.matches ? new Set(props.matches) : undefined,
showSpanFilterMatchesOnly: false,
@ -46,7 +48,6 @@ describe('<NextPrevResult>', () => {
setFocusedSpanIndexForSearch: setFocusedSpanIndexForSearch,
datasourceType: '',
clear: jest.fn(),
totalSpans: 100,
showSpanFilters: true,
};
@ -70,11 +71,11 @@ describe('<NextPrevResult>', () => {
it('renders total spans', async () => {
render(<NextPrevResultWithProps matches={undefined} />);
expect(screen.getByText('100 spans')).toBeDefined();
expect(screen.getByText('3 spans')).toBeDefined();
});
it('renders buttons that can be used to search if filters added', () => {
render(<NextPrevResultWithProps matches={['2ed38015486087ca']} />);
render(<NextPrevResultWithProps matches={['264afda25df92413']} />);
const nextResButton = screen.queryByRole('button', { name: 'Next result button' });
const prevResButton = screen.queryByRole('button', { name: 'Prev result button' });
expect(nextResButton).toBeInTheDocument();
@ -85,7 +86,7 @@ describe('<NextPrevResult>', () => {
});
it('renders correctly when moving through matches', async () => {
render(<NextPrevResultWithProps matches={['1ed38015486087ca', '2ed38015486087ca', '3ed38015486087ca']} />);
render(<NextPrevResultWithProps matches={['264afda25df92413', '364afda25df92413', '464afda25df92413']} />);
const nextResButton = screen.queryByRole('button', { name: 'Next result button' });
const prevResButton = screen.queryByRole('button', { name: 'Prev result button' });
expect(screen.getByText('3 matches')).toBeDefined();
@ -106,7 +107,7 @@ describe('<NextPrevResult>', () => {
it('renders correctly when there are no matches i.e. too many filters added', async () => {
const { container } = render(<NextPrevResultWithProps matches={[]} />);
const theme = createTheme();
const tooltip = container.querySelector('.' + getStyles(theme, true).matchesTooltip);
const tooltip = container.querySelector('.' + getStyles(theme, true).tooltip);
expect(screen.getByText('0 matches')).toBeDefined();
userEvent.hover(tooltip!);
jest.advanceTimersByTime(1000);
@ -114,4 +115,16 @@ describe('<NextPrevResult>', () => {
expect(screen.getByText(/0 span matches for the filters selected/)).toBeDefined();
});
});
it('renders services, depth correctly', async () => {
const { container } = render(<NextPrevResultWithProps matches={['264afda25df92413', '364afda25df92413']} />);
const theme = createTheme();
const tooltip = container.querySelector('.' + getStyles(theme, true).tooltip);
userEvent.hover(tooltip!);
jest.advanceTimersByTime(1000);
await waitFor(() => {
expect(screen.getByText(/Services: 2\/3/)).toBeDefined();
expect(screen.getByText(/Depth: 1\/1/)).toBeDefined();
});
});
});

@ -13,31 +13,34 @@
// limitations under the License.
import { css, cx } from '@emotion/css';
import React, { memo, Dispatch, SetStateAction, useEffect } from 'react';
import { get, maxBy, values } from 'lodash';
import React, { memo, Dispatch, SetStateAction, useEffect, useCallback } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { Icon, Tooltip, useTheme2 } from '@grafana/ui';
import { Icon, PopoverContent, Tooltip, useTheme2 } from '@grafana/ui';
import { getButtonStyles } from '@grafana/ui/src/components/Button';
import { Trace } from '../../types';
export type NextPrevResultProps = {
trace: Trace;
spanFilterMatches: Set<string> | undefined;
setFocusedSpanIdForSearch: Dispatch<SetStateAction<string>>;
focusedSpanIndexForSearch: number;
setFocusedSpanIndexForSearch: Dispatch<SetStateAction<number>>;
datasourceType: string;
totalSpans: number;
showSpanFilters: boolean;
};
export default memo(function NextPrevResult(props: NextPrevResultProps) {
const {
trace,
spanFilterMatches,
setFocusedSpanIdForSearch,
focusedSpanIndexForSearch,
setFocusedSpanIndexForSearch,
datasourceType,
totalSpans,
showSpanFilters,
} = props;
const styles = getStyles(useTheme2(), showSpanFilters);
@ -109,30 +112,88 @@ export default memo(function NextPrevResult(props: NextPrevResultProps) {
};
const buttonEnabled = (spanFilterMatches && spanFilterMatches?.size > 0) ?? false;
const amountText = spanFilterMatches?.size === 1 ? 'match' : 'matches';
const matches =
spanFilterMatches?.size === 0 ? (
<>
<span>0 matches</span>
<Tooltip
content="There are 0 span matches for the filters selected. Please try removing some of the selected filters."
placement="left"
>
<span className={styles.matchesTooltip}>
<Icon name="info-circle" size="lg" />
const buttonClass = buttonEnabled ? styles.button : cx(styles.button, styles.buttonDisabled);
const getTooltip = useCallback(
(content: PopoverContent) => {
return (
<Tooltip content={content} placement="top">
<span className={styles.tooltip}>
<Icon name="info-circle" size="md" />
</span>
</Tooltip>
);
},
[styles.tooltip]
);
const getMatchesMetadata = useCallback(
(depth: number, services: number) => {
const matchedServices: string[] = [];
const matchedDepth: number[] = [];
let metadata = (
<>
<span>{`${trace.spans.length} spans`}</span>
{getTooltip(
<>
<div>Services: {services}</div>
<div>Depth: {depth}</div>
</>
)}
</>
) : focusedSpanIndexForSearch !== -1 ? (
`${focusedSpanIndexForSearch + 1}/${spanFilterMatches?.size} ${amountText}`
) : (
`${spanFilterMatches?.size} ${amountText}`
);
const buttonClass = buttonEnabled ? styles.button : cx(styles.button, styles.buttonDisabled);
if (spanFilterMatches) {
spanFilterMatches.forEach((spanID) => {
matchedServices.push(trace.processes[spanID].serviceName);
matchedDepth.push(trace.spans.find((span) => span.spanID === spanID)?.depth || 0);
});
if (spanFilterMatches.size === 0) {
metadata = (
<>
<span>0 matches</span>
{getTooltip(
'There are 0 span matches for the filters selected. Please try removing some of the selected filters.'
)}
</>
);
} else {
const type = spanFilterMatches.size === 1 ? 'match' : 'matches';
const text =
focusedSpanIndexForSearch !== -1
? `${focusedSpanIndexForSearch + 1}/${spanFilterMatches.size} ${type}`
: `${spanFilterMatches.size} ${type}`;
metadata = (
<>
<span>{text}</span>
{getTooltip(
<>
<div>
Services: {new Set(matchedServices).size}/{services}
</div>
<div>
Depth: {new Set(matchedDepth).size}/{depth}
</div>
</>
)}
</>
);
}
}
return metadata;
},
[focusedSpanIndexForSearch, getTooltip, spanFilterMatches, trace.processes, trace.spans]
);
const services = new Set(values(trace.processes).map((p) => p.serviceName)).size;
const depth = get(maxBy(trace.spans, 'depth'), 'depth', 0) + 1;
return (
<>
<span className={styles.matches}>{spanFilterMatches ? matches : `${totalSpans} spans`}</span>
<span className={styles.matches}>{getMatchesMetadata(depth, services)}</span>
<div className={buttonEnabled ? styles.buttons : cx(styles.buttons, styles.buttonsDisabled)}>
<div
aria-label="Prev result button"
@ -186,9 +247,9 @@ export const getStyles = (theme: GrafanaTheme2, showSpanFilters: boolean) => {
matches: css`
margin-right: ${theme.spacing(2)};
`,
matchesTooltip: css`
tooltip: css`
color: #aaa;
margin: -2px 0 0 10px;
margin: 0 0 0 5px;
`,
};
};

@ -277,12 +277,12 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
{!showSpanFilters && (
<div className={styles.nextPrevResult}>
<NextPrevResult
trace={trace}
spanFilterMatches={spanFilterMatches}
setFocusedSpanIdForSearch={setFocusedSpanIdForSearch}
focusedSpanIndexForSearch={focusedSpanIndexForSearch}
setFocusedSpanIndexForSearch={setFocusedSpanIndexForSearch}
datasourceType={datasourceType}
totalSpans={trace.spans.length}
showSpanFilters={showSpanFilters}
/>
</div>
@ -448,7 +448,7 @@ export const SpanFilters = memo((props: SpanFilterProps) => {
</InlineFieldRow>
<NewTracePageSearchBar
totalSpans={trace.spans.length}
trace={trace}
search={search}
spanFilterMatches={spanFilterMatches}
showSpanFilterMatchesOnly={showSpanFilterMatchesOnly}

@ -24,7 +24,7 @@ export const trace = {
spans: [
{
traceID: '164afda25df92413',
spanID: '164afda25df92413',
spanID: '264afda25df92413',
operationName: 'HTTP Client',
serviceName: 'serviceA',
subsidiarilyReferencedBy: [],
@ -33,7 +33,7 @@ export const trace = {
logs: [],
references: [],
tags: [],
processID: '164afda25df92413',
processID: '264afda25df92413',
flags: 0,
process: {
serviceName: 'lb',
@ -47,7 +47,7 @@ export const trace = {
},
{
traceID: '164afda25df92413',
spanID: '164afda25df92413',
spanID: '364afda25df92413',
operationName: 'HTTP Client',
serviceName: 'serviceB',
subsidiarilyReferencedBy: [],
@ -72,7 +72,7 @@ export const trace = {
value: `200`,
},
],
processID: '164afda25df92413',
processID: '364afda25df92413',
flags: 0,
process: {
serviceName: 'lb',
@ -84,10 +84,62 @@ export const trace = {
childSpanCount: 0,
warnings: [],
},
{
traceID: '164afda25df92413',
spanID: '464afda25df92413',
operationName: 'HTTP Server',
serviceName: 'serviceC',
subsidiarilyReferencedBy: [],
startTime: 1675602037286989,
duration: 5685,
logs: [],
references: [],
tags: [
{
key: 'http.url',
type: 'String',
value: `/v2/gamma/792edh2w897y2huehd2h89`,
},
{
key: 'http.method',
type: 'String',
value: `POST`,
},
{
key: 'http.status_code',
type: 'String',
value: `200`,
},
],
processID: '464afda25df92413',
flags: 0,
process: {
serviceName: 'db',
tags: [],
},
relativeStartTime: 0,
depth: 0,
hasChildren: false,
childSpanCount: 0,
warnings: [],
},
],
traceID: '8bb35a31-eb64-512d-aaed-ddd61887bb2b',
traceName: 'serviceA: GET',
processes: {},
processes: {
'264afda25df92413': {
serviceName: 'serviceA',
tags: [],
},
'364afda25df92413': {
serviceName: 'serviceB',
tags: [],
},
'464afda25df92413': {
serviceName: 'serviceC',
tags: [],
},
},
duration: 2355515,
startTime: 1675605056289000,
endTime: 1675605058644515,

Loading…
Cancel
Save