Skip to content

Commit 6a39ac7

Browse files
Explore: Download traces as JSON in Explore Inspector (grafana#38614)
* Transform dataframe to jaeger format * Transform dataframe to Zipkin format * Add endpoint type and shared to Zipkin * Transform dataframe to OTLP format * Add data tab tests and note in inspector docs * Remove comments and logs * Resolve typescript strict errors * Update docs/sources/explore/explore-inspector.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs * Improve OTLP conversion to include service info and additional tags Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
1 parent 3bb2ee9 commit 6a39ac7

16 files changed

Lines changed: 702 additions & 11 deletions

‎docs/sources/explore/explore-inspector.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ Grafana generates a TXT file in your default browser download location. You can
5757
1. Inspect the log query results as described above.
5858
1. Click **Download logs**.
5959

60+
### Download trace results
61+
62+
Based on the data source type, Grafana generates a JSON file for the trace results in one of the supported formats: Jaeger, Zipkin, or OTLP formats.
63+
64+
1. Open the inspector.
65+
1. Inspect the trace query results [as described above](#inspect-raw-query-results).
66+
1. Click **Download traces**.
67+
6068
### Inspect query performance
6169

6270
The Stats tab displays statistics that tell you how long your query takes, how many queries you send, and the number of rows returned. This information can help you troubleshoot your queries, especially if any of the numbers are unexpectedly high or low.

‎public/app/features/inspector/InspectDataTab.test.tsx‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,63 @@ describe('InspectDataTab', () => {
8383
render(<InspectDataTab {...createProps()} />);
8484
expect(screen.queryByText(/Download logs/i)).not.toBeInTheDocument();
8585
});
86+
it('should show download traces button if traces data', () => {
87+
const dataWithtraces = ([
88+
{
89+
name: 'Data frame with traces',
90+
fields: [
91+
{ name: 'traceID', values: ['3fa414edcef6ad90', '3fa414edcef6ad90'] },
92+
{ name: 'spanID', values: ['3fa414edcef6ad90', '0f5c1808567e4403'] },
93+
{ name: 'parentSpanID', values: [undefined, '3fa414edcef6ad90'] },
94+
{ name: 'operationName', values: ['HTTP GET - api_traces_traceid', '/tempopb.Querier/FindTraceByID'] },
95+
{ name: 'serviceName', values: ['tempo-querier', 'tempo-querier'] },
96+
{
97+
name: 'serviceTags',
98+
values: [
99+
[
100+
{ key: 'cluster', type: 'string', value: 'ops-tools1' },
101+
{ key: 'container', type: 'string', value: 'tempo-query' },
102+
],
103+
[
104+
{ key: 'cluster', type: 'string', value: 'ops-tools1' },
105+
{ key: 'container', type: 'string', value: 'tempo-query' },
106+
],
107+
],
108+
},
109+
{ name: 'startTime', values: [1605873894680.409, 1605873894680.587] },
110+
{ name: 'duration', values: [1049.141, 1.847] },
111+
{ name: 'logs', values: [[], []] },
112+
{
113+
name: 'tags',
114+
values: [
115+
[
116+
{ key: 'sampler.type', type: 'string', value: 'probabilistic' },
117+
{ key: 'sampler.param', type: 'float64', value: 1 },
118+
],
119+
[
120+
{ key: 'component', type: 'string', value: 'gRPC' },
121+
{ key: 'span.kind', type: 'string', value: 'client' },
122+
],
123+
],
124+
},
125+
{ name: 'warnings', values: [undefined, undefined] },
126+
{ name: 'stackTraces', values: [undefined, undefined] },
127+
],
128+
length: 2,
129+
meta: {
130+
preferredVisualisationType: 'trace',
131+
custom: {
132+
traceFormat: 'jaeger',
133+
},
134+
},
135+
},
136+
] as unknown) as DataFrame[];
137+
render(<InspectDataTab {...createProps({ data: dataWithtraces })} />);
138+
expect(screen.getByText(/Download traces/i)).toBeInTheDocument();
139+
});
140+
it('should not show download traces button if no traces data', () => {
141+
render(<InspectDataTab {...createProps()} />);
142+
expect(screen.queryByText(/Download traces/i)).not.toBeInTheDocument();
143+
});
86144
});
87145
});

