- {node.title}
+
+
+ {node.mainStat}
- {node.subTitle}
+ {node.secondaryStat}
+
+
+
+
+ {node.title}
+
+ {node.subTitle}
@@ -87,9 +110,9 @@ export const Node = memo(function Node(props: {
});
/**
- * Shows the outer segmented circle with different color for each response type.
+ * Shows the outer segmented circle with different colors based on the supplied data.
*/
-function ResponseTypeCircle(props: { node: NodeDatum }) {
+function ColorCircle(props: { node: NodeDatum }) {
const { node } = props;
const fullStat = node.arcSections.find((s) => s.value === 1);
const theme = useTheme();
@@ -109,6 +132,10 @@ function ResponseTypeCircle(props: { node: NodeDatum }) {
}
const nonZero = node.arcSections.filter((s) => s.value !== 0);
+ if (nonZero.length === 0) {
+ // Fallback if no arc is defined
+ return
;
+ }
const { elements } = nonZero.reduce(
(acc, section) => {
diff --git a/packages/grafana-ui/src/components/NodeGraph/NodeGraph.tsx b/packages/grafana-ui/src/components/NodeGraph/NodeGraph.tsx
index 9adc9cef720..62af3ee0f8a 100644
--- a/packages/grafana-ui/src/components/NodeGraph/NodeGraph.tsx
+++ b/packages/grafana-ui/src/components/NodeGraph/NodeGraph.tsx
@@ -87,11 +87,14 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit }: Props) {
const firstNodesDataFrame = nodesDataFrames[0];
const firstEdgesDataFrame = edgesDataFrames[0];
+ const theme = useTheme();
+
// TODO we should be able to allow multiple dataframes for both edges and nodes, could be issue with node ids which in
// that case should be unique or figure a way to link edges and nodes dataframes together.
- const processed = useMemo(() => processNodes(firstNodesDataFrame, firstEdgesDataFrame), [
+ const processed = useMemo(() => processNodes(firstNodesDataFrame, firstEdgesDataFrame, theme), [
firstEdgesDataFrame,
firstNodesDataFrame,
+ theme,
]);
const { nodes: rawNodes, edges: rawEdges } = useNodeLimit(processed.nodes, processed.edges, nodeCountLimit);
@@ -102,16 +105,19 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit }: Props) {
bounds
);
const { onEdgeOpen, onNodeOpen, MenuComponent } = useContextMenu(getLinks, nodesDataFrames[0], edgesDataFrames[0]);
- const styles = getStyles(useTheme());
+ const styles = getStyles(theme);
+
+ // This cannot be inline func or it will create infinite render cycle.
+ const topLevelRef = useCallback(
+ (r) => {
+ measureRef(r);
+ (zoomRef as MutableRefObject
).current = r;
+ },
+ [measureRef, zoomRef]
+ );
return (
- {
- measureRef(r);
- (zoomRef as MutableRefObject
).current = r;
- }}
- className={styles.wrapper}
- >
+
;
+ const hasStats = e.mainStat || e.secondaryStat;
+ return shouldShow && hasStats &&
;
})}
>
);
diff --git a/packages/grafana-ui/src/components/NodeGraph/types.ts b/packages/grafana-ui/src/components/NodeGraph/types.ts
index 561fbeef621..f7a7a2ff3d3 100644
--- a/packages/grafana-ui/src/components/NodeGraph/types.ts
+++ b/packages/grafana-ui/src/components/NodeGraph/types.ts
@@ -12,6 +12,7 @@ export type NodeDatum = SimulationNodeDatum & {
value: number;
color: string;
}>;
+ color: string;
};
export type EdgeDatum = SimulationLinkDatum
& {
id: string;
diff --git a/packages/grafana-ui/src/components/NodeGraph/utils.test.ts b/packages/grafana-ui/src/components/NodeGraph/utils.test.ts
index 6a49be65737..3376fcccf8d 100644
--- a/packages/grafana-ui/src/components/NodeGraph/utils.test.ts
+++ b/packages/grafana-ui/src/components/NodeGraph/utils.test.ts
@@ -1,8 +1,9 @@
import { makeEdgesDataFrame, makeNodesDataFrame, processNodes } from './utils';
+import lightTheme from '../../themes/light';
describe('processNodes', () => {
it('handles empty args', async () => {
- expect(processNodes()).toEqual({ nodes: [], edges: [] });
+ expect(processNodes(undefined, undefined, lightTheme)).toEqual({ nodes: [], edges: [] });
});
it('returns proper nodes and edges', async () => {
@@ -13,7 +14,8 @@ describe('processNodes', () => {
[0, 1],
[0, 2],
[1, 2],
- ])
+ ]),
+ lightTheme
)
).toEqual({
nodes: [
@@ -28,6 +30,7 @@ describe('processNodes', () => {
value: 0.5,
},
],
+ color: 'rgb(213, 172, 32)',
dataFrameRowIndex: 0,
id: '0',
incoming: 0,
@@ -47,6 +50,7 @@ describe('processNodes', () => {
value: 0.5,
},
],
+ color: 'rgb(213, 172, 32)',
dataFrameRowIndex: 1,
id: '1',
incoming: 1,
@@ -66,6 +70,7 @@ describe('processNodes', () => {
value: 0.5,
},
],
+ color: 'rgb(213, 172, 32)',
dataFrameRowIndex: 2,
id: '2',
incoming: 2,
diff --git a/packages/grafana-ui/src/components/NodeGraph/utils.ts b/packages/grafana-ui/src/components/NodeGraph/utils.ts
index 56730350616..067e51e242e 100644
--- a/packages/grafana-ui/src/components/NodeGraph/utils.ts
+++ b/packages/grafana-ui/src/components/NodeGraph/utils.ts
@@ -1,4 +1,13 @@
-import { DataFrame, Field, FieldCache, FieldType, ArrayVector, MutableDataFrame } from '@grafana/data';
+import {
+ ArrayVector,
+ DataFrame,
+ Field,
+ FieldCache,
+ FieldType,
+ getFieldColorModeForField,
+ GrafanaTheme,
+ MutableDataFrame,
+} from '@grafana/data';
import { EdgeDatum, NodeDatum } from './types';
import { NodeGraphDataFrameFieldNames } from './index';
@@ -36,6 +45,7 @@ export function getNodeFields(nodes: DataFrame) {
secondaryStat: fieldsCache.getFieldByName(DataFrameFieldNames.secondaryStat),
arc: findFieldsByPrefix(nodes, DataFrameFieldNames.arc),
details: findFieldsByPrefix(nodes, DataFrameFieldNames.detail),
+ color: fieldsCache.getFieldByName(DataFrameFieldNames.color),
};
}
@@ -65,12 +75,17 @@ export enum DataFrameFieldNames {
target = 'target',
detail = 'detail__',
arc = 'arc__',
+ color = 'color',
}
/**
* Transform nodes and edges dataframes into array of objects that the layout code can then work with.
*/
-export function processNodes(nodes?: DataFrame, edges?: DataFrame): { nodes: NodeDatum[]; edges: EdgeDatum[] } {
+export function processNodes(
+ nodes: DataFrame | undefined,
+ edges: DataFrame | undefined,
+ theme: GrafanaTheme
+): { nodes: NodeDatum[]; edges: EdgeDatum[] } {
if (!nodes) {
return { nodes: [], edges: [] };
}
@@ -96,6 +111,7 @@ export function processNodes(nodes?: DataFrame, edges?: DataFrame): { nodes: Nod
color: f.config.color?.fixedColor || '',
};
}),
+ color: nodeFields.color ? getColor(nodeFields.color, index, theme) : '',
};
return acc;
}, {}) || {};
@@ -167,6 +183,7 @@ function makeNode(index: number) {
arc__errors: 0.5,
mainStat: 0.1,
secondaryStat: 2,
+ color: 0.5,
};
}
@@ -202,6 +219,12 @@ function nodesFrame() {
type: FieldType.number,
config: { color: { fixedColor: 'red' } },
},
+
+ [NodeGraphDataFrameFieldNames.color]: {
+ values: new ArrayVector(),
+ type: FieldType.number,
+ config: { color: { mode: 'continuous-GrYlRd' } },
+ },
};
return new MutableDataFrame({
@@ -252,3 +275,11 @@ function edgesFrame() {
meta: { preferredVisualisationType: 'nodeGraph' },
});
}
+
+function getColor(field: Field, index: number, theme: GrafanaTheme): string {
+ if (!field.config.color) {
+ return field.values.get(index);
+ }
+
+ return getFieldColorModeForField(field).getCalculator(field, theme)(0, field.values.get(index));
+}
diff --git a/public/app/plugins/datasource/jaeger/datasource.test.ts b/public/app/plugins/datasource/jaeger/datasource.test.ts
index d1f5edb6c92..7e741e083fc 100644
--- a/public/app/plugins/datasource/jaeger/datasource.test.ts
+++ b/public/app/plugins/datasource/jaeger/datasource.test.ts
@@ -1,16 +1,14 @@
-import {
- ArrayVector,
- DataQueryRequest,
- DataSourceInstanceSettings,
- dateTime,
- FieldType,
- PluginType,
-} from '@grafana/data';
+import { DataQueryRequest, DataSourceInstanceSettings, dateTime, FieldType, PluginType } from '@grafana/data';
import { backendSrv } from 'app/core/services/backend_srv';
import { of, throwError } from 'rxjs';
import { createFetchResponse } from 'test/helpers/createFetchResponse';
import { JaegerDatasource, JaegerQuery } from './datasource';
-import { testResponse } from './testResponse';
+import {
+ testResponse,
+ testResponseDataFrameFields,
+ testResponseNodesFields,
+ testResponseEdgesFields,
+} from './testResponse';
jest.mock('@grafana/runtime', () => ({
...((jest.requireActual('@grafana/runtime') as unknown) as object),
@@ -22,51 +20,15 @@ describe('JaegerDatasource', () => {
jest.clearAllMocks();
});
- it('returns trace when queried', async () => {
+ it('returns trace and graph when queried', async () => {
setupFetchMock({ data: [testResponse] });
+
const ds = new JaegerDatasource(defaultSettings);
- await expect(ds.query(defaultQuery)).toEmitValuesWith((val) => {
- expect(val[0].data[0].fields).toMatchObject(
- [
- { name: 'traceID', values: ['3fa414edcef6ad90', '3fa414edcef6ad90'] },
- { name: 'spanID', values: ['3fa414edcef6ad90', '0f5c1808567e4403'] },
- { name: 'parentSpanID', values: [undefined, '3fa414edcef6ad90'] },
- { name: 'operationName', values: ['HTTP GET - api_traces_traceid', '/tempopb.Querier/FindTraceByID'] },
- { name: 'serviceName', values: ['tempo-querier', 'tempo-querier'] },
- {
- name: 'serviceTags',
- values: [
- [
- { key: 'cluster', type: 'string', value: 'ops-tools1' },
- { key: 'container', type: 'string', value: 'tempo-query' },
- ],
- [
- { key: 'cluster', type: 'string', value: 'ops-tools1' },
- { key: 'container', type: 'string', value: 'tempo-query' },
- ],
- ],
- },
- { name: 'startTime', values: [1605873894680.409, 1605873894680.587] },
- { name: 'duration', values: [1049.141, 1.847] },
- { name: 'logs', values: [[], []] },
- {
- name: 'tags',
- values: [
- [
- { key: 'sampler.type', type: 'string', value: 'probabilistic' },
- { key: 'sampler.param', type: 'float64', value: 1 },
- ],
- [
- { key: 'component', type: 'string', value: 'gRPC' },
- { key: 'span.kind', type: 'string', value: 'client' },
- ],
- ],
- },
- { name: 'warnings', values: [undefined, undefined] },
- { name: 'stackTraces', values: [undefined, undefined] },
- ].map((f) => ({ ...f, values: new ArrayVector(f.values) }))
- );
- });
+ const response = await ds.query(defaultQuery).toPromise();
+ expect(response.data.length).toBe(3);
+ expect(response.data[0].fields).toMatchObject(testResponseDataFrameFields);
+ expect(response.data[1].fields).toMatchObject(testResponseNodesFields);
+ expect(response.data[2].fields).toMatchObject(testResponseEdgesFields);
});
it('returns trace when traceId with special characters is queried', async () => {
diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts
index 02bd4817d5a..e5c6104156c 100644
--- a/public/app/plugins/datasource/jaeger/datasource.ts
+++ b/public/app/plugins/datasource/jaeger/datasource.ts
@@ -15,6 +15,7 @@ import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { createTraceFrame } from './responseTransform';
+import { createGraphFrames } from './graphTransform';
export type JaegerQuery = {
query: string;
@@ -41,8 +42,13 @@ export class JaegerDatasource extends DataSourceApi {
// TODO: this api is internal, used in jaeger ui. Officially they have gRPC api that should be used.
return this._request(`/api/traces/${encodeURIComponent(id)}`).pipe(
map((response) => {
+ // We assume there is only one trace, as the querying right now does not work to query for multiple traces.
+ const traceData = response?.data?.data?.[0];
+ if (!traceData) {
+ return { data: [emptyTraceDataFrame] };
+ }
return {
- data: [createTraceFrame(response?.data?.data?.[0] || [])],
+ data: [createTraceFrame(traceData), ...createGraphFrames(traceData)],
};
})
);
diff --git a/public/app/plugins/datasource/jaeger/graphTransform.test.ts b/public/app/plugins/datasource/jaeger/graphTransform.test.ts
new file mode 100644
index 00000000000..1a58320a546
--- /dev/null
+++ b/public/app/plugins/datasource/jaeger/graphTransform.test.ts
@@ -0,0 +1,66 @@
+import { createGraphFrames } from './graphTransform';
+import {
+ testResponse,
+ testResponseEdgesFields,
+ testResponseNodesFields,
+ toEdgesFrame,
+ toNodesFrame,
+} from './testResponse';
+import { TraceResponse } from './types';
+
+describe('createGraphFrames', () => {
+ it('transforms basic response into nodes and edges frame', async () => {
+ const frames = createGraphFrames(testResponse);
+ expect(frames.length).toBe(2);
+ expect(frames[0].fields).toMatchObject(testResponseNodesFields);
+ expect(frames[1].fields).toMatchObject(testResponseEdgesFields);
+ });
+
+ it('handles single span response', async () => {
+ const frames = createGraphFrames(singleSpanResponse);
+ expect(frames.length).toBe(2);
+ expect(frames[0].fields).toMatchObject(
+ toNodesFrame([
+ ['3fa414edcef6ad90'],
+ ['tempo-querier'],
+ ['HTTP GET - api_traces_traceid'],
+ ['total: 1049.14ms (100%)'],
+ ['self: 1049.14ms (100%)'],
+ [1],
+ ])
+ );
+ expect(frames[1].fields).toMatchObject(toEdgesFrame([[], [], []]));
+ });
+});
+
+export const singleSpanResponse: TraceResponse = {
+ traceID: '3fa414edcef6ad90',
+ spans: [
+ {
+ traceID: '3fa414edcef6ad90',
+ spanID: '3fa414edcef6ad90',
+ operationName: 'HTTP GET - api_traces_traceid',
+ references: [],
+ startTime: 1605873894680409,
+ duration: 1049141,
+ tags: [
+ { key: 'sampler.type', type: 'string', value: 'probabilistic' },
+ { key: 'sampler.param', type: 'float64', value: 1 },
+ ],
+ logs: [],
+ processID: 'p1',
+ warnings: null,
+ flags: 0,
+ },
+ ],
+ processes: {
+ p1: {
+ serviceName: 'tempo-querier',
+ tags: [
+ { key: 'cluster', type: 'string', value: 'ops-tools1' },
+ { key: 'container', type: 'string', value: 'tempo-query' },
+ ],
+ },
+ },
+ warnings: null,
+};
diff --git a/public/app/plugins/datasource/jaeger/graphTransform.ts b/public/app/plugins/datasource/jaeger/graphTransform.ts
new file mode 100644
index 00000000000..6f57829d210
--- /dev/null
+++ b/public/app/plugins/datasource/jaeger/graphTransform.ts
@@ -0,0 +1,181 @@
+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);
+}
diff --git a/public/app/plugins/datasource/jaeger/responseTransform.test.ts b/public/app/plugins/datasource/jaeger/responseTransform.test.ts
index 83b0540f258..ca1ee03d97e 100644
--- a/public/app/plugins/datasource/jaeger/responseTransform.test.ts
+++ b/public/app/plugins/datasource/jaeger/responseTransform.test.ts
@@ -1,49 +1,9 @@
import { createTraceFrame } from './responseTransform';
-import { ArrayVector } from '@grafana/data';
-import { testResponse } from './testResponse';
+import { testResponse, testResponseDataFrameFields } from './testResponse';
describe('createTraceFrame', () => {
it('creates data frame from jaeger response', () => {
const dataFrame = createTraceFrame(testResponse);
- expect(dataFrame.fields).toMatchObject(
- [
- { name: 'traceID', values: ['3fa414edcef6ad90', '3fa414edcef6ad90'] },
- { name: 'spanID', values: ['3fa414edcef6ad90', '0f5c1808567e4403'] },
- { name: 'parentSpanID', values: [undefined, '3fa414edcef6ad90'] },
- { name: 'operationName', values: ['HTTP GET - api_traces_traceid', '/tempopb.Querier/FindTraceByID'] },
- { name: 'serviceName', values: ['tempo-querier', 'tempo-querier'] },
- {
- name: 'serviceTags',
- values: [
- [
- { key: 'cluster', type: 'string', value: 'ops-tools1' },
- { key: 'container', type: 'string', value: 'tempo-query' },
- ],
- [
- { key: 'cluster', type: 'string', value: 'ops-tools1' },
- { key: 'container', type: 'string', value: 'tempo-query' },
- ],
- ],
- },
- { name: 'startTime', values: [1605873894680.409, 1605873894680.587] },
- { name: 'duration', values: [1049.141, 1.847] },
- { name: 'logs', values: [[], []] },
- {
- name: 'tags',
- values: [
- [
- { key: 'sampler.type', type: 'string', value: 'probabilistic' },
- { key: 'sampler.param', type: 'float64', value: 1 },
- ],
- [
- { key: 'component', type: 'string', value: 'gRPC' },
- { key: 'span.kind', type: 'string', value: 'client' },
- ],
- ],
- },
- { name: 'warnings', values: [undefined, undefined] },
- { name: 'stackTraces', values: [undefined, undefined] },
- ].map((f) => ({ ...f, values: new ArrayVector(f.values) }))
- );
+ expect(dataFrame.fields).toMatchObject(testResponseDataFrameFields);
});
});
diff --git a/public/app/plugins/datasource/jaeger/testResponse.ts b/public/app/plugins/datasource/jaeger/testResponse.ts
index 3a85a2975a1..47d6b6c36fd 100644
--- a/public/app/plugins/datasource/jaeger/testResponse.ts
+++ b/public/app/plugins/datasource/jaeger/testResponse.ts
@@ -1,4 +1,5 @@
import { TraceResponse } from './types';
+import { ArrayVector, FieldDTO } from '@grafana/data';
export const testResponse: TraceResponse = {
traceID: '3fa414edcef6ad90',
@@ -47,3 +48,88 @@ export const testResponse: TraceResponse = {
},
warnings: null,
};
+
+function toVectors(fields: FieldDTO[]) {
+ return fields.map((f) => ({ ...f, values: new ArrayVector(f.values as any[]) }));
+}
+
+export const testResponseDataFrameFields = toVectors([
+ { name: 'traceID', values: ['3fa414edcef6ad90', '3fa414edcef6ad90'] },
+ { name: 'spanID', values: ['3fa414edcef6ad90', '0f5c1808567e4403'] },
+ { name: 'parentSpanID', values: [undefined, '3fa414edcef6ad90'] },
+ { name: 'operationName', values: ['HTTP GET - api_traces_traceid', '/tempopb.Querier/FindTraceByID'] },
+ { name: 'serviceName', values: ['tempo-querier', 'tempo-querier'] },
+ {
+ name: 'serviceTags',
+ values: [
+ [
+ { key: 'cluster', type: 'string', value: 'ops-tools1' },
+ { key: 'container', type: 'string', value: 'tempo-query' },
+ ],
+ [
+ { key: 'cluster', type: 'string', value: 'ops-tools1' },
+ { key: 'container', type: 'string', value: 'tempo-query' },
+ ],
+ ],
+ },
+ { name: 'startTime', values: [1605873894680.409, 1605873894680.587] },
+ { name: 'duration', values: [1049.141, 1.847] },
+ { name: 'logs', values: [[], []] },
+ {
+ name: 'tags',
+ values: [
+ [
+ { key: 'sampler.type', type: 'string', value: 'probabilistic' },
+ { key: 'sampler.param', type: 'float64', value: 1 },
+ ],
+ [
+ { key: 'component', type: 'string', value: 'gRPC' },
+ { key: 'span.kind', type: 'string', value: 'client' },
+ ],
+ ],
+ },
+ { name: 'warnings', values: [undefined, undefined] },
+ { name: 'stackTraces', values: [undefined, undefined] },
+]);
+
+export const testResponseNodesFields = toNodesFrame([
+ ['3fa414edcef6ad90', '0f5c1808567e4403'],
+ ['tempo-querier', 'tempo-querier'],
+ ['HTTP GET - api_traces_traceid', '/tempopb.Querier/FindTraceByID'],
+ ['total: 1049.14ms (100%)', 'total: 1.85ms (0.18%)'],
+ ['self: 1047.29ms (99.82%)', 'self: 1.85ms (100%)'],
+ [0.9982395121342127, 0.0017604878657873442],
+]);
+
+export const testResponseEdgesFields = toEdgesFrame([
+ ['3fa414edcef6ad90--0f5c1808567e4403'],
+ ['0f5c1808567e4403'],
+ ['3fa414edcef6ad90'],
+]);
+
+export function toNodesFrame(values: any[]) {
+ return toVectors([
+ { name: 'id', values: values[0] },
+ { name: 'title', values: values[1] },
+ { name: 'subTitle', values: values[2] },
+ { name: 'mainStat', values: values[3] },
+ { name: 'secondaryStat', values: values[4] },
+ {
+ name: 'color',
+ config: {
+ color: {
+ mode: 'continuous-GrYlRd',
+ },
+ },
+ values: values[5],
+ },
+ ]);
+}
+
+export function toEdgesFrame(values: any[]) {
+ return toVectors([
+ { name: 'id', values: values[0] },
+ { name: 'target', values: values[1] },
+ { name: 'source', values: values[2] },
+ ]);
+}