Move createGraphFrames to grafana/o11y-ds-frontend package (#97394)
* Move createGraphFrames to o11y-ds-frontend package * Remove duplicated mock response * Rename files and function
This commit is contained in:
+1
-1
@@ -381,7 +381,7 @@ exports[`better eslint`] = {
|
||||
"packages/grafana-e2e-selectors/src/resolver.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"packages/grafana-o11y-ds-frontend/src/utils.ts:5381": [
|
||||
"packages/grafana-o11y-ds-frontend/src/createNodeGraphFrames.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"packages/grafana-prometheus/src/components/PromQueryField.test.tsx:5381": [
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { createDataFrame, DataFrameView } from '@grafana/data';
|
||||
|
||||
import { createNodeGraphFrames } from './createNodeGraphFrames';
|
||||
import { bigTraceResponse } from './mocks/traceResponse';
|
||||
|
||||
describe('createGraphFrames', () => {
|
||||
it('transforms basic response into nodes and edges frame', async () => {
|
||||
const frames = createNodeGraphFrames(bigTraceResponse);
|
||||
expect(frames.length).toBe(2);
|
||||
expect(frames[0].length).toBe(30);
|
||||
expect(frames[1].length).toBe(29);
|
||||
|
||||
let view = new DataFrameView(frames[0]);
|
||||
expect(view.get(0)).toMatchObject({
|
||||
id: '4322526419282105830',
|
||||
title: 'loki-all',
|
||||
subtitle: 'store.validateQueryTimeRange',
|
||||
mainstat: '0ms (0.02%)',
|
||||
secondarystat: '0ms (100%)',
|
||||
color: 0.00021968356127648162,
|
||||
});
|
||||
|
||||
expect(view.get(29)).toMatchObject({
|
||||
id: '4450900759028499335',
|
||||
title: 'loki-all',
|
||||
subtitle: 'HTTP GET - loki_api_v1_query_range',
|
||||
mainstat: '18.21ms (100%)',
|
||||
secondarystat: '3.22ms (17.71%)',
|
||||
color: 0.17707117189595056,
|
||||
});
|
||||
|
||||
view = new DataFrameView(frames[1]);
|
||||
expect(view.get(28)).toMatchObject({
|
||||
id: '4450900759028499335--4790760741274015949',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles single span response', async () => {
|
||||
const frames = createNodeGraphFrames(singleSpanResponse);
|
||||
expect(frames.length).toBe(2);
|
||||
expect(frames[0].length).toBe(1);
|
||||
|
||||
const view = new DataFrameView(frames[0]);
|
||||
expect(view.get(0)).toMatchObject({
|
||||
id: '4322526419282105830',
|
||||
title: 'loki-all',
|
||||
subtitle: 'store.validateQueryTimeRange',
|
||||
mainstat: '14.98ms (100%)',
|
||||
secondarystat: '14.98ms (100%)',
|
||||
color: 1.000007560204647,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing spans', async () => {
|
||||
const frames = createNodeGraphFrames(missingSpanResponse);
|
||||
expect(frames.length).toBe(2);
|
||||
expect(frames[0].length).toBe(2);
|
||||
expect(frames[1].length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
const missingSpanResponse = createDataFrame({
|
||||
fields: [
|
||||
{ name: 'traceID', values: ['04450900759028499335', '04450900759028499335'] },
|
||||
{ name: 'spanID', values: ['1', '2'] },
|
||||
{ name: 'parentSpanID', values: ['', '3'] },
|
||||
{ name: 'operationName', values: ['store.validateQueryTimeRange', 'store.validateQueryTimeRange'] },
|
||||
{ name: 'serviceName', values: ['loki-all', 'loki-all'] },
|
||||
{ name: 'startTime', values: [1619712655875.4539, 1619712655880.4539] },
|
||||
{ name: 'duration', values: [14.984, 4.984] },
|
||||
],
|
||||
});
|
||||
|
||||
const singleSpanResponse = createDataFrame({
|
||||
fields: [
|
||||
{ name: 'traceID', values: ['04450900759028499335'] },
|
||||
{ name: 'spanID', values: ['4322526419282105830'] },
|
||||
{ name: 'parentSpanID', values: [''] },
|
||||
{ name: 'operationName', values: ['store.validateQueryTimeRange'] },
|
||||
{ name: 'serviceName', values: ['loki-all'] },
|
||||
{ name: 'startTime', values: [1619712655875.4539] },
|
||||
{ name: 'duration', values: [14.984] },
|
||||
],
|
||||
});
|
||||
+125
-2
@@ -1,8 +1,80 @@
|
||||
import {
|
||||
FieldType,
|
||||
NodeGraphDataFrameFieldNames as Fields,
|
||||
DataFrameView,
|
||||
DataFrame,
|
||||
MutableDataFrame,
|
||||
} from '@grafana/data';
|
||||
|
||||
export function createNodeGraphFrames(data: DataFrame): DataFrame[] {
|
||||
const { nodes, edges } = convertTraceToGraph(data);
|
||||
const [nodesFrame, edgesFrame] = makeFrames();
|
||||
|
||||
for (const node of nodes) {
|
||||
nodesFrame.add(node);
|
||||
}
|
||||
for (const edge of edges) {
|
||||
edgesFrame.add(edge);
|
||||
}
|
||||
|
||||
return [nodesFrame, edgesFrame];
|
||||
}
|
||||
|
||||
function convertTraceToGraph(data: DataFrame): { nodes: Node[]; edges: Edge[] } {
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
const view = new DataFrameView<TraceRow>(data);
|
||||
|
||||
const traceDuration = findTraceDuration(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 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]: stats.main,
|
||||
[Fields.secondaryStat]: stats.secondary,
|
||||
[Fields.color]: selfDuration / traceDuration,
|
||||
});
|
||||
|
||||
// Sometimes some span can be missing. Don't add edges for those.
|
||||
if (row.parentSpanID && spanMap[row.parentSpanID].span) {
|
||||
edges.push({
|
||||
[Fields.id]: row.parentSpanID + '--' + row.spanID,
|
||||
[Fields.target]: row.spanID,
|
||||
[Fields.source]: row.parentSpanID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get non overlapping duration of the ranges as they can overlap or have gaps.
|
||||
*/
|
||||
import { FieldType, MutableDataFrame, NodeGraphDataFrameFieldNames as Fields } from '@grafana/data';
|
||||
|
||||
export function getNonOverlappingDuration(ranges: Array<[number, number]>): number {
|
||||
ranges.sort((a, b) => a[0] - b[0]);
|
||||
const mergedRanges = ranges.reduce<Array<[number, number]>>((acc, range) => {
|
||||
@@ -117,3 +189,54 @@ export function makeFrames() {
|
||||
|
||||
return [nodesFrame, edgesFrame];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(view: DataFrameView<TraceRow>): number {
|
||||
let traceEndTime = 0;
|
||||
let traceStartTime = Infinity;
|
||||
|
||||
for (let i = 0; i < view.length; i++) {
|
||||
const row = view.get(i);
|
||||
|
||||
if (row.startTime < traceStartTime) {
|
||||
traceStartTime = row.startTime;
|
||||
}
|
||||
|
||||
if (row.startTime + row.duration > traceEndTime) {
|
||||
traceEndTime = row.startTime + row.duration;
|
||||
}
|
||||
}
|
||||
|
||||
return traceEndTime - traceStartTime;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
interface TraceRow {
|
||||
traceID: string;
|
||||
spanID: string;
|
||||
parentSpanID: string;
|
||||
operationName: string;
|
||||
serviceName: string;
|
||||
serviceTags: string;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
logs: string;
|
||||
tags: string;
|
||||
}
|
||||
@@ -12,5 +12,5 @@ export * from './TraceToLogs/TagMappingInput';
|
||||
export * from './TraceToLogs/TraceToLogsSettings';
|
||||
export * from './TraceToMetrics/TraceToMetricsSettings';
|
||||
export * from './TraceToProfiles/TraceToProfilesSettings';
|
||||
export * from './utils';
|
||||
export * from './createNodeGraphFrames';
|
||||
export * from './combineResponses';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,63 +1,6 @@
|
||||
import { DataFrameView, dateTime, createDataFrame, FieldType } from '@grafana/data';
|
||||
import { dateTime, createDataFrame, FieldType } from '@grafana/data';
|
||||
|
||||
import { createGraphFrames, mapPromMetricsToServiceMap } from './graphTransform';
|
||||
import { bigResponse } from './test/testResponse';
|
||||
|
||||
describe('createGraphFrames', () => {
|
||||
it('transforms basic response into nodes and edges frame', async () => {
|
||||
const frames = createGraphFrames(bigResponse);
|
||||
expect(frames.length).toBe(2);
|
||||
expect(frames[0].length).toBe(30);
|
||||
expect(frames[1].length).toBe(29);
|
||||
|
||||
let view = new DataFrameView(frames[0]);
|
||||
expect(view.get(0)).toMatchObject({
|
||||
id: '4322526419282105830',
|
||||
title: 'loki-all',
|
||||
subtitle: 'store.validateQueryTimeRange',
|
||||
mainstat: '0ms (0.02%)',
|
||||
secondarystat: '0ms (100%)',
|
||||
color: 0.00021968356127648162,
|
||||
});
|
||||
|
||||
expect(view.get(29)).toMatchObject({
|
||||
id: '4450900759028499335',
|
||||
title: 'loki-all',
|
||||
subtitle: 'HTTP GET - loki_api_v1_query_range',
|
||||
mainstat: '18.21ms (100%)',
|
||||
secondarystat: '3.22ms (17.71%)',
|
||||
color: 0.17707117189595056,
|
||||
});
|
||||
|
||||
view = new DataFrameView(frames[1]);
|
||||
expect(view.get(28)).toMatchObject({
|
||||
id: '4450900759028499335--4790760741274015949',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles single span response', async () => {
|
||||
const frames = createGraphFrames(singleSpanResponse);
|
||||
expect(frames.length).toBe(2);
|
||||
expect(frames[0].length).toBe(1);
|
||||
|
||||
const view = new DataFrameView(frames[0]);
|
||||
expect(view.get(0)).toMatchObject({
|
||||
id: '4322526419282105830',
|
||||
title: 'loki-all',
|
||||
subtitle: 'store.validateQueryTimeRange',
|
||||
mainstat: '14.98ms (100%)',
|
||||
secondarystat: '14.98ms (100%)',
|
||||
color: 1.000007560204647,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing spans', async () => {
|
||||
const frames = createGraphFrames(missingSpanResponse);
|
||||
expect(frames.length).toBe(2);
|
||||
expect(frames[0].length).toBe(2);
|
||||
expect(frames[1].length).toBe(0);
|
||||
});
|
||||
});
|
||||
import { mapPromMetricsToServiceMap } from './graphTransform';
|
||||
|
||||
it('assigns correct field type even if values are numbers', async () => {
|
||||
const range = {
|
||||
@@ -199,30 +142,6 @@ describe('mapPromMetricsToServiceMap', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const singleSpanResponse = createDataFrame({
|
||||
fields: [
|
||||
{ name: 'traceID', values: ['04450900759028499335'] },
|
||||
{ name: 'spanID', values: ['4322526419282105830'] },
|
||||
{ name: 'parentSpanID', values: [''] },
|
||||
{ name: 'operationName', values: ['store.validateQueryTimeRange'] },
|
||||
{ name: 'serviceName', values: ['loki-all'] },
|
||||
{ name: 'startTime', values: [1619712655875.4539] },
|
||||
{ name: 'duration', values: [14.984] },
|
||||
],
|
||||
});
|
||||
|
||||
const missingSpanResponse = createDataFrame({
|
||||
fields: [
|
||||
{ name: 'traceID', values: ['04450900759028499335', '04450900759028499335'] },
|
||||
{ name: 'spanID', values: ['1', '2'] },
|
||||
{ name: 'parentSpanID', values: ['', '3'] },
|
||||
{ name: 'operationName', values: ['store.validateQueryTimeRange', 'store.validateQueryTimeRange'] },
|
||||
{ name: 'serviceName', values: ['loki-all', 'loki-all'] },
|
||||
{ name: 'startTime', values: [1619712655875.4539, 1619712655880.4539] },
|
||||
{ name: 'duration', values: [14.984, 4.984] },
|
||||
],
|
||||
});
|
||||
|
||||
const totalsPromMetric = (namespace?: boolean) =>
|
||||
createDataFrame({
|
||||
refId: 'traces_service_graph_request_total',
|
||||
|
||||
@@ -10,127 +10,6 @@ import {
|
||||
FieldType,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { getNonOverlappingDuration, getStats, makeFrames, makeSpanMap } from '@grafana/o11y-ds-frontend';
|
||||
|
||||
/**
|
||||
* Row in a trace dataFrame
|
||||
*/
|
||||
interface Row {
|
||||
traceID: string;
|
||||
spanID: string;
|
||||
parentSpanID: string;
|
||||
operationName: string;
|
||||
serviceName: string;
|
||||
serviceTags: string;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
logs: string;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
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: DataFrame): DataFrame[] {
|
||||
const { nodes, edges } = convertTraceToGraph(data);
|
||||
const [nodesFrame, edgesFrame] = makeFrames();
|
||||
|
||||
for (const node of nodes) {
|
||||
nodesFrame.add(node);
|
||||
}
|
||||
for (const edge of edges) {
|
||||
edgesFrame.add(edge);
|
||||
}
|
||||
|
||||
return [nodesFrame, edgesFrame];
|
||||
}
|
||||
|
||||
function convertTraceToGraph(data: DataFrame): { nodes: Node[]; edges: Edge[] } {
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
const view = new DataFrameView<Row>(data);
|
||||
|
||||
const traceDuration = findTraceDuration(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 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]: stats.main,
|
||||
[Fields.secondaryStat]: stats.secondary,
|
||||
[Fields.color]: selfDuration / traceDuration,
|
||||
});
|
||||
|
||||
// Sometimes some span can be missing. Don't add edges for those.
|
||||
if (row.parentSpanID && spanMap[row.parentSpanID].span) {
|
||||
edges.push({
|
||||
[Fields.id]: row.parentSpanID + '--' + row.spanID,
|
||||
[Fields.target]: row.spanID,
|
||||
[Fields.source]: row.parentSpanID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(view: DataFrameView<Row>): number {
|
||||
let traceEndTime = 0;
|
||||
let traceStartTime = Infinity;
|
||||
|
||||
for (let i = 0; i < view.length; i++) {
|
||||
const row = view.get(i);
|
||||
|
||||
if (row.startTime < traceStartTime) {
|
||||
traceStartTime = row.startTime;
|
||||
}
|
||||
|
||||
if (row.startTime + row.duration > traceEndTime) {
|
||||
traceEndTime = row.startTime + row.duration;
|
||||
}
|
||||
}
|
||||
|
||||
return traceEndTime - traceStartTime;
|
||||
}
|
||||
|
||||
export const secondsMetric = 'traces_service_graph_request_server_seconds_sum';
|
||||
export const totalsMetric = 'traces_service_graph_request_total';
|
||||
|
||||
@@ -22,11 +22,10 @@ import {
|
||||
TraceSpanReference,
|
||||
TraceSpanRow,
|
||||
} from '@grafana/data';
|
||||
import { TraceToProfilesData } from '@grafana/o11y-ds-frontend';
|
||||
import { createNodeGraphFrames, TraceToProfilesData } from '@grafana/o11y-ds-frontend';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
|
||||
import { SearchTableType } from './dataquery.gen';
|
||||
import { createGraphFrames } from './graphTransform';
|
||||
import { Span, SpanAttributes, Spanset, TempoJsonData, TraceSearchMetadata } from './types';
|
||||
|
||||
function getAttributeValue(value: collectorTypes.opentelemetryProto.common.v1.AnyValue): any {
|
||||
@@ -195,7 +194,7 @@ export function transformFromOTLP(
|
||||
|
||||
let data = [frame];
|
||||
if (nodeGraph) {
|
||||
data.push(...(createGraphFrames(frame) as MutableDataFrame[]));
|
||||
data.push(...(createNodeGraphFrames(frame) as MutableDataFrame[]));
|
||||
}
|
||||
|
||||
return { data };
|
||||
@@ -446,7 +445,7 @@ export function transformTrace(
|
||||
|
||||
let data = [...response.data];
|
||||
if (nodeGraph) {
|
||||
data.push(...createGraphFrames(toDataFrame(frame)));
|
||||
data.push(...createNodeGraphFrames(toDataFrame(frame)));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user