Zipkin: Add node graph view to trace response (#34414)

* Add graph transform

* Add tests

* Refactor code

* Update test

* Fix zipkin block

Co-authored-by: David Kaltschmidt <david.kaltschmidt@gmail.com>
This commit is contained in:
Andrej Ocenas
2021-05-20 10:01:28 +02:00
committed by GitHub
parent d1d118a474
commit 615de9bf34
10 changed files with 471 additions and 228 deletions

View File

@@ -52,8 +52,8 @@ describe('Tempo data source', () => {
{ name: 'id', values: ['4322526419282105830'] },
{ name: 'title', values: ['service'] },
{ name: 'subTitle', values: ['store.validateQueryTimeRange'] },
{ name: 'mainStat', values: ['total: 14.98ms (100%)'] },
{ name: 'secondaryStat', values: ['self: 14.98ms (100%)'] },
{ name: 'mainStat', values: ['14.98ms (100%)'] },
{ name: 'secondaryStat', values: ['14.98ms (100%)'] },
{ name: 'color', values: [1.000007560204647] },
]);

View File

@@ -14,8 +14,8 @@ describe('createGraphFrames', () => {
id: '4322526419282105830',
title: 'loki-all',
subTitle: 'store.validateQueryTimeRange',
mainStat: 'total: 0ms (0.02%)',
secondaryStat: 'self: 0ms (100%)',
mainStat: '0ms (0.02%)',
secondaryStat: '0ms (100%)',
color: 0.00021968356127648162,
});
@@ -23,8 +23,8 @@ describe('createGraphFrames', () => {
id: '4450900759028499335',
title: 'loki-all',
subTitle: 'HTTP GET - loki_api_v1_query_range',
mainStat: 'total: 18.21ms (100%)',
secondaryStat: 'self: 3.22ms (17.71%)',
mainStat: '18.21ms (100%)',
secondaryStat: '3.22ms (17.71%)',
color: 0.17707117189595056,
});
@@ -44,8 +44,8 @@ describe('createGraphFrames', () => {
id: '4322526419282105830',
title: 'loki-all',
subTitle: 'store.validateQueryTimeRange',
mainStat: 'total: 14.98ms (100%)',
secondaryStat: 'self: 14.98ms (100%)',
mainStat: '14.98ms (100%)',
secondaryStat: '14.98ms (100%)',
color: 1.000007560204647,
});
});

View File

