From ed5c90a8b197b83ad23719514ce1252a9dc4bede Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:28:13 +0100 Subject: [PATCH] Tempo: Fix Spans table format (#79938) --- .../tempo/resultTransformer.test.ts | 147 ++++++++++++++++++ .../datasource/tempo/resultTransformer.ts | 78 ++++++---- public/app/plugins/datasource/tempo/types.ts | 2 +- 3 files changed, 198 insertions(+), 29 deletions(-) diff --git a/public/app/plugins/datasource/tempo/resultTransformer.test.ts b/public/app/plugins/datasource/tempo/resultTransformer.test.ts index 413c58aa011..fe452c43de4 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.test.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.test.ts @@ -15,6 +15,7 @@ import { transformFromOTLP, createTableFrameFromSearch, createTableFrameFromTraceQlQuery, + createTableFrameFromTraceQlQueryAsSpans, } from './resultTransformer'; import { badOTLPResponse, @@ -198,6 +199,152 @@ describe('createTableFrameFromTraceQlQuery()', () => { }); }); +describe('createTableFrameFromTraceQlQueryAsSpans()', () => { + test('transforms TraceQL legacy response to DataFrame for Spans table type', () => { + const traces = [ + { + traceID: '1', + rootServiceName: 'prometheus', + rootTraceName: 'POST /api/v1/write', + startTimeUnixNano: '1702984850354934104', + durationMs: 1, + spanSet: { + spans: [ + { + spanID: '11', + startTimeUnixNano: '1702984850354934104', + durationNanos: '1377608', + }, + ], + matched: 1, + }, + }, + { + traceID: '2', + rootServiceName: 'prometheus', + rootTraceName: 'GET /api/v1/status/config', + startTimeUnixNano: '1702984840786143459', + spanSet: { + spans: [ + { + spanID: '21', + startTimeUnixNano: '1702984840786143459', + durationNanos: '542316', + }, + ], + matched: 1, + }, + }, + ]; + const frameList = createTableFrameFromTraceQlQueryAsSpans(traces, defaultSettings); + const frame = frameList[0]; + + // Trace ID field + expect(frame.fields[0].name).toBe('traceIdHidden'); + expect(frame.fields[0].values[0]).toBe('1'); + // Trace service field + expect(frame.fields[1].name).toBe('traceService'); + expect(frame.fields[1].type).toBe('string'); + expect(frame.fields[1].values[0]).toBe('prometheus'); + // Trace name field + expect(frame.fields[2].name).toBe('traceName'); + expect(frame.fields[2].type).toBe('string'); + expect(frame.fields[2].values[0]).toBe('POST /api/v1/write'); + // Span ID field + expect(frame.fields[3].name).toBe('spanID'); + expect(frame.fields[3].type).toBe('string'); + expect(frame.fields[3].values[0]).toBe('11'); + // Time field + expect(frame.fields[4].name).toBe('time'); + expect(frame.fields[4].type).toBe('time'); + expect(frame.fields[4].values[0]).toBe(1702984850354.934); + // Name field + expect(frame.fields[5].name).toBe('name'); + expect(frame.fields[5].type).toBe('string'); + expect(frame.fields[5].values[0]).toBe(undefined); + // Duration field + expect(frame.fields[6].name).toBe('duration'); + expect(frame.fields[6].type).toBe('number'); + expect(frame.fields[6].values[0]).toBe(1377608); + // No more fields + expect(frame.fields.length).toBe(7); + }); + + test('transforms TraceQL response to DataFrame for Spans table type', () => { + const traces = [ + { + traceID: '1', + rootServiceName: 'prometheus', + rootTraceName: 'POST /api/v1/write', + startTimeUnixNano: '1702984850354934104', + durationMs: 1, + spanSets: [ + { + spans: [ + { + spanID: '11', + startTimeUnixNano: '1702984850354934104', + durationNanos: '1377608', + }, + ], + matched: 1, + }, + ], + }, + { + traceID: '2', + rootServiceName: 'prometheus', + rootTraceName: 'GET /api/v1/status/config', + startTimeUnixNano: '1702984840786143459', + spanSets: [ + { + spans: [ + { + spanID: '21', + startTimeUnixNano: '1702984840786143459', + durationNanos: '542316', + }, + ], + matched: 1, + }, + ], + }, + ]; + const frameList = createTableFrameFromTraceQlQueryAsSpans(traces, defaultSettings); + const frame = frameList[0]; + + // Trace ID field + expect(frame.fields[0].name).toBe('traceIdHidden'); + expect(frame.fields[0].values[0]).toBe('1'); + // Trace service field + expect(frame.fields[1].name).toBe('traceService'); + expect(frame.fields[1].type).toBe('string'); + expect(frame.fields[1].values[0]).toBe('prometheus'); + // Trace name field + expect(frame.fields[2].name).toBe('traceName'); + expect(frame.fields[2].type).toBe('string'); + expect(frame.fields[2].values[0]).toBe('POST /api/v1/write'); + // Span ID field + expect(frame.fields[3].name).toBe('spanID'); + expect(frame.fields[3].type).toBe('string'); + expect(frame.fields[3].values[0]).toBe('11'); + // Time field + expect(frame.fields[4].name).toBe('time'); + expect(frame.fields[4].type).toBe('time'); + expect(frame.fields[4].values[0]).toBe(1702984850354.934); + // Name field + expect(frame.fields[5].name).toBe('name'); + expect(frame.fields[5].type).toBe('string'); + expect(frame.fields[5].values[0]).toBe(undefined); + // Duration field + expect(frame.fields[6].name).toBe('duration'); + expect(frame.fields[6].type).toBe('number'); + expect(frame.fields[6].values[0]).toBe(1377608); + // No more fields + expect(frame.fields.length).toBe(7); + }); +}); + describe('transformFromOTLP()', () => { // Mock the console error so that running the test suite doesnt throw the error const origError = console.error; diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts index dfb68c4216c..e552c7657ea 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.ts @@ -739,32 +739,6 @@ export function createTableFrameFromTraceQlQueryAsSpans( ): DataFrame[] { const spanDynamicAttrs: Record = {}; let hasNameAttribute = false; - - data?.forEach( - (t) => - t.spanSets?.forEach((ss) => { - ss.attributes?.forEach((attr) => { - spanDynamicAttrs[attr.key] = { - name: attr.key, - type: FieldType.string, - config: { displayNameFromDS: attr.key }, - }; - }); - ss.spans.forEach((span) => { - if (span.name) { - hasNameAttribute = true; - } - span.attributes?.forEach((attr) => { - spanDynamicAttrs[attr.key] = { - name: attr.key, - type: FieldType.string, - config: { displayNameFromDS: attr.key }, - }; - }); - }); - }) - ); - const frame = new MutableDataFrame({ name: 'Spans', refId: 'traces', @@ -856,15 +830,46 @@ export function createTableFrameFromTraceQlQueryAsSpans( }, }); - if (!data?.length) { + // According to the parameter type, `data` should never be undefined of null, but the old code had + // entries such as `!data` or `data?`, so we keep this check just for safety + if (data === undefined || data === null) { + console.error(`Unexpected ${data} value for \`data\``); return [frame]; } + if (!data.length) { + return [frame]; + } + + data.forEach((trace) => + getSpanSets(trace).forEach((ss) => { + ss.attributes?.forEach((attr) => { + spanDynamicAttrs[attr.key] = { + name: attr.key, + type: FieldType.string, + config: { displayNameFromDS: attr.key }, + }; + }); + ss.spans.forEach((span) => { + if (span.name) { + hasNameAttribute = true; + } + span.attributes?.forEach((attr) => { + spanDynamicAttrs[attr.key] = { + name: attr.key, + type: FieldType.string, + config: { displayNameFromDS: attr.key }, + }; + }); + }); + }) + ); + data // Show the most recent traces .sort((a, b) => parseInt(b?.startTimeUnixNano!, 10) / 1000000 - parseInt(a?.startTimeUnixNano!, 10) / 1000000) .forEach((trace) => { - trace.spanSets?.forEach((spanSet) => { + getSpanSets(trace).forEach((spanSet) => { spanSet.spans.forEach((span) => { frame.add(transformSpanToTraceData(span, spanSet, trace)); }); @@ -874,6 +879,23 @@ export function createTableFrameFromTraceQlQueryAsSpans( return [frame]; } +/** + * Get the spansets of a trace. + * + * Field `spanSets` is preferred to `spanSet` since the latter is deprecated in Tempo, but we + * support both for backward compatibility. + * + * @param trace a trace + * @returns the spansets of the trace, if existing + */ +const getSpanSets = (trace: TraceSearchMetadata): Spanset[] => { + if (trace.spanSets && trace.spanSet) { + console.warn('Both `spanSets` and `spanSet` are set. `spanSet` will be ignored'); + } + + return trace.spanSets || (trace.spanSet ? [trace.spanSet] : []); +}; + const traceSubFrame = ( trace: TraceSearchMetadata, spanSet: Spanset, diff --git a/public/app/plugins/datasource/tempo/types.ts b/public/app/plugins/datasource/tempo/types.ts index a888b650bc1..68503d6c278 100644 --- a/public/app/plugins/datasource/tempo/types.ts +++ b/public/app/plugins/datasource/tempo/types.ts @@ -55,7 +55,7 @@ export type TraceSearchMetadata = { rootTraceName: string; startTimeUnixNano?: string; durationMs?: number; - spanSet?: Spanset; + spanSet?: Spanset; // deprecated in Tempo, https://github.com/grafana/tempo/blob/3cc44fca03ba7d676dc77da6a18b8222546ede3c/docs/sources/tempo/api_docs/_index.md?plain=1#L619 spanSets?: Spanset[]; };