Tempo: Support TraceQL metrics queries (#81886)

* Support requesting traceql metrics. Small refactor of Tempo query function

* Address PR comments and improve displayName
This commit is contained in:
Andre Pereira
2024-02-14 11:24:03 +00:00
committed by GitHub
parent f6ea39ff00
commit 7694e7bca3
3 changed files with 171 additions and 51 deletions
@@ -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<TempoQuery, TempoJson
try {
const appliedQuery = this.applyVariables(targets.traceql[0], options.scopedVars);
const queryValue = appliedQuery?.query || '';
const hexOnlyRegex = /^[0-9A-Fa-f]*$/;
// Check whether this is a trace ID or traceQL query by checking if it only contains hex characters
if (queryValue.trim().match(hexOnlyRegex)) {
if (this.isTraceIdQuery(queryValue)) {
// There's only hex characters so let's assume that this is a trace ID
reportInteraction('grafana_traces_traceID_queried', {
datasourceType: 'tempo',
@@ -350,39 +350,23 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
subQueries.push(this.handleTraceIdQuery(options, targets.traceql));
} else {
reportInteraction('grafana_traces_traceql_queried', {
datasourceType: 'tempo',
app: options.app ?? '',
grafana_version: config.buildInfo.version,
query: queryValue ?? '',
streaming: config.featureToggles.traceQLStreaming,
});
if (config.featureToggles.traceQLStreaming && this.isFeatureAvailable(FeatureName.streaming)) {
subQueries.push(this.handleStreamingSearch(options, targets.traceql, queryValue));
if (this.isTraceQlMetricsQuery(queryValue)) {
reportInteraction('grafana_traces_traceql_metrics_queried', {
datasourceType: 'tempo',
app: options.app ?? '',
grafana_version: config.buildInfo.version,
query: queryValue ?? '',
});
subQueries.push(this.handleTraceQlMetricsQuery(options, queryValue));
} else {
subQueries.push(
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: [] });
})
)
);
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<TempoQuery, TempoJson
return merge(...subQueries);
}
isTraceQlMetricsQuery(query: string): boolean {
// Check whether this is a metrics query by checking if it contains a metrics function
const metricsFnRegex =
/\|\s*(rate|count_over_time|avg_over_time|max_over_time|min_over_time|quantile_over_time)\s*\(/;
return !!query.trim().match(metricsFnRegex);
}
isTraceIdQuery(query: string): boolean {
const hexOnlyRegex = /^[0-9A-Fa-f]*$/;
// Check whether this is a trace ID or traceQL query by checking if it only contains hex characters
return !!query.trim().match(hexOnlyRegex);
}
applyTemplateVariables(query: TempoQuery, scopedVars: ScopedVars) {
return this.applyVariables(query, scopedVars);
}
@@ -640,6 +637,55 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
);
}
handleTraceQlQuery = (
options: DataQueryRequest<TempoQuery>,
targets: {
[type: string]: TempoQuery[];
},
queryValue: string
): Observable<DataQueryResponse> => {
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<TempoQuery>,
queryValue: string
): Observable<DataQueryResponse> => {
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<TempoQuery>, targets: TempoQuery[]): DataQueryRequest<TempoQuery> {
const request = {
...options,
@@ -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] : []);
};
@@ -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;
};