@@ -1,10 +1,5 @@
import {
DataFrame,
DataFrameView,
FieldType,
MutableDataFrame,
NodeGraphDataFrameFieldNames as Fields,
} from '@grafana/data';
import { DataFrame, DataFrameView, NodeGraphDataFrameFieldNames as Fields } from '@grafana/data';
import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '../../../core/utils/tracing';
interface Row {
traceID: string;
@@ -36,40 +31,11 @@ interface Edge {
export function createGraphFrames(data: DataFrame): DataFrame[] {
const { nodes, edges } = convertTraceToGraph(data);
const nodesFrame = new MutableDataFrame({
fields: [
{ name: Fields.id, type: FieldType.string },
{ name: Fields.title, type: FieldType.string },
{ name: Fields.subTitle, type: FieldType.string },
{ name: Fields.mainStat, type: FieldType.string, config: { displayName: 'Total time (% of trace)' } },
{ name: Fields.secondaryStat, type: FieldType.string, config: { displayName: 'Self time (% of total)' } },
{
name: Fields.color,
type: FieldType.number,
config: { color: { mode: 'continuous-GrYlRd' }, displayName: 'Self time / Trace duration' },
},
],
meta: {
preferredVisualisationType: 'nodeGraph',
},
});
const [nodesFrame, edgesFrame] = makeFrames();
for (const node of nodes) {
nodesFrame.add(node);
}
const edgesFrame = new MutableDataFrame({
fields: [
{ name: Fields.id, type: FieldType.string },
{ name: Fields.target, type: FieldType.string },
{ name: Fields.source, type: FieldType.string },
],
meta: {
preferredVisualisationType: 'nodeGraph',
},
});
for (const edge of edges) {
edgesFrame.add(edge);
}
@@ -84,24 +50,35 @@ function convertTraceToGraph(data: DataFrame): { nodes: Node[]; edges: Edge[] }
const view = new DataFrameView<Row>(data);
const traceDuration = findTraceDuration(view);
const spanMap = makeSpanMap(view);
const spanMap = makeSpanMap((index) => {
if (index >= data.length) {
return undefined;
}
const span = view.get(index);
return {
span: { ...span },
id: span.spanID,
parentIds: span.parentSpanID ? [span.parentSpanID] : [],
};
});
for (let i = 0; i < view.length; i++) {
const row = view.get(i);
const childrenDuration = getDuration(spanMap[row.spanID].children.map((c) => spanMap[c].span));
const ranges: Array<[number, number]> = spanMap[row.spanID].children.map((c) => {
const span = spanMap[c].span;
return [span.startTime, span.startTime + span.duration];
});
const childrenDuration = getNonOverlappingDuration(ranges);
const selfDuration = row.duration - childrenDuration;
const stats = getStats(row.duration, traceDuration, selfDuration);
nodes.push({
[Fields.id]: row.spanID,
[Fields.title]: row.serviceName ?? '',
[Fields.subTitle]: row.operationName,
[Fields.mainStat]: `total: ${toFixedNoTrailingZeros(row.duration)}ms (${toFixedNoTrailingZeros(
(row.duration / traceDuration) * 100
)}%)`,
[Fields.secondaryStat]: `self: ${toFixedNoTrailingZeros(selfDuration)}ms (${toFixedNoTrailingZeros(
(selfDuration / row.duration) * 100
)}%)`,
[Fields.mainStat]: stats.main,
[Fields.secondaryStat]: stats.secondary,
[Fields.color]: selfDuration / traceDuration,
});
@@ -118,10 +95,6 @@ function convertTraceToGraph(data: DataFrame): { nodes: Node[]; edges: Edge[] }
return { nodes, edges };
}
function toFixedNoTrailingZeros(n: number) {
return parseFloat(n.toFixed(2));
}
/**
* Get the duration of the whole trace as it isn't a part of the response data.
* Note: Seems like this should be the same as just longest span, but this is probably safer.
@@ -144,66 +117,3 @@ function findTraceDuration(view: DataFrameView<Row>): number {
return traceEndTime - traceStartTime;
}
/**
* Returns a map of the spans with children array for easier processing. It will also contain empty spans in case
* span is missing but other spans are it's children.
*/
function makeSpanMap(view: DataFrameView<Row>): { [id: string]: { span: Row; children: string[] } } {
const spanMap: { [id: string]: { span?: Row; children: string[] } } = {};
for (let i = 0; i < view.length; i++) {
const row = view.get(i);
if (!spanMap[row.spanID]) {
spanMap[row.spanID] = {
// Need copy because of how the view works
span: { ...row },
children: [],
};
} else {
spanMap[row.spanID].span = { ...row };
}
if (!spanMap[row.parentSpanID]) {
spanMap[row.parentSpanID] = {
span: undefined,
children: [row.spanID],
};
} else {
spanMap[row.parentSpanID].children.push(row.spanID);
}
}
return spanMap as { [id: string]: { span: Row; children: string[] } };
}
/**
* Get non overlapping duration of the spans.
*/
function getDuration(rows: Row[]): number {
const ranges = rows.map<[number, number]>((r) => [r.startTime, r.startTime + r.duration]);
ranges.sort((a, b) => a[0] - b[0]);
const mergedRanges = ranges.reduce((acc, range) => {
if (!acc.length) {
return [range];
}
const tail = acc.slice(-1)[0];
const [prevStart, prevEnd] = tail;
const [start, end] = range;
if (end < prevEnd) {
// In this case the range is completely inside the prev range so we can just ignore it.
return acc;
}
if (start > prevEnd) {
// There is no overlap so we can just add it to stack
return [...acc, range];
}
// We know there is overlap and current range ends later than previous so we can just extend the range
return [...acc.slice(0, -1), [prevStart, end]] as Array<[number, number]>;
}, [] as Array<[number, number]>);
return mergedRanges.reduce((acc, range) => {
return acc + (range[1] - range[0]);
}, 0);
}