mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tracing: Specify type of the data frame that is expected for TraceView (#31465)
* Use dataframe API for jeager * Move types around * Fix imports * Simplify the data frame type * Add comment * Move the transform to separate file * Fix logs timestamp * Add/update tests for trace view * Fix lint * Add test to compare old and new format rendering * Fix test imports * Update data source tests
This commit is contained in:
@@ -17,7 +17,6 @@ import {
|
||||
RawTimeRange,
|
||||
TimeZone,
|
||||
LogsModel,
|
||||
TraceViewData,
|
||||
DataFrame,
|
||||
} from '@grafana/data';
|
||||
|
||||
@@ -292,15 +291,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
const dataFrames = queryResponse.series.filter((series) => series.meta?.preferredVisualisationType === 'trace');
|
||||
|
||||
return (
|
||||
// We expect only one trace at the moment to be in the dataframe
|
||||
// If there is no data (like 404) we show a separate error so no need to show anything here
|
||||
dataFrames[0] && (
|
||||
<TraceView
|
||||
exploreId={exploreId}
|
||||
trace={dataFrames[0].fields[0].values.get(0) as TraceViewData | undefined}
|
||||
splitOpenFn={splitOpen}
|
||||
/>
|
||||
)
|
||||
dataFrames.length && <TraceView exploreId={exploreId} dataFrames={dataFrames} splitOpenFn={splitOpen} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import { render, prettyDOM } from '@testing-library/react';
|
||||
import { TraceView } from './TraceView';
|
||||
import { TracePageHeader, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceSpanData, TraceData } from '@grafana/data';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { ExploreId } from 'app/types';
|
||||
import { TraceData, TraceSpanData } from '@jaegertracing/jaeger-ui-components/src/types/trace';
|
||||
import { MutableDataFrame } from '@grafana/data';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useSelector: jest.fn(() => undefined),
|
||||
}));
|
||||
|
||||
function renderTraceView() {
|
||||
const wrapper = shallow(<TraceView exploreId={ExploreId.left} trace={response} splitOpenFn={() => {}} />);
|
||||
const wrapper = shallow(<TraceView exploreId={ExploreId.left} dataFrames={[frameOld]} splitOpenFn={() => {}} />);
|
||||
return {
|
||||
timeline: wrapper.find(TraceTimelineViewer),
|
||||
header: wrapper.find(TracePageHeader),
|
||||
wrapper,
|
||||
};
|
||||
}
|
||||
|
||||
function renderTraceViewNew() {
|
||||
const wrapper = shallow(<TraceView exploreId={ExploreId.left} dataFrames={[frameNew]} splitOpenFn={() => {}} />);
|
||||
return {
|
||||
timeline: wrapper.find(TraceTimelineViewer),
|
||||
header: wrapper.find(TracePageHeader),
|
||||
@@ -35,15 +45,30 @@ describe('TraceView', () => {
|
||||
expect(header).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders TraceTimelineViewer with new format', () => {
|
||||
const { timeline, header } = renderTraceViewNew();
|
||||
expect(timeline).toHaveLength(1);
|
||||
expect(header).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders renders the same for old and new format', () => {
|
||||
const { baseElement } = render(
|
||||
<TraceView exploreId={ExploreId.left} dataFrames={[frameNew]} splitOpenFn={() => {}} />
|
||||
);
|
||||
const { baseElement: baseElementOld } = render(
|
||||
<TraceView exploreId={ExploreId.left} dataFrames={[frameOld]} splitOpenFn={() => {}} />
|
||||
);
|
||||
expect(prettyDOM(baseElement)).toEqual(prettyDOM(baseElementOld));
|
||||
});
|
||||
|
||||
it('does not render anything on missing trace', () => {
|
||||
// Simulating Explore's access to empty response data
|
||||
const trace = [][0];
|
||||
const { container } = render(<TraceView exploreId={ExploreId.left} trace={trace} splitOpenFn={() => {}} />);
|
||||
const { container } = render(<TraceView exploreId={ExploreId.left} dataFrames={[]} splitOpenFn={() => {}} />);
|
||||
expect(container.hasChildNodes()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('toggles detailState', () => {
|
||||
let { timeline, wrapper } = renderTraceView();
|
||||
let { timeline, wrapper } = renderTraceViewNew();
|
||||
expect(timeline.props().traceTimeline.detailStates.size).toBe(0);
|
||||
|
||||
timeline.props().detailToggle('1');
|
||||
@@ -57,7 +82,7 @@ describe('TraceView', () => {
|
||||
});
|
||||
|
||||
it('toggles children visibility', () => {
|
||||
let { timeline, wrapper } = renderTraceView();
|
||||
let { timeline, wrapper } = renderTraceViewNew();
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
|
||||
timeline.props().childrenToggle('1');
|
||||
@@ -71,7 +96,7 @@ describe('TraceView', () => {
|
||||
});
|
||||
|
||||
it('toggles adds and removes hover indent guides', () => {
|
||||
let { timeline, wrapper } = renderTraceView();
|
||||
let { timeline, wrapper } = renderTraceViewNew();
|
||||
expect(timeline.props().traceTimeline.hoverIndentGuideIds.size).toBe(0);
|
||||
|
||||
timeline.props().addHoverIndentGuideId('1');
|
||||
@@ -85,7 +110,7 @@ describe('TraceView', () => {
|
||||
});
|
||||
|
||||
it('toggles collapses and expands one level of spans', () => {
|
||||
let { timeline, wrapper } = renderTraceView();
|
||||
let { timeline, wrapper } = renderTraceViewNew();
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
const spans = timeline.props().trace.spans;
|
||||
|
||||
@@ -100,7 +125,7 @@ describe('TraceView', () => {
|
||||
});
|
||||
|
||||
it('toggles collapses and expands all levels', () => {
|
||||
let { timeline, wrapper } = renderTraceView();
|
||||
let { timeline, wrapper } = renderTraceViewNew();
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
const spans = timeline.props().trace.spans;
|
||||
|
||||
@@ -116,7 +141,7 @@ describe('TraceView', () => {
|
||||
});
|
||||
|
||||
it('searches for spans', () => {
|
||||
let { wrapper, header } = renderTraceView();
|
||||
let { wrapper, header } = renderTraceViewNew();
|
||||
header.props().onSearchValueChange('HTTP POST - api_prom_push');
|
||||
|
||||
const timeline = wrapper.find(TraceTimelineViewer);
|
||||
@@ -124,7 +149,7 @@ describe('TraceView', () => {
|
||||
});
|
||||
|
||||
it('change viewRange', () => {
|
||||
let { header, timeline, wrapper } = renderTraceView();
|
||||
let { header, timeline, wrapper } = renderTraceViewNew();
|
||||
const defaultRange = { time: { current: [0, 1] } };
|
||||
expect(timeline.props().viewRange).toEqual(defaultRange);
|
||||
expect(header.props().viewRange).toEqual(defaultRange);
|
||||
@@ -237,3 +262,107 @@ const response: TraceData & { spans: TraceSpanData[] } = {
|
||||
},
|
||||
warnings: null as any,
|
||||
};
|
||||
|
||||
const frameOld = new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'trace',
|
||||
values: [response],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'trace',
|
||||
},
|
||||
});
|
||||
|
||||
const frameNew = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'traceID', values: ['1ed38015486087ca', '1ed38015486087ca', '1ed38015486087ca'] },
|
||||
{ name: 'spanID', values: ['1ed38015486087ca', '3fb050342773d333', '35118c298fc91f68'] },
|
||||
{ name: 'parentSpanID', values: [undefined, '1ed38015486087ca', '3fb050342773d333'] },
|
||||
{ name: 'operationName', values: ['HTTP POST - api_prom_push', '/logproto.Pusher/Push', '/logproto.Pusher/Push'] },
|
||||
{ name: 'serviceName', values: ['loki-all', 'loki-all', 'loki-all'] },
|
||||
{
|
||||
name: 'serviceTags',
|
||||
values: [
|
||||
[
|
||||
{ key: 'client-uuid', value: '2a59d08899ef6a8a' },
|
||||
{ key: 'hostname', value: '0080b530fae3' },
|
||||
{ key: 'ip', value: '172.18.0.6' },
|
||||
{ key: 'jaeger.version', value: 'Go-2.20.1' },
|
||||
],
|
||||
[
|
||||
{ key: 'client-uuid', value: '2a59d08899ef6a8a' },
|
||||
{ key: 'hostname', value: '0080b530fae3' },
|
||||
{ key: 'ip', value: '172.18.0.6' },
|
||||
{ key: 'jaeger.version', value: 'Go-2.20.1' },
|
||||
],
|
||||
[
|
||||
{ key: 'client-uuid', value: '2a59d08899ef6a8a' },
|
||||
{ key: 'hostname', value: '0080b530fae3' },
|
||||
{ key: 'ip', value: '172.18.0.6' },
|
||||
{ key: 'jaeger.version', value: 'Go-2.20.1' },
|
||||
],
|
||||
],
|
||||
},
|
||||
{ name: 'startTime', values: [1585244579835.187, 1585244579835.341, 1585244579836.04] },
|
||||
{ name: 'duration', values: [1.098, 0.921, 0.036] },
|
||||
{
|
||||
name: 'logs',
|
||||
values: [
|
||||
[
|
||||
{
|
||||
timestamp: 1585244579835.229,
|
||||
fields: [{ key: 'event', value: 'util.ParseProtoRequest[start reading]' }],
|
||||
},
|
||||
{
|
||||
timestamp: 1585244579835.241,
|
||||
fields: [
|
||||
{ key: 'event', value: 'util.ParseProtoRequest[decompress]' },
|
||||
{ key: 'size', value: 315 },
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamp: 1585244579835.245,
|
||||
fields: [
|
||||
{ key: 'event', value: 'util.ParseProtoRequest[unmarshal]' },
|
||||
{ key: 'size', value: 446 },
|
||||
],
|
||||
},
|
||||
],
|
||||
[],
|
||||
[],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
values: [
|
||||
[
|
||||
{ key: 'sampler.type', value: 'const' },
|
||||
{ key: 'sampler.param', value: true },
|
||||
{ key: 'span.kind', value: 'server' },
|
||||
{ key: 'http.method', value: 'POST' },
|
||||
{ key: 'http.url', value: '/api/prom/push' },
|
||||
{ key: 'component', value: 'net/http' },
|
||||
{ key: 'http.status_code', value: 204 },
|
||||
{ key: 'internal.span.format', value: 'proto' },
|
||||
],
|
||||
[
|
||||
{ key: 'span.kind', value: 'client' },
|
||||
{ key: 'component', value: 'gRPC' },
|
||||
{ key: 'internal.span.format', value: 'proto' },
|
||||
],
|
||||
[
|
||||
{ key: 'span.kind', value: 'server' },
|
||||
{ key: 'component', value: 'gRPC' },
|
||||
{ key: 'internal.span.format', value: 'proto' },
|
||||
],
|
||||
],
|
||||
},
|
||||
{ name: 'warnings', values: [undefined, undefined] },
|
||||
{ name: 'stackTraces', values: [undefined, undefined] },
|
||||
],
|
||||
meta: {
|
||||
preferredVisualisationType: 'trace',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,7 +3,13 @@ import {
|
||||
ThemeOptions,
|
||||
ThemeProvider,
|
||||
ThemeType,
|
||||
Trace,
|
||||
TraceKeyValuePair,
|
||||
TraceLink,
|
||||
TracePageHeader,
|
||||
TraceProcess,
|
||||
TraceResponse,
|
||||
TraceSpan,
|
||||
TraceTimelineViewer,
|
||||
transformTraceData,
|
||||
TTraceTimeline,
|
||||
@@ -16,7 +22,7 @@ import { useChildrenState } from './useChildrenState';
|
||||
import { useDetailState } from './useDetailState';
|
||||
import { useHoverIndentGuide } from './useHoverIndentGuide';
|
||||
import { colors, useTheme } from '@grafana/ui';
|
||||
import { TraceViewData, Trace, TraceSpan, TraceKeyValuePair, TraceLink } from '@grafana/data';
|
||||
import { DataFrame, DataFrameView, TraceSpanRow } from '@grafana/data';
|
||||
import { createSpanLinkFactory } from './createSpanLink';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { StoreState } from 'app/types';
|
||||
@@ -25,13 +31,13 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { TraceToLogsData } from 'app/core/components/TraceToLogsSettings';
|
||||
|
||||
type Props = {
|
||||
trace?: TraceViewData;
|
||||
dataFrames: DataFrame[];
|
||||
splitOpenFn: SplitOpen;
|
||||
exploreId: ExploreId;
|
||||
};
|
||||
|
||||
export function TraceView(props: Props) {
|
||||
if (!props.trace?.traceID) {
|
||||
if (!props.dataFrames.length) {
|
||||
return null;
|
||||
}
|
||||
const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState();
|
||||
@@ -58,7 +64,7 @@ export function TraceView(props: Props) {
|
||||
*/
|
||||
const [slim, setSlim] = useState(false);
|
||||
|
||||
const traceProp = useMemo(() => transformTraceData(props.trace), [props.trace]);
|
||||
const traceProp = useMemo(() => transformDataFrames(props.dataFrames), [props.dataFrames]);
|
||||
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
|
||||
const dataSourceName = useSelector((state: StoreState) => state.explore[props.exploreId]?.datasourceInstance?.name);
|
||||
const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(dataSourceName)?.jsonData as TraceToLogsData)
|
||||
@@ -167,3 +173,44 @@ export function TraceView(props: Props) {
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function transformDataFrames(frames: DataFrame[]): Trace | null {
|
||||
// At this point we only show single trace.
|
||||
const frame = frames[0];
|
||||
let data: TraceResponse =
|
||||
frame.fields.length === 1
|
||||
? // For backward compatibility when we sent whole json response in a single field/value
|
||||
frame.fields[0].values.get(0)
|
||||
: transformTraceDataFrame(frame);
|
||||
return transformTraceData(data);
|
||||
}
|
||||
|
||||
function transformTraceDataFrame(frame: DataFrame): TraceResponse {
|
||||
const view = new DataFrameView<TraceSpanRow>(frame);
|
||||
const processes: Record<string, TraceProcess> = {};
|
||||
for (let i = 0; i < view.length; i++) {
|
||||
const span = view.get(i);
|
||||
if (!processes[span.serviceName]) {
|
||||
processes[span.serviceName] = {
|
||||
serviceName: span.serviceName,
|
||||
tags: span.serviceTags,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
traceID: view.get(0).traceID,
|
||||
processes,
|
||||
spans: view.toArray().map((s) => {
|
||||
return {
|
||||
...s,
|
||||
duration: s.duration * 1000,
|
||||
startTime: s.startTime * 1000,
|
||||
processID: s.serviceName,
|
||||
flags: 0,
|
||||
references: s.parentSpanID ? [{ refType: 'CHILD_OF', spanID: s.parentSpanID, traceID: s.traceID }] : undefined,
|
||||
logs: s.logs?.map((l) => ({ ...l, timestamp: l.timestamp * 1000 })) || [],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataLink, dateTime, Field, mapInternalLinkToExplore, TimeRange, TraceSpan } from '@grafana/data';
|
||||
import { DataLink, dateTime, Field, mapInternalLinkToExplore, TimeRange } from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { Icon } from '@grafana/ui';
|
||||
import { SplitOpen } from 'app/types/explore';
|
||||
@@ -6,6 +6,7 @@ import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import React from 'react';
|
||||
import { LokiQuery } from '../../../plugins/datasource/loki/types';
|
||||
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
/**
|
||||
* This is a factory for the link creator. It returns the function mainly so it can return undefined in which case
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { useChildrenState } from './useChildrenState';
|
||||
import { TraceSpan } from '@grafana/data';
|
||||
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
describe('useChildrenState', () => {
|
||||
describe('childrenToggle', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { TraceSpan } from '@grafana/data';
|
||||
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
/**
|
||||
* Children state means whether spans are collapsed or not. Also provides some functions to manipulate that state.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { TraceLog } from '@grafana/data';
|
||||
import { useDetailState } from './useDetailState';
|
||||
import { TraceLog } from '@jaegertracing/jaeger-ui-components/src/types/trace';
|
||||
|
||||
describe('useDetailState', () => {
|
||||
it('toggles detail', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { DetailState } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceLog } from '@grafana/data';
|
||||
import { TraceLog } from '@jaegertracing/jaeger-ui-components/src/types/trace';
|
||||
|
||||
/**
|
||||
* Keeps state of the span detail. This means whether span details are open but also state of each detail subitem
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { useSearch } from './useSearch';
|
||||
import { TraceSpan } from '@grafana/data';
|
||||
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
describe('useSearch', () => {
|
||||
it('returns matching span IDs', async () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { filterSpans } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceSpan } from '@grafana/data';
|
||||
import { filterSpans, TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
|
||||
/**
|
||||
* Controls the state of search input that highlights spans if they match the search string.
|
||||
|
||||
Reference in New Issue
Block a user