‎public/app/features/inspector/InspectDataTab.tsx‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
DataTransformerID,
99
dateTimeFormat,
1010
dateTimeFormatISO,
11+
MutableDataFrame,
1112
SelectableValue,
1213
toCSV,
1314
transformDataFrame,
@@ -22,6 +23,9 @@ import { css } from '@emotion/css';
2223
import { GetDataOptions } from 'app/features/query/state/PanelQueryRunner';
2324
import { PanelModel } from 'app/features/dashboard/state';
2425
import { dataFrameToLogsModel } from 'app/core/logs_model';
26+
import { transformToJaeger } from 'app/plugins/datasource/jaeger/responseTransform';
27+
import { transformToZipkin } from 'app/plugins/datasource/zipkin/utils/transforms';
28+
import { transformToOTLP } from 'app/plugins/datasource/tempo/resultTransformer';
2529

2630
interface Props {
2731
isLoading: boolean;
@@ -122,6 +126,48 @@ export class InspectDataTab extends PureComponent<Props, State> {
122126
saveAs(blob, fileName);
123127
};
124128

129+
exportTracesAsJson = () => {
130+
const { data, panel } = this.props;
131+
if (!data) {
132+
return;
133+
}
134+
135+
for (const df of data) {
136+
// Only export traces
137+
if (df.meta?.preferredVisualisationType !== 'trace') {
138+
continue;
139+
}
140+
141+
switch (df.meta?.custom?.traceFormat) {
142+
case 'jaeger': {
143+
let res = transformToJaeger(new MutableDataFrame(df));
144+
this.saveTraceJson(res, panel);
145+
break;
146+
}
147+
case 'zipkin': {
148+
let res = transformToZipkin(new MutableDataFrame(df));
149+
this.saveTraceJson(res, panel);
150+
break;
151+
}
152+
case 'otlp':
153+
default: {
154+
let res = transformToOTLP(new MutableDataFrame(df));
155+
this.saveTraceJson(res, panel);
156+
break;
157+
}
158+
}
159+
}
160+
};
161+
162+
saveTraceJson = (json: any, panel?: PanelModel) => {
163+
const blob = new Blob([JSON.stringify(json)], {
164+
type: 'application/json',
165+
});
166+
const displayTitle = panel ? panel.getDisplayTitle() : 'Explore';
167+
const fileName = `${displayTitle}-traces-${dateTimeFormat(new Date())}.json`;
168+
saveAs(blob, fileName);
169+
};
170+
125171
onDataFrameChange = (item: SelectableValue<DataTransformerID | number>) => {
126172
this.setState({
127173
transformId:
@@ -180,6 +226,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
180226
const index = !dataFrames[dataFrameIndex] ? 0 : dataFrameIndex;
181227
const dataFrame = dataFrames[index];
182228
const hasLogs = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'logs');
229+
const hasTraces = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'trace');
183230

184231
return (
185232
<div className={styles.dataTabContent} aria-label={selectors.components.PanelInspector.Data.content}>
@@ -218,6 +265,18 @@ export class InspectDataTab extends PureComponent<Props, State> {
218265
Download logs
219266
</Button>
220267
)}
268+
{hasTraces && (
269+
<Button
270+
variant="primary"
271+
onClick={this.exportTracesAsJson}
272+
className={css`
273+
margin-bottom: 10px;
274+
margin-left: 10px;
275+
`}
276+
>
277+
Download traces
278+
</Button>
279+
)}
221280
</div>
222281
<Container grow={1}>
223282
<AutoSizer>

‎public/app/plugins/datasource/jaeger/datasource.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,8 @@ const emptyTraceDataFrame = new MutableDataFrame({
171171
],
172172
meta: {
173173
preferredVisualisationType: 'trace',
174+
custom: {
175+
traceFormat: 'jaeger',
176+
},
174177
},
175178
});
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import { createTraceFrame } from './responseTransform';
1+
import { createTraceFrame, transformToJaeger } from './responseTransform';
22
import { testResponse, testResponseDataFrameFields } from './testResponse';
3+
import { MutableDataFrame } from '@grafana/data';
34

45
describe('createTraceFrame', () => {
56
it('creates data frame from jaeger response', () => {
67
const dataFrame = createTraceFrame(testResponse);
78
expect(dataFrame.fields).toMatchObject(testResponseDataFrameFields);
89
});
10+
11+
it('transforms to jaeger format from data frame', () => {
12+
const dataFrame = createTraceFrame(testResponse);
13+
const response = transformToJaeger(new MutableDataFrame(dataFrame));
14+
expect(response).toMatchObject({ data: [testResponse] });
15+
});
916
});

‎public/app/plugins/datasource/jaeger/responseTransform.ts‎

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import { DataFrame, DataSourceInstanceSettings, FieldType, MutableDataFrame, TraceSpanRow } from '@grafana/data';
1+
import {
2+
DataFrame,
3+
DataSourceInstanceSettings,
4+
FieldType,
5+
MutableDataFrame,
6+
TraceLog,
7+
TraceSpanRow,
8+
} from '@grafana/data';
29
import { transformTraceData } from '@jaegertracing/jaeger-ui-components';
3-
import { Span, TraceProcess, TraceResponse } from './types';
10+
import { JaegerResponse, Span, TraceProcess, TraceResponse } from './types';
411

512
export function createTraceFrame(data: TraceResponse): DataFrame {
613
const spans = data.spans.map((s) => toSpanRow(s, data.processes));
@@ -22,6 +29,9 @@ export function createTraceFrame(data: TraceResponse): DataFrame {
2229
],
2330
meta: {
2431
preferredVisualisationType: 'trace',
32+
custom: {
33+
traceFormat: 'jaeger',
34+
},
2535
},
2636
});
2737

