mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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] },
|
||||
]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user