diff --git a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts index 61024e6773e..721b556f4df 100644 --- a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts @@ -59,6 +59,10 @@ export interface TempoQuery extends common.DataQuery { * Defines the maximum number of spans per spanset that are returned from Tempo */ spss?: number; + /** + * The type of the table that is used to display the search results + */ + tableType?: SearchTableType; } export const defaultTempoQuery: Partial = { @@ -81,6 +85,14 @@ export enum SearchStreamingState { Streaming = 'streaming', } +/** + * The type of the table that is used to display the search results + */ +export enum SearchTableType { + Spans = 'spans', + Traces = 'traces', +} + /** * static fields are pre-set in the UI, dynamic fields are added by the user */ diff --git a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go index 570c7bdbca2..6c6c80f5883 100644 --- a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go @@ -17,6 +17,12 @@ const ( SearchStreamingStateStreaming SearchStreamingState = "streaming" ) +// Defines values for SearchTableType. +const ( + SearchTableTypeSpans SearchTableType = "spans" + SearchTableTypeTraces SearchTableType = "traces" +) + // Defines values for TempoQueryType. const ( TempoQueryTypeClear TempoQueryType = "clear" @@ -65,6 +71,9 @@ type DataQuery struct { // The state of the TraceQL streaming search query type SearchStreamingState string +// The type of the table that is used to display the search results +type SearchTableType string + // TempoDataQuery defines model for TempoDataQuery. type TempoDataQuery = map[string]any @@ -128,6 +137,9 @@ type TempoQuery struct { // Defines the maximum number of spans per spanset that are returned from Tempo Spss *int64 `json:"spss,omitempty"` + + // The type of the table that is used to display the search results + TableType *SearchTableType `json:"tableType,omitempty"` } // TempoQueryType search = Loki search, nativeSearch = Tempo search for backwards compatibility diff --git a/public/app/plugins/datasource/tempo/dataquery.cue b/public/app/plugins/datasource/tempo/dataquery.cue index d480dfa6fe9..358b88d9c6d 100644 --- a/public/app/plugins/datasource/tempo/dataquery.cue +++ b/public/app/plugins/datasource/tempo/dataquery.cue @@ -49,6 +49,8 @@ composableKinds: DataQuery: { filters: [...#TraceqlFilter] // Filters that are used to query the metrics summary groupBy?: [...#TraceqlFilter] + // The type of the table that is used to display the search results + tableType?: #SearchTableType } @cuetsy(kind="interface") @grafana(TSVeneer="type") // search = Loki search, nativeSearch = Tempo search for backwards compatibility @@ -57,6 +59,9 @@ composableKinds: DataQuery: { // The state of the TraceQL streaming search query #SearchStreamingState: "pending" | "streaming" | "done" | "error" @cuetsy(kind="enum") + // The type of the table that is used to display the search results + #SearchTableType: "traces" | "spans" @cuetsy(kind="enum") + // static fields are pre-set in the UI, dynamic fields are added by the user #TraceqlSearchScope: "intrinsic" | "unscoped" | "resource" | "span" @cuetsy(kind="enum") #TraceqlFilter: { diff --git a/public/app/plugins/datasource/tempo/dataquery.gen.ts b/public/app/plugins/datasource/tempo/dataquery.gen.ts index 49de9e390bf..b1e743f4bb2 100644 --- a/public/app/plugins/datasource/tempo/dataquery.gen.ts +++ b/public/app/plugins/datasource/tempo/dataquery.gen.ts @@ -56,6 +56,10 @@ export interface TempoQuery extends common.DataQuery { * Defines the maximum number of spans per spanset that are returned from Tempo */ spss?: number; + /** + * The type of the table that is used to display the search results + */ + tableType?: SearchTableType; } export const defaultTempoQuery: Partial = { @@ -78,6 +82,14 @@ export enum SearchStreamingState { Streaming = 'streaming', } +/** + * The type of the table that is used to display the search results + */ +export enum SearchTableType { + Spans = 'spans', + Traces = 'traces', +} + /** * static fields are pre-set in the UI, dynamic fields are added by the user */ diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index bd4f717d205..2f9b63fae36 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -60,7 +60,7 @@ import { transformTraceList, transformFromOTLP as transformFromOTEL, createTableFrameFromSearch, - createTableFrameFromTraceQlQuery, + formatTraceQLResponse, } from './resultTransformer'; import { doTempoChannelStream } from './streaming'; import { SearchQueryParams, TempoQuery, TempoJsonData } from './types'; @@ -359,7 +359,11 @@ export class TempoDatasource extends DataSourceWithBackend { return { - data: createTableFrameFromTraceQlQuery(response.data.traces, this.instanceSettings), + data: formatTraceQLResponse( + response.data.traces, + this.instanceSettings, + targets.traceql[0].tableType + ), }; }), catchError((err) => { @@ -413,7 +417,11 @@ export class TempoDatasource extends DataSourceWithBackend { return { - data: createTableFrameFromTraceQlQuery(response.data.traces, this.instanceSettings), + data: formatTraceQLResponse( + response.data.traces, + this.instanceSettings, + targets.traceqlSearch[0].tableType + ), }; }), catchError((err) => { diff --git a/public/app/plugins/datasource/tempo/resultTransformer.test.ts b/public/app/plugins/datasource/tempo/resultTransformer.test.ts index d99da22c990..413c58aa011 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.test.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.test.ts @@ -157,7 +157,7 @@ describe('createTableFrameFromTraceQlQuery()', () => { expect(frame.fields[5].values[0][0].fields[0].values[0]).toBe('b1586c3c8c34d'); expect(frame.fields[5].values[0][0].fields[1].name).toBe('spanID'); expect(frame.fields[5].values[0][0].fields[1].values[0]).toBe('162a4adae63b61f1'); - expect(frame.fields[5].values[0][0].fields[2].name).toBe('spanStartTime'); + expect(frame.fields[5].values[0][0].fields[2].name).toBe('time'); expect(frame.fields[5].values[0][0].fields[2].values[0]).toBe(1666188214303.201); expect(frame.fields[5].values[0][0].fields[4].name).toBe('http.method'); expect(frame.fields[5].values[0][0].fields[4].values[0]).toBe('GET'); @@ -170,7 +170,7 @@ describe('createTableFrameFromTraceQlQuery()', () => { expect(frame.fields[5].values[1][0].fields[0].values[0]).toBe('9161e77388f3e'); expect(frame.fields[5].values[1][0].fields[1].name).toBe('spanID'); expect(frame.fields[5].values[1][0].fields[1].values[0]).toBe('3b9a5c222d3ddd8f'); - expect(frame.fields[5].values[1][0].fields[2].name).toBe('spanStartTime'); + expect(frame.fields[5].values[1][0].fields[2].name).toBe('time'); expect(frame.fields[5].values[1][0].fields[2].values[0]).toBe(1666187875397.7212); expect(frame.fields[5].values[1][0].fields[4].name).toBe('by(resource.service.name)'); expect(frame.fields[5].values[1][0].fields[4].values[0]).toBe('db'); @@ -185,7 +185,7 @@ describe('createTableFrameFromTraceQlQuery()', () => { expect(frame.fields[5].values[1][1].fields[0].values[0]).toBe('9161e77388f3e'); expect(frame.fields[5].values[1][1].fields[1].name).toBe('spanID'); expect(frame.fields[5].values[1][1].fields[1].values[0]).toBe('894d90db6b5807f'); - expect(frame.fields[5].values[1][1].fields[2].name).toBe('spanStartTime'); + expect(frame.fields[5].values[1][1].fields[2].name).toBe('time'); expect(frame.fields[5].values[1][1].fields[2].values[0]).toBe(1666187875393.293); expect(frame.fields[5].values[1][1].fields[4].name).toBe('by(resource.service.name)'); expect(frame.fields[5].values[1][1].fields[4].values[0]).toBe('app'); diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts index 082b1e881d6..6f78a049c8c 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.ts @@ -18,6 +18,7 @@ import { createTheme, } from '@grafana/data'; +import { SearchTableType } from './dataquery.gen'; import { createGraphFrames } from './graphTransform'; import { Span, SpanAttributes, Spanset, TraceSearchMetadata } from './types'; @@ -564,6 +565,17 @@ function transformToTraceData(data: TraceSearchMetadata) { }; } +export function formatTraceQLResponse( + data: TraceSearchMetadata[], + instanceSettings: DataSourceInstanceSettings, + tableType?: SearchTableType +) { + if (tableType === SearchTableType.Spans) { + return createTableFrameFromTraceQlQueryAsSpans(data, instanceSettings); + } + return createTableFrameFromTraceQlQuery(data, instanceSettings); +} + export function createTableFrameFromTraceQlQuery( data: TraceSearchMetadata[], instanceSettings: DataSourceInstanceSettings @@ -660,6 +672,147 @@ export function createTableFrameFromTraceQlQuery( return [frame]; } +export function createTableFrameFromTraceQlQueryAsSpans( + data: TraceSearchMetadata[], + instanceSettings: DataSourceInstanceSettings +): 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', + fields: [ + { + name: 'traceIdHidden', + type: FieldType.string, + config: { + custom: { hidden: true }, + }, + }, + { + name: 'traceService', + type: FieldType.string, + config: { + displayNameFromDS: 'Trace Service', + custom: { + width: 200, + }, + }, + }, + { + name: 'traceName', + type: FieldType.string, + config: { + displayNameFromDS: 'Trace Name', + custom: { + width: 200, + }, + }, + }, + { + name: 'spanID', + type: FieldType.string, + config: { + unit: 'string', + displayNameFromDS: 'Span ID', + custom: { + width: 200, + }, + links: [ + { + title: 'Span: ${__value.raw}', + url: '', + internal: { + datasourceUid: instanceSettings.uid, + datasourceName: instanceSettings.name, + query: { + query: '${__data.fields.traceIdHidden}', + queryType: 'traceql', + }, + panelsState: { + trace: { + spanId: '${__value.raw}', + }, + }, + }, + }, + ], + }, + }, + { + name: 'time', + type: FieldType.time, + config: { + displayNameFromDS: 'Start time', + }, + }, + { + name: 'name', + type: FieldType.string, + config: { displayNameFromDS: 'Name', custom: { hidden: !hasNameAttribute } }, + }, + ...Object.values(spanDynamicAttrs).sort((a, b) => a.name.localeCompare(b.name)), + { + name: 'duration', + type: FieldType.number, + config: { + displayNameFromDS: 'Duration', + unit: 'ns', + custom: { + width: 120, + }, + }, + }, + ], + meta: { + preferredVisualisationType: 'table', + }, + }); + + if (!data?.length) { + return [frame]; + } + + 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) => { + spanSet.spans.forEach((span) => { + frame.add(transformSpanToTraceData(span, spanSet, trace)); + }); + }); + }); + + return [frame]; +} + const traceSubFrame = ( trace: TraceSearchMetadata, spanSet: Spanset, @@ -728,7 +881,7 @@ const traceSubFrame = ( }, }, { - name: 'spanStartTime', + name: 'time', type: FieldType.time, config: { displayNameFromDS: 'Start time', @@ -766,7 +919,7 @@ const traceSubFrame = ( } spanSet.spans.forEach((span) => { - subFrame.add(transformSpanToTraceData(span, spanSet, trace.traceID)); + subFrame.add(transformSpanToTraceData(span, spanSet, trace)); }); return subFrame; @@ -781,11 +934,15 @@ interface TraceTableData { traceDuration?: number; } -function transformSpanToTraceData(span: Span, spanSet: Spanset, traceID: string): TraceTableData { +function transformSpanToTraceData(span: Span, spanSet: Spanset, trace: TraceSearchMetadata): TraceTableData { + const spanStartTimeUnixMs = parseInt(span.startTimeUnixNano, 10) / 1000000; + const data: TraceTableData = { - traceIdHidden: traceID, + traceIdHidden: trace.traceID, + traceService: trace.rootServiceName || '', + traceName: trace.rootTraceName || '', spanID: span.spanID, - spanStartTime: parseInt(span.startTimeUnixNano, 10) / 1000000, + time: spanStartTimeUnixMs, duration: parseInt(span.durationNanos, 10), name: span.name, }; diff --git a/public/app/plugins/datasource/tempo/streaming.ts b/public/app/plugins/datasource/tempo/streaming.ts index 93f89a04f29..baf88073d41 100644 --- a/public/app/plugins/datasource/tempo/streaming.ts +++ b/public/app/plugins/datasource/tempo/streaming.ts @@ -18,7 +18,7 @@ import { getGrafanaLiveSrv } from '@grafana/runtime'; import { SearchStreamingState } from './dataquery.gen'; import { DEFAULT_SPSS, TempoDatasource } from './datasource'; -import { createTableFrameFromTraceQlQuery } from './resultTransformer'; +import { formatTraceQLResponse } from './resultTransformer'; import { SearchMetrics, TempoJsonData, TempoQuery } from './types'; export async function getLiveStreamKey(): Promise { return uuidv4(); @@ -76,7 +76,7 @@ export function doTempoChannelStream( frames = [ metricsDataFrame(metrics, frameState, elapsedTime), - ...createTableFrameFromTraceQlQuery(traces, instanceSettings), + ...formatTraceQLResponse(traces, instanceSettings, query.tableType), ]; } return { diff --git a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx index 9e6766e96d7..5c5e239cfcd 100644 --- a/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TempoQueryBuilderOptions.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { EditorField, EditorRow } from '@grafana/experimental'; -import { AutoSizeInput } from '@grafana/ui'; +import { AutoSizeInput, RadioButtonGroup } from '@grafana/ui'; import { QueryOptionGroup } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup'; +import { SearchTableType } from '../dataquery.gen'; import { DEFAULT_LIMIT, DEFAULT_SPSS } from '../datasource'; import { TempoQuery } from '../types'; @@ -17,14 +18,25 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query }) query.limit = DEFAULT_LIMIT; } + if (!query.hasOwnProperty('tableType')) { + query.tableType = SearchTableType.Traces; + } + const onLimitChange = (e: React.FormEvent) => { onChange({ ...query, limit: parseInt(e.currentTarget.value, 10) }); }; const onSpssChange = (e: React.FormEvent) => { onChange({ ...query, spss: parseInt(e.currentTarget.value, 10) }); }; + const onTableTypeChange = (val: SearchTableType) => { + onChange({ ...query, tableType: val }); + }; - const collapsedInfoList = [`Limit: ${query.limit || DEFAULT_LIMIT}`, `Spans Limit: ${query.spss || DEFAULT_SPSS}`]; + const collapsedInfoList = [ + `Limit: ${query.limit || DEFAULT_LIMIT}`, + `Spans Limit: ${query.spss || DEFAULT_SPSS}`, + `Table Format: ${query.tableType === SearchTableType.Traces ? 'Traces' : 'Spans'}`, + ]; return ( <> @@ -52,6 +64,16 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query }) value={query.spss} /> + + +