@@ -107,3 +117,62 @@ function transformToTraceData(data: TraceResponse) {
107117
traceName: traceData.traceName,
108118
};
109119
}
120+
121+
export function transformToJaeger(data: MutableDataFrame): JaegerResponse {
122+
let traceResponse: TraceResponse = {
123+
traceID: '',
124+
spans: [],
125+
processes: {},
126+
warnings: null,
127+
};
128+
let processes: string[] = [];
129+
130+
for (let i = 0; i < data.length; i++) {
131+
const span = data.get(i);
132+
133+
// Set traceID
134+
if (!traceResponse.traceID) {
135+
traceResponse.traceID = span.traceID;
136+
}
137+
138+
// Create process if doesn't exist
139+
if (!processes.find((p) => p === span.serviceName)) {
140+
processes.push(span.serviceName);
141+
traceResponse.processes[`p${processes.length}`] = {
142+
serviceName: span.serviceName,
143+
tags: span.serviceTags,
144+
};
145+
}
146+
147+
// Create span
148+
traceResponse.spans.push({
149+
traceID: span.traceID,
150+
spanID: span.spanID,
151+
duration: span.duration * 1000,
152+
references: span.parentSpanID
153+
? [
154+
{
155+
refType: 'CHILD_OF',
156+
spanID: span.parentSpanID,
157+
traceID: span.traceID,
158+
},
159+
]
160+
: [],
161+
flags: 0,
162+
logs: span.logs.map((l: TraceLog) => ({
163+
...l,
164+
timestamp: l.timestamp * 1000,
165+
})),
166+
operationName: span.operationName,
167+
processID:
168+
Object.keys(traceResponse.processes).find(
169+
(key) => traceResponse.processes[key].serviceName === span.serviceName
170+
) || '',
171+
startTime: span.startTime * 1000,
172+
tags: span.tags,
173+
warnings: span.warnings ? span.warnings : null,
174+
});
175+
}
176+
177+
return { data: [traceResponse], total: 0, limit: 0, offset: 0, errors: null };
178+
}

‎public/app/plugins/datasource/jaeger/types.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,11 @@ export type JaegerQuery = {
6464
} & DataQuery;
6565

6666
export type JaegerQueryType = 'search' | 'upload';
67+
68+
export type JaegerResponse = {
69+
data: TraceResponse[];
70+
total: number;
71+
limit: number;
72+
offset: number;
73+
errors?: string[] | null;
74+
};

‎public/app/plugins/datasource/tempo/datasource.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
8484
.map((field) => field.matcherRegex) || [];
8585
if (!traceLinkMatcher || traceLinkMatcher.length === 0) {
8686
return throwError(
87-
'No Loki datasource configured for search. Set up Derived Fields for traces in a Loki datasource settings and link it to this Tempo datasource.'
87+
() =>
88+
new Error(
89+
'No Loki datasource configured for search. Set up Derived Fields for traces in a Loki datasource settings and link it to this Tempo datasource.'
90+
)
8891
);
8992
} else {
9093
return (linkedDatasource.query(linkedRequest) as Observable<DataQueryResponse>).pipe(

‎public/app/plugins/datasource/tempo/resultTransformer.test.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FieldType, MutableDataFrame } from '@grafana/data';
2-
import { createTableFrame } from './resultTransformer';
2+
import { createTableFrame, transformToOTLP } from './resultTransformer';
3+
import { otlpDataFrame, otlpResponse } from './testResponse';
34

45
describe('transformTraceList()', () => {
56
const lokiDataFrame = new MutableDataFrame({
@@ -35,3 +36,10 @@ describe('transformTraceList()', () => {
3536
expect(frame.fields[1].values.get(1)).toBe('as');
3637
});
3738
});
39+
40+
describe('transformToOTLP()', () => {
41+
test('transforms dataframe to OTLP format', () => {
42+
const otlp = transformToOTLP(otlpDataFrame);
43+
expect(otlp).toMatchObject(otlpResponse);
44+
});
45+
});

0 commit comments

Comments
 (0)