Explore: Download and upload service graphs for Tempo (#50260)

* Download service graph from inspect data tab

* Upload service graph

* Fix tests
pull/50317/head
Connor Lindsey 3 years ago committed by GitHub
parent cbfac157fb
commit f9ddb8bf86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      docs/sources/datasources/tempo.md
  2. 26
      public/app/features/inspector/InspectDataTab.test.tsx
  3. 27
      public/app/features/inspector/InspectDataTab.tsx
  4. 19
      public/app/plugins/datasource/tempo/datasource.test.ts
  5. 14
      public/app/plugins/datasource/tempo/datasource.ts
  6. 315
      public/app/plugins/datasource/tempo/mockServiceGraph.json

@ -116,7 +116,9 @@ To query a particular trace, select the **TraceID** query type, and then put the
## Upload JSON trace file
You can upload a JSON file that contains a single trace to visualize it. If the file has multiple traces then the first trace is used for visualization.
You can upload a JSON file that contains a single trace or service graph to visualize it. If the file has multiple traces, the first trace is used for visualization.
You can download a trace or service graph through the inspector. Open the inspector, navigate to the 'Data' tab, and click 'Download traces' or 'Download service graph'.
Here is an example JSON:

@ -147,5 +147,31 @@ describe('InspectDataTab', () => {
render(<InspectDataTab {...createProps()} />);
expect(screen.queryByText(/Download traces/i)).not.toBeInTheDocument();
});
it('should show download service graph button', () => {
const sgFrames = [
{
name: 'Nodes',
fields: [],
meta: {
preferredVisualisationType: 'nodeGraph',
},
},
{
name: 'Edges',
fields: [],
meta: {
preferredVisualisationType: 'nodeGraph',
},
},
] as unknown as DataFrame[];
render(
<InspectDataTab
{...createProps({
data: sgFrames,
})}
/>
);
expect(screen.getByText(/Download service graph/i)).toBeInTheDocument();
});
});
});

