Tracing: Add services, depth to span filters metadata (#71084)
* Add services, depth to span filters metadata * Add default metadata to method
This commit is contained in:
+2
@@ -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,
|
||||
|
||||
+4
-3
@@ -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>
|
||||
|
||||
+18
-5
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+86
-25
@@ -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>
|
||||
</>
|
||||
) : focusedSpanIndexForSearch !== -1 ? (
|
||||
`${focusedSpanIndexForSearch + 1}/${spanFilterMatches?.size} ${amountText}`
|
||||
) : (
|
||||
`${spanFilterMatches?.size} ${amountText}`
|
||||
);
|
||||
const buttonClass = buttonEnabled ? styles.button : cx(styles.button, styles.buttonDisabled);
|
||||
);
|
||||
},
|
||||
[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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
+2
-2
@@ -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}
|
||||
|
||||
+57
-5
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user