From 7694e7bca321547fa2f78f5777685bdf74d8dd5f Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Wed, 14 Feb 2024 11:24:03 +0000 Subject: [PATCH] Tempo: Support TraceQL metrics queries (#81886) * Support requesting traceql metrics. Small refactor of Tempo query function * Address PR comments and improve displayName --- .../plugins/datasource/tempo/datasource.ts | 118 ++++++++++++------ .../datasource/tempo/resultTransformer.ts | 75 ++++++++--- public/app/plugins/datasource/tempo/types.ts | 29 +++++ 3 files changed, 171 insertions(+), 51 deletions(-) diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index d87c04ab9cc2..398fd25f1857 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -38,7 +38,7 @@ import { BarGaugeDisplayMode, TableCellDisplayMode, VariableFormatID } from '@gr import { generateQueryFromFilters } from './SearchTraceQLEditor/utils'; import { TempoVariableQuery, TempoVariableQueryType } from './VariableQueryEditor'; import { LokiOptions } from './_importedDependencies/datasources/loki/types'; -import { PromQuery, PrometheusDatasource } from './_importedDependencies/datasources/prometheus/types'; +import { PrometheusDatasource, PromQuery } from './_importedDependencies/datasources/prometheus/types'; import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen'; import { defaultTableFilter, @@ -55,10 +55,11 @@ import TempoLanguageProvider from './language_provider'; import { createTableFrameFromMetricsSummaryQuery, emptyResponse, MetricsSummary } from './metricsSummary'; import { createTableFrameFromSearch, + formatTraceQLMetrics, + formatTraceQLResponse, transformFromOTLP as transformFromOTEL, transformTrace, transformTraceList, - formatTraceQLResponse, } from './resultTransformer'; import { doTempoChannelStream } from './streaming'; import { SearchQueryParams, TempoJsonData, TempoQuery } from './types'; @@ -337,9 +338,8 @@ export class TempoDatasource extends DataSourceWithBackend { - return { - data: formatTraceQLResponse( - response.data.traces, - this.instanceSettings, - targets.traceql[0].tableType - ), - }; - }), - catchError((err) => { - return of({ error: { message: getErrorMessage(err.data.message) }, data: [] }); - }) - ) - ); + reportInteraction('grafana_traces_traceql_queried', { + datasourceType: 'tempo', + app: options.app ?? '', + grafana_version: config.buildInfo.version, + query: queryValue ?? '', + streaming: config.featureToggles.traceQLStreaming, + }); + subQueries.push(this.handleTraceQlQuery(options, targets, queryValue)); } } } catch (error) { @@ -497,6 +481,19 @@ export class TempoDatasource extends DataSourceWithBackend, + targets: { + [type: string]: TempoQuery[]; + }, + queryValue: string + ): Observable => { + if (config.featureToggles.traceQLStreaming && this.isFeatureAvailable(FeatureName.streaming)) { + return this.handleStreamingSearch(options, targets.traceql, queryValue); + } else { + return this._request('/api/search', { + q: queryValue, + limit: options.targets[0].limit ?? DEFAULT_LIMIT, + spss: options.targets[0].spss ?? DEFAULT_SPSS, + start: options.range.from.unix(), + end: options.range.to.unix(), + }).pipe( + map((response) => { + return { + data: formatTraceQLResponse(response.data.traces, this.instanceSettings, targets.traceql[0].tableType), + }; + }), + catchError((err) => { + return of({ error: { message: getErrorMessage(err.data.message) }, data: [] }); + }) + ); + } + }; + + handleTraceQlMetricsQuery = ( + options: DataQueryRequest, + queryValue: string + ): Observable => { + return this._request('/api/metrics/query_range', { + query: queryValue, + start: options.range.from.unix(), + end: options.range.to.unix(), + }).pipe( + map((response) => { + return { + data: formatTraceQLMetrics(response.data), + }; + }), + catchError((err) => { + return of({ error: { message: getErrorMessage(err.data.message) }, data: [] }); + }) + ); + }; + traceIdQueryRequest(options: DataQueryRequest, targets: TempoQuery[]): DataQueryRequest { const request = { ...options, diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts index f28ca0e5bf14..187c20b711a4 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.ts @@ -3,32 +3,41 @@ import { collectorTypes } from '@opentelemetry/exporter-collector'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { + createDataFrame, + createTheme, DataFrame, + DataFrameDTO, + DataLink, + DataLinkConfigOrigin, DataQueryResponse, DataSourceInstanceSettings, + DataSourceJsonData, + Field, + FieldDTO, FieldType, + getDisplayProcessor, + Labels, MutableDataFrame, + toDataFrame, TraceKeyValuePair, TraceLog, TraceSpanReference, TraceSpanRow, - FieldDTO, - createDataFrame, - getDisplayProcessor, - createTheme, - DataFrameDTO, - toDataFrame, - DataLink, - DataSourceJsonData, - Field, - DataLinkConfigOrigin, } from '@grafana/data'; import { 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'; +import { + ProtoValue, + Span, + SpanAttributes, + Spanset, + TempoJsonData, + TraceqlMetricsResponse, + TraceSearchMetadata, +} from './types'; export function createTableFrame( logsFrame: DataFrame | DataFrameDTO, @@ -623,6 +632,46 @@ function transformToTraceData(data: TraceSearchMetadata) { }; } +const metricsValueToString = (value: ProtoValue): string => { + return '' + (value.stringValue || value.intValue || value.doubleValue || value.boolValue || ''); +}; + +export function formatTraceQLMetrics(data: TraceqlMetricsResponse) { + const frames = data.series.map((series) => { + const labels: Labels = {}; + series.labels.forEach((label) => { + labels[label.key] = metricsValueToString(label.value); + }); + const displayName = + series.labels.length === 1 + ? metricsValueToString(series.labels[0].value) + : `{${series.labels.map((label) => `${label.key}=${metricsValueToString(label.value)}`).join(',')}}`; + return createDataFrame({ + refId: series.promLabels, + fields: [ + { + name: 'time', + type: FieldType.time, + values: series.samples.map((sample) => parseInt(sample.timestampMs, 10)), + }, + { + name: series.promLabels, + labels, + type: FieldType.number, + values: series.samples.map((sample) => sample.value), + config: { + displayNameFromDS: displayName, + }, + }, + ], + meta: { + preferredVisualisationType: 'graph', + }, + }); + }); + return frames; +} + export function formatTraceQLResponse( data: TraceSearchMetadata[], instanceSettings: DataSourceInstanceSettings, @@ -886,10 +935,6 @@ export function createTableFrameFromTraceQlQueryAsSpans( * @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] : []); }; diff --git a/public/app/plugins/datasource/tempo/types.ts b/public/app/plugins/datasource/tempo/types.ts index 1f58ccb9eed3..8dc938635bab 100644 --- a/public/app/plugins/datasource/tempo/types.ts +++ b/public/app/plugins/datasource/tempo/types.ts @@ -119,3 +119,32 @@ export type Scope = { name: string; tags: string[]; }; + +// Maps to QueryRangeResponse of tempopb https://github.com/grafana/tempo/blob/cfda98fc5cb0777963f41e0949b9ad2d24b4b5b8/pkg/tempopb/tempo.proto#L360 +export type TraceqlMetricsResponse = { + series: MetricsSeries[]; + metrics: SearchMetrics; +}; + +export type MetricsSeries = { + labels: MetricsSeriesLabel[]; + samples: MetricsSeriesSample[]; + promLabels: string; +}; + +export type MetricsSeriesLabel = { + key: string; + value: ProtoValue; +}; + +export type ProtoValue = { + stringValue?: string; + intValue?: string; + boolValue?: boolean; + doubleValue?: string; +}; + +export type MetricsSeriesSample = { + timestampMs: string; + value: number; +};