@ -172,6 +172,20 @@ export class InspectDataTab extends PureComponent<Props, State> {
saveAs(blob, fileName);
};
exportServiceGraph = () => {
const { data, panel } = this.props;
if (!data) {
return;
}
const blob = new Blob([JSON.stringify(data)], {
type: 'application/json',
});
const displayTitle = panel ? panel.getDisplayTitle() : 'Explore';
const fileName = `${displayTitle}-service-graph-${dateTimeFormat(new Date())}.json`;
saveAs(blob, fileName);
};
onDataFrameChange = (item: SelectableValue<DataTransformerID | number>) => {
this.setState({
transformId:
@ -232,6 +246,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
const dataFrame = dataFrames[index];
const hasLogs = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'logs');
const hasTraces = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'trace');
const hasServiceGraph = dataFrames.some((df) => df?.meta?.preferredVisualisationType === 'nodeGraph');
return (
<div className={styles.wrap} aria-label={selectors.components.PanelInspector.Data.content}>
@ -282,6 +297,18 @@ export class InspectDataTab extends PureComponent<Props, State> {
Download traces
</Button>
)}
{hasServiceGraph && (
<Button
variant="primary"
onClick={this.exportServiceGraph}
className={css`
margin-bottom: 10px;
margin-left: 10px;
`}
>
Download service graph
</Button>
)}
</div>
<div className={styles.content}>
<AutoSizer>

@ -15,6 +15,7 @@ import { BackendDataSourceResponse, FetchResponse, setBackendSrv, setDataSourceS
import { DEFAULT_LIMIT, TempoJsonData, TempoDatasource, TempoQuery } from './datasource';
import mockJson from './mockJsonResponse.json';
import mockServiceGraph from './mockServiceGraph.json';
jest.mock('@grafana/runtime', () => {
return {
@ -210,6 +211,24 @@ describe('Tempo data source', () => {
expect(response.data.length).toBe(0);
});
it('should handle service graph upload', async () => {
const ds = new TempoDatasource(defaultSettings);
ds.uploadedJson = JSON.stringify(mockServiceGraph);
const response = await lastValueFrom(
ds.query({
targets: [{ queryType: 'upload', refId: 'A' }],
} as any)
);
expect(response.data).toHaveLength(2);
const nodesFrame = response.data[0];
expect(nodesFrame.name).toBe('Nodes');
expect(nodesFrame.meta.preferredVisualisationType).toBe('nodeGraph');
const edgesFrame = response.data[1];
expect(edgesFrame.name).toBe('Edges');
expect(edgesFrame.meta.preferredVisualisationType).toBe('nodeGraph');
});
it('should build search query correctly', () => {
const templateSrv: any = { replace: jest.fn() };
const ds = new TempoDatasource(defaultSettings, templateSrv);

@ -190,11 +190,17 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
if (targets.upload?.length) {
if (this.uploadedJson) {
const otelTraceData = JSON.parse(this.uploadedJson as string);
if (!otelTraceData.batches) {
subQueries.push(of({ error: { message: 'JSON is not valid OpenTelemetry format' }, data: [] }));
const jsonData = JSON.parse(this.uploadedJson as string);
const isTraceData = jsonData.batches;
const isServiceGraphData =
Array.isArray(jsonData) && jsonData.some((df) => df?.meta?.preferredVisualisationType === 'nodeGraph');
if (isTraceData) {
subQueries.push(of(transformFromOTEL(jsonData.batches, this.nodeGraph?.enabled)));
} else if (isServiceGraphData) {
subQueries.push(of({ data: jsonData, state: LoadingState.Done }));
} else {
subQueries.push(of(transformFromOTEL(otelTraceData.batches, this.nodeGraph?.enabled)));
subQueries.push(of({ error: { message: 'Unable to parse uploaded data.' }, data: [] }));
}
} else {
subQueries.push(of({ data: [], state: LoadingState.Done }));

@ -0,0 +1,315 @@
[
{
"name": "Nodes",
"meta": {
"preferredVisualisationType": "nodeGraph"
},
"fields": [
{
"name": "id",
"type": "string",
"config": {
"links": [
{
"url": "",
"title": "Request rate",
"internal": {
"query": {
"expr": "rate(traces_service_graph_request_total{server=\"${__data.fields.id}\"}[$__rate_interval])"
},
"datasourceUid": "7_o0Ho87z",
"datasourceName": "Prometheus"
}
},
{
"url": "",
"title": "Request histogram",
"internal": {
"query": {
"expr": "histogram_quantile(0.9, sum(rate(traces_service_graph_request_server_seconds_bucket{server=\"${__data.fields.id}\"}[$__rate_interval])) by (le, client, server))"
},
"datasourceUid": "7_o0Ho87z",
"datasourceName": "Prometheus"
}
},
{
"url": "",
"title": "Failed request rate",
"internal": {
"query": {
"expr": "rate(traces_service_graph_request_failed_total{server=\"${__data.fields.id}\"}[$__rate_interval])"
},
"datasourceUid": "7_o0Ho87z",
"datasourceName": "Prometheus"
}
},
{
"url": "",
"title": "View traces",
"internal": {
"query": {
"queryType": "nativeSearch",
"serviceName": "${__data.fields[0]}"
},
"datasourceUid": "TNS Tempo",
"datasourceName": "Tempo"
}
}
]
},
"values": ["db", "app", "lb"],
"state": null
},
{
"name": "title",
"config": {
"displayName": "Service name"
},
"type": "string",
"values": ["db", "app", "lb"],
"state": null
},
{
"name": "mainstat",
"config": {
"unit": "ms/r",
"displayName": "Average response time"
},
"type": "number",
"values": [8.817128259611174, 51.8661058283908, null],
"state": {
"calcs": {
"sum": null,
"max": 51.8661058283908,
"min": 8.817128259611174,
"logmin": 8.817128259611174,
"mean": null,
"last": null,
"first": 8.817128259611174,
"lastNotNull": null,
"firstNotNull": 8.817128259611174,
"count": 3,
"nonNullCount": 3,
"allIsNull": false,
"allIsZero": false,
"range": 43.04897756877963,
"diff": null,
"delta": null,
"step": 43.04897756877963,
"diffperc": null,
"previousDeltaUp": true
},
"displayName": "Average response time",
"multipleFrames": false
}
},
{
"name": "secondarystat",
"config": {
"unit": "r/sec",
"displayName": "Requests per second"
},
"type": "number",
"values": [10.11, 10.16, null],
"state": {
"calcs": {
"sum": null,
"max": 10.16,
"min": 10.11,
"logmin": 10.11,
"mean": null,
"last": null,
"first": 10.11,
"lastNotNull": null,
"firstNotNull": 10.11,
"count": 3,
"nonNullCount": 3,
"allIsNull": false,
"allIsZero": false,
"range": 0.05000000000000071,
"diff": null,
"delta": null,
"step": 0.05000000000000071,
"diffperc": null,
"previousDeltaUp": true
},
"displayName": "Requests per second",
"multipleFrames": false
}
},
{
"name": "arc__success",
"config": {
"displayName": "Success",
"color": {
"fixedColor": "green",
"mode": "fixed"
}
},
"type": "number",
"values": [0.9223488506857254, 0.9141946824873654, 1],
"state": {
"calcs": {
"sum": 2.836543533173091,
"max": 1,
"min": 0.9141946824873654,
"logmin": 0.9141946824873654,
"mean": 0.9455145110576969,
"last": 1,
"first": 0.9223488506857254,
"lastNotNull": 1,
"firstNotNull": 0.9223488506857254,
"count": 3,
"nonNullCount": 3,
"allIsNull": false,
"allIsZero": false,
"range": 0.08580531751263465,
"diff": 0.07765114931427464,
"delta": 1,
"step": -0.008154168198360012,
"diffperc": 0.08418848167539263,
"previousDeltaUp": true
},
"displayName": "Success",
"multipleFrames": false
}
},
{
"name": "arc__failed",
"config": {
"displayName": "Failed",
"color": {
"fixedColor": "red",
"mode": "fixed"
}
},
"type": "number",
"values": [0.07765114931427468, 0.08580531751263458, 0],
"state": {
"calcs": {
"sum": 0.16345646682690926,
"max": 0.08580531751263458,
"min": 0,
"logmin": 0.07765114931427468,
"mean": 0.05448548894230309,
"last": 0,
"first": 0.07765114931427468,
"lastNotNull": 0,
"firstNotNull": 0.07765114931427468,
"count": 3,
"nonNullCount": 3,
"allIsNull": false,
"allIsZero": false,
"range": 0.08580531751263458,
"diff": -0.07765114931427468,
"delta": 0.008154168198359901,
"step": -0.08580531751263458,
"diffperc": -1,
"previousDeltaUp": false
},
"displayName": "Failed",
"multipleFrames": false
}
}
],
"first": ["db", "app", "lb"],
"parsers": {},
"length": 3
},
{
"name": "Edges",
"meta": {
"preferredVisualisationType": "nodeGraph"
},
"fields": [
{
"name": "id",
"type": "string",
"config": {},
"values": ["app_db", "lb_app"],
"state": null
},
{
"name": "source",
"type": "string",
"config": {},
"values": ["app", "lb"],
"state": null
},
{
"name": "target",
"type": "string",
"config": {},
"values": ["db", "app"],
"state": null
},
{
"name": "mainstat",
"config": {
"unit": "r",
"displayName": "Requests"
},
"type": "number",
"values": [36389.368959065294, 36559.070202313786],
"state": {
"calcs": {
"sum": 72948.43916137908,
"max": 36559.070202313786,
"min": 36389.368959065294,
"logmin": 36389.368959065294,
"mean": 36474.21958068954,
"last": 36559.070202313786,
"first": 36389.368959065294,
"lastNotNull": 36559.070202313786,
"firstNotNull": 36389.368959065294,
"count": 2,
"nonNullCount": 2,
"allIsNull": false,
"allIsZero": false,
"range": 169.70124324849166,
"diff": 169.70124324849166,
"delta": 169.70124324849166,
"step": 169.70124324849166,
"diffperc": 0.0046634840917244265,
"previousDeltaUp": true
}
}
},
{
"name": "secondarystat",
"config": {
"unit": "ms/r",
"displayName": "Average response time"
},
"type": "number",
"values": [8.817128259611174, 51.8661058283908],
"state": {
"calcs": {
"sum": 60.683234088001974,
"max": 51.8661058283908,
"min": 8.817128259611174,
"logmin": 8.817128259611174,
"mean": 30.341617044000987,
"last": 51.8661058283908,
"first": 8.817128259611174,
"lastNotNull": 51.8661058283908,
"firstNotNull": 8.817128259611174,
"count": 2,
"nonNullCount": 2,
"allIsNull": false,
"allIsZero": false,
"range": 43.04897756877963,
"diff": 43.04897756877963,
"delta": 43.04897756877963,
"step": 43.04897756877963,
"diffperc": 4.882426148429198,
"previousDeltaUp": true
}
}
}
],
"first": ["app_db", "lb_app"],
"parsers": {},
"length": 2
}
]
Loading…
Cancel
Save