mirror of
https://github.com/grafana/grafana.git
synced 2024-11-27 03:11:01 -06:00
Tempo: TraceQL results as a spans list (#75660)
* Format tempo search results as spans * Thank you test
This commit is contained in:
parent
c08606d2c5
commit
1ce603b9e9
@ -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<TempoQuery> = {
|
||||
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -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<TempoQuery> = {
|
||||
@ -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
|
||||
*/
|
||||
|
@ -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<TempoQuery, TempoJson
|
||||
}).pipe(
|
||||
map((response) => {
|
||||
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<TempoQuery, TempoJson
|
||||
}).pipe(
|
||||
map((response) => {
|
||||
return {
|
||||
data: createTableFrameFromTraceQlQuery(response.data.traces, this.instanceSettings),
|
||||
data: formatTraceQLResponse(
|
||||
response.data.traces,
|
||||
this.instanceSettings,
|
||||
targets.traceqlSearch[0].tableType
|
||||
),
|
||||
};
|
||||
}),
|
||||
catchError((err) => {
|
||||
|
@ -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');
|
||||
|
@ -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<string, FieldDTO> = {};
|
||||
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,
|
||||
};
|
||||
|
@ -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<string> {
|
||||
return uuidv4();
|
||||
@ -76,7 +76,7 @@ export function doTempoChannelStream(
|
||||
|
||||
frames = [
|
||||
metricsDataFrame(metrics, frameState, elapsedTime),
|
||||
...createTableFrameFromTraceQlQuery(traces, instanceSettings),
|
||||
...formatTraceQLResponse(traces, instanceSettings, query.tableType),
|
||||
];
|
||||
}
|
||||
return {
|
||||
|
@ -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<Props>(({ onChange, query })
|
||||
query.limit = DEFAULT_LIMIT;
|
||||
}
|
||||
|
||||
if (!query.hasOwnProperty('tableType')) {
|
||||
query.tableType = SearchTableType.Traces;
|
||||
}
|
||||
|
||||
const onLimitChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
onChange({ ...query, limit: parseInt(e.currentTarget.value, 10) });
|
||||
};
|
||||
const onSpssChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
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<Props>(({ onChange, query })
|
||||
value={query.spss}
|
||||
/>
|
||||
</EditorField>
|
||||
<EditorField label="Table Format" tooltip="How the query data should be displayed in the results table">
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{ label: 'Traces', value: SearchTableType.Traces },
|
||||
{ label: 'Spans', value: SearchTableType.Spans },
|
||||
]}
|
||||
value={query.tableType}
|
||||
onChange={onTableTypeChange}
|
||||
/>
|
||||
</EditorField>
|
||||
</QueryOptionGroup>
|
||||
</EditorRow>
|
||||
</>
|
||||
|
Loading…
Reference in New Issue
Block a user