2021-08-17 08:48:29 -05:00
|
|
|
import { groupBy } from 'lodash';
|
2021-08-18 23:38:31 -05:00
|
|
|
import { from, lastValueFrom, merge, Observable, of, throwError } from 'rxjs';
|
|
|
|
import { map, mergeMap, toArray } from 'rxjs/operators';
|
2020-10-13 12:12:49 -05:00
|
|
|
import {
|
2021-03-05 07:28:17 -06:00
|
|
|
DataQuery,
|
2020-10-13 12:12:49 -05:00
|
|
|
DataQueryRequest,
|
|
|
|
DataQueryResponse,
|
2021-05-10 10:12:19 -05:00
|
|
|
DataSourceApi,
|
2021-03-05 07:28:17 -06:00
|
|
|
DataSourceInstanceSettings,
|
2021-08-17 08:48:29 -05:00
|
|
|
DataSourceJsonData,
|
2021-08-05 08:13:44 -05:00
|
|
|
LoadingState,
|
2020-10-13 12:12:49 -05:00
|
|
|
} from '@grafana/data';
|
2021-03-05 07:28:17 -06:00
|
|
|
import { DataSourceWithBackend } from '@grafana/runtime';
|
2021-08-18 23:38:31 -05:00
|
|
|
|
2021-08-17 08:48:29 -05:00
|
|
|
import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings';
|
2021-05-10 10:12:19 -05:00
|
|
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
2021-08-17 08:48:29 -05:00
|
|
|
import { LokiOptions, LokiQuery } from '../loki/types';
|
|
|
|
import { transformTrace, transformTraceList, transformFromOTLP as transformFromOTEL } from './resultTransformer';
|
|
|
|
import { PrometheusDatasource } from '../prometheus/datasource';
|
|
|
|
import { PromQuery } from '../prometheus/types';
|
|
|
|
import { mapPromMetricsToServiceMap, serviceMapMetrics } from './graphTransform';
|
2021-05-10 10:12:19 -05:00
|
|
|
|
2021-08-17 08:48:29 -05:00
|
|
|
export type TempoQueryType = 'search' | 'traceId' | 'serviceMap' | 'upload';
|
|
|
|
|
|
|
|
export interface TempoJsonData extends DataSourceJsonData {
|
|
|
|
tracesToLogs?: TraceToLogsOptions;
|
|
|
|
serviceMap?: {
|
|
|
|
datasourceUid?: string;
|
|
|
|
};
|
|
|
|
}
|
2020-10-13 12:12:49 -05:00
|
|
|
|
|
|
|
export type TempoQuery = {
|
|
|
|
query: string;
|
2021-05-10 10:12:19 -05:00
|
|
|
// Query to find list of traces, e.g., via Loki
|
2021-08-17 08:48:29 -05:00
|
|
|
linkedQuery?: LokiQuery;
|
2021-05-10 10:12:19 -05:00
|
|
|
queryType: TempoQueryType;
|
2020-10-13 12:12:49 -05:00
|
|
|
} & DataQuery;
|
|
|
|
|
2021-08-17 08:48:29 -05:00
|
|
|
export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJsonData> {
|
2021-06-02 09:37:36 -05:00
|
|
|
tracesToLogs?: TraceToLogsOptions;
|
2021-08-17 08:48:29 -05:00
|
|
|
serviceMap?: {
|
|
|
|
datasourceUid?: string;
|
|
|
|
};
|
2021-08-05 08:13:44 -05:00
|
|
|
uploadedJson?: string | ArrayBuffer | null = null;
|
2021-06-02 09:37:36 -05:00
|
|
|
|
2021-08-17 08:48:29 -05:00
|
|
|
constructor(instanceSettings: DataSourceInstanceSettings<TempoJsonData>) {
|
2020-10-13 12:12:49 -05:00
|
|
|
super(instanceSettings);
|
2021-06-02 09:37:36 -05:00
|
|
|
this.tracesToLogs = instanceSettings.jsonData.tracesToLogs;
|
2021-08-17 08:48:29 -05:00
|
|
|
this.serviceMap = instanceSettings.jsonData.serviceMap;
|
2020-10-13 12:12:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
query(options: DataQueryRequest<TempoQuery>): Observable<DataQueryResponse> {
|
2021-05-10 10:12:19 -05:00
|
|
|
const subQueries: Array<Observable<DataQueryResponse>> = [];
|
|
|
|
const filteredTargets = options.targets.filter((target) => !target.hide);
|
2021-08-17 08:48:29 -05:00
|
|
|
const targets: { [type: string]: TempoQuery[] } = groupBy(filteredTargets, (t) => t.queryType || 'traceId');
|
2021-04-06 11:35:00 -05:00
|
|
|
|
2021-05-10 10:12:19 -05:00
|
|
|
// Run search queries on linked datasource
|
2021-08-17 08:48:29 -05:00
|
|
|
if (this.tracesToLogs?.datasourceUid && targets.search?.length > 0) {
|
2021-06-02 09:37:36 -05:00
|
|
|
const dsSrv = getDatasourceSrv();
|
|
|
|
subQueries.push(
|
|
|
|
from(dsSrv.get(this.tracesToLogs.datasourceUid)).pipe(
|
|
|
|
mergeMap((linkedDatasource: DataSourceApi) => {
|
|
|
|
// Wrap linked query into a data request based on original request
|
2021-08-17 08:48:29 -05:00
|
|
|
const linkedRequest: DataQueryRequest = { ...options, targets: targets.search.map((t) => t.linkedQuery!) };
|
2021-06-02 09:37:36 -05:00
|
|
|
// Find trace matchers in derived fields of the linked datasource that's identical to this datasource
|
|
|
|
const settings: DataSourceInstanceSettings<LokiOptions> = (linkedDatasource as any).instanceSettings;
|
|
|
|
const traceLinkMatcher: string[] =
|
|
|
|
settings.jsonData.derivedFields
|
|
|
|
?.filter((field) => field.datasourceUid === this.uid && field.matcherRegex)
|
|
|
|
.map((field) => field.matcherRegex) || [];
|
|
|
|
if (!traceLinkMatcher || traceLinkMatcher.length === 0) {
|
|
|
|
return throwError(
|
|
|
|
'No Loki datasource configured for search. Set up Derived Fields for traces in a Loki datasource settings and link it to this Tempo datasource.'
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return (linkedDatasource.query(linkedRequest) as Observable<DataQueryResponse>).pipe(
|
|
|
|
map((response) =>
|
|
|
|
response.error ? response : transformTraceList(response, this.uid, this.name, traceLinkMatcher)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
)
|
|
|
|
);
|
2021-05-10 10:12:19 -05:00
|
|
|
}
|
2021-04-06 11:35:00 -05:00
|
|
|
|
2021-08-17 08:48:29 -05:00
|
|
|
if (targets.upload?.length) {
|
2021-08-05 08:13:44 -05:00
|
|
|
if (this.uploadedJson) {
|
|
|
|
const otelTraceData = JSON.parse(this.uploadedJson as string);
|
|
|
|
if (!otelTraceData.batches) {
|
|
|
|
subQueries.push(of({ error: { message: 'JSON is not valid opentelemetry format' }, data: [] }));
|
|
|
|
} else {
|
|
|
|
subQueries.push(of(transformFromOTEL(otelTraceData.batches)));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
subQueries.push(of({ data: [], state: LoadingState.Done }));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-17 08:48:29 -05:00
|
|
|
if (this.serviceMap?.datasourceUid && targets.serviceMap?.length > 0) {
|
|
|
|
subQueries.push(serviceMapQuery(options, this.serviceMap.datasourceUid));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (targets.traceId?.length > 0) {
|
|
|
|
const traceRequest: DataQueryRequest<TempoQuery> = { ...options, targets: targets.traceId };
|
2021-05-10 10:12:19 -05:00
|
|
|
subQueries.push(
|
|
|
|
super.query(traceRequest).pipe(
|
|
|
|
map((response) => {
|
|
|
|
if (response.error) {
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
return transformTrace(response);
|
|
|
|
})
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
2021-03-22 13:09:15 -05:00
|
|
|
|
2021-05-10 10:12:19 -05:00
|
|
|
return merge(...subQueries);
|
2020-10-13 12:12:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
async testDatasource(): Promise<any> {
|
2021-05-27 07:47:10 -05:00
|
|
|
// to test Tempo we send a dummy traceID and verify Tempo answers with 'trace not found'
|
2021-08-18 23:38:31 -05:00
|
|
|
const response = await lastValueFrom(super.query({ targets: [{ query: '0' }] } as any));
|
2021-03-05 07:28:17 -06:00
|
|
|
|
2021-05-27 07:47:10 -05:00
|
|
|
const errorMessage = response.error?.message;
|
|
|
|
if (
|
|
|
|
errorMessage &&
|
|
|
|
errorMessage.startsWith('failed to get trace') &&
|
|
|
|
errorMessage.endsWith('trace not found in Tempo')
|
|
|
|
) {
|
|
|
|
return { status: 'success', message: 'Data source is working' };
|
2020-10-13 12:12:49 -05:00
|
|
|
}
|
|
|
|
|
2021-05-27 07:47:10 -05:00
|
|
|
return { status: 'error', message: 'Data source is not working' + (errorMessage ? `: ${errorMessage}` : '') };
|
2021-03-04 14:20:26 -06:00
|
|
|
}
|
|
|
|
|
2020-10-13 12:12:49 -05:00
|
|
|
getQueryDisplayText(query: TempoQuery) {
|
|
|
|
return query.query;
|
|
|
|
}
|
|
|
|
}
|
2021-08-17 08:48:29 -05:00
|
|
|
|
|
|
|
function queryServiceMapPrometheus(request: DataQueryRequest<PromQuery>, datasourceUid: string) {
|
|
|
|
return from(getDatasourceSrv().get(datasourceUid)).pipe(
|
|
|
|
mergeMap((ds) => {
|
|
|
|
return (ds as PrometheusDatasource).query(request);
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function serviceMapQuery(request: DataQueryRequest<TempoQuery>, datasourceUid: string) {
|
|
|
|
return queryServiceMapPrometheus(makePromServiceMapRequest(request), datasourceUid).pipe(
|
|
|
|
// Just collect all the responses first before processing into node graph data
|
|
|
|
toArray(),
|
|
|
|
map((responses: DataQueryResponse[]) => {
|
|
|
|
return {
|
|
|
|
data: mapPromMetricsToServiceMap(responses, request.range),
|
|
|
|
state: LoadingState.Done,
|
|
|
|
};
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function makePromServiceMapRequest(options: DataQueryRequest<TempoQuery>): DataQueryRequest<PromQuery> {
|
|
|
|
return {
|
|
|
|
...options,
|
|
|
|
targets: serviceMapMetrics.map((metric) => {
|
|
|
|
return {
|
|
|
|
refId: metric,
|
|
|
|
expr: `delta(${metric}[$__range])`,
|
|
|
|
instant: true,
|
|
|
|
};
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
}
|