grafana/public/app/plugins/datasource/jaeger/graphTransform.ts
Andrej Ocenas c16083fcf5
Jaeger: Add node graph view for trace (#31521)
* add default arc and don't display stats background without any stats

* Add node graph transform

* Use coloring scheme for the node graph

* Fix type

* Add tests

* Fix and update test

* Fix strict ts errors

* Fix ref handling

* Update test data to connect spans to a parent
2021-03-31 17:56:15 +02:00

182 lines
5.4 KiB
TypeScript

import { DataFrame, FieldType, MutableDataFrame } from '@grafana/data';
import { NodeGraphDataFrameFieldNames as Fields } from '@grafana/ui';
import { Span, TraceResponse } from './types';
interface Node {
[Fields.id]: string;
[Fields.title]: string;
[Fields.subTitle]: string;
[Fields.mainStat]: string;
[Fields.secondaryStat]: string;
[Fields.color]: number;
}
interface Edge {
[Fields.id]: string;
[Fields.target]: string;
[Fields.source]: string;
}
export function createGraphFrames(data: TraceResponse): 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 },
{ name: Fields.secondaryStat, type: FieldType.string },
{ name: Fields.color, type: FieldType.number, config: { color: { mode: 'continuous-GrYlRd' } } },
],
meta: {
preferredVisualisationType: 'nodeGraph',
},
});
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);
}
return [nodesFrame, edgesFrame];
}
function convertTraceToGraph(data: TraceResponse): { nodes: Node[]; edges: Edge[] } {
const nodes: Node[] = [];
const edges: Edge[] = [];
const traceDuration = findTraceDuration(data.spans);
const spanMap = makeSpanMap(data.spans);
for (const span of data.spans) {
const process = data.processes[span.processID];
const childrenDuration = getDuration(spanMap[span.spanID].children.map((c) => spanMap[c].span));
const selfDuration = span.duration - childrenDuration;
nodes.push({
[Fields.id]: span.spanID,
[Fields.title]: process?.serviceName ?? '',
[Fields.subTitle]: span.operationName,
[Fields.mainStat]: `total: ${toFixedNoTrailingZeros(span.duration / 1000)}ms (${toFixedNoTrailingZeros(
(span.duration / traceDuration) * 100
)}%)`,
[Fields.secondaryStat]: `self: ${toFixedNoTrailingZeros(selfDuration / 1000)}ms (${toFixedNoTrailingZeros(
(selfDuration / span.duration) * 100
)}%)`,
[Fields.color]: selfDuration / traceDuration,
});
const parentSpanID = span.references?.find((r) => r.refType === 'CHILD_OF')?.spanID;
if (parentSpanID) {
edges.push({
[Fields.id]: parentSpanID + '--' + span.spanID,
[Fields.target]: span.spanID,
[Fields.source]: parentSpanID,
});
}
}
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.
*/
function findTraceDuration(spans: Span[]): number {
let traceEndTime = 0;
let traceStartTime = Infinity;
for (const span of spans) {
if (span.startTime < traceStartTime) {
traceStartTime = span.startTime;
}
if (span.startTime + span.duration > traceEndTime) {
traceEndTime = span.startTime + span.duration;
}
}
return traceEndTime - traceStartTime;
}
/**
* Returns a map of the spans with children array for easier processing.
*/
function makeSpanMap(spans: Span[]): { [id: string]: { span: Span; children: string[] } } {
const spanMap: { [id: string]: { span?: Span; children: string[] } } = {};
for (const span of spans) {
if (!spanMap[span.spanID]) {
spanMap[span.spanID] = {
span,
children: [],
};
} else {
spanMap[span.spanID].span = span;
}
for (const parent of span.references?.filter((r) => r.refType === 'CHILD_OF').map((r) => r.spanID) || []) {
if (!spanMap[parent]) {
spanMap[parent] = {
span: undefined,
children: [span.spanID],
};
} else {
spanMap[parent].children.push(span.spanID);
}
}
}
return spanMap as { [id: string]: { span: Span; children: string[] } };
}
/**
* Get non overlapping duration of the spans.
*/
function getDuration(spans: Span[]): number {
const ranges = spans.map<[number, number]>((span) => [span.startTime, span.startTime + span.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);
}