mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 09:05:45 -06:00
7688b12f9a
* Tracing: Add config for multiple named trace to metrics queries
365 lines
12 KiB
TypeScript
365 lines
12 KiB
TypeScript
import { SpanLinks } from '@jaegertracing/jaeger-ui-components/src/types/links';
|
|
import React from 'react';
|
|
|
|
import {
|
|
DataFrame,
|
|
DataLink,
|
|
DataQuery,
|
|
DataSourceInstanceSettings,
|
|
DataSourceJsonData,
|
|
dateTime,
|
|
Field,
|
|
KeyValue,
|
|
LinkModel,
|
|
mapInternalLinkToExplore,
|
|
rangeUtil,
|
|
SplitOpen,
|
|
TimeRange,
|
|
} from '@grafana/data';
|
|
import { getTemplateSrv } from '@grafana/runtime';
|
|
import { Icon } from '@grafana/ui';
|
|
import { SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
|
import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
|
import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
|
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
|
import { PromQuery } from 'app/plugins/datasource/prometheus/types';
|
|
|
|
import { LokiQuery } from '../../../plugins/datasource/loki/types';
|
|
import { getFieldLinksForExplore } from '../utils/links';
|
|
|
|
/**
|
|
* This is a factory for the link creator. It returns the function mainly so it can return undefined in which case
|
|
* the trace view won't create any links and to capture the datasource and split function making it easier to memoize
|
|
* with useMemo.
|
|
*/
|
|
export function createSpanLinkFactory({
|
|
splitOpenFn,
|
|
traceToLogsOptions,
|
|
traceToMetricsOptions,
|
|
dataFrame,
|
|
createFocusSpanLink,
|
|
}: {
|
|
splitOpenFn: SplitOpen;
|
|
traceToLogsOptions?: TraceToLogsOptions;
|
|
traceToMetricsOptions?: TraceToMetricsOptions;
|
|
dataFrame?: DataFrame;
|
|
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
|
|
}): SpanLinkFunc | undefined {
|
|
if (!dataFrame || dataFrame.fields.length === 1 || !dataFrame.fields.some((f) => Boolean(f.config.links?.length))) {
|
|
// if the dataframe contains just a single blob of data (legacy format) or does not have any links configured,
|
|
// let's try to use the old legacy path.
|
|
return legacyCreateSpanLinkFactory(splitOpenFn, traceToLogsOptions, traceToMetricsOptions, createFocusSpanLink);
|
|
} else {
|
|
return function SpanLink(span: TraceSpan): SpanLinks | undefined {
|
|
// We should be here only if there are some links in the dataframe
|
|
const field = dataFrame.fields.find((f) => Boolean(f.config.links?.length))!;
|
|
try {
|
|
const links = getFieldLinksForExplore({
|
|
field,
|
|
rowIndex: span.dataFrameRowIndex!,
|
|
splitOpenFn,
|
|
range: getTimeRangeFromSpan(span),
|
|
dataFrame,
|
|
});
|
|
|
|
return {
|
|
logLinks: [
|
|
{
|
|
href: links[0].href,
|
|
onClick: links[0].onClick,
|
|
content: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
|
|
},
|
|
],
|
|
};
|
|
} catch (error) {
|
|
// It's fairly easy to crash here for example if data source defines wrong interpolation in the data link
|
|
console.error(error);
|
|
return undefined;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
function legacyCreateSpanLinkFactory(
|
|
splitOpenFn: SplitOpen,
|
|
traceToLogsOptions?: TraceToLogsOptions,
|
|
traceToMetricsOptions?: TraceToMetricsOptions,
|
|
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>
|
|
) {
|
|
let logsDataSourceSettings: DataSourceInstanceSettings<DataSourceJsonData> | undefined;
|
|
const isSplunkDS = logsDataSourceSettings?.type === 'grafana-splunk-datasource';
|
|
if (traceToLogsOptions?.datasourceUid) {
|
|
logsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToLogsOptions.datasourceUid);
|
|
}
|
|
|
|
let metricsDataSourceSettings: DataSourceInstanceSettings<DataSourceJsonData> | undefined;
|
|
if (traceToMetricsOptions?.datasourceUid) {
|
|
metricsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToMetricsOptions.datasourceUid);
|
|
}
|
|
|
|
return function SpanLink(span: TraceSpan): SpanLinks {
|
|
const links: SpanLinks = { traceLinks: [] };
|
|
// This is reusing existing code from derived fields which may not be ideal match so some data is a bit faked at
|
|
// the moment. Issue is that the trace itself isn't clearly mapped to dataFrame (right now it's just a json blob
|
|
// inside a single field) so the dataLinks as config of that dataFrame abstraction breaks down a bit and we do
|
|
// it manually here instead of leaving it for the data source to supply the config.
|
|
let dataLink: DataLink<LokiQuery | DataQuery> | undefined = {} as DataLink<LokiQuery | DataQuery> | undefined;
|
|
|
|
// Get logs link
|
|
if (logsDataSourceSettings && traceToLogsOptions) {
|
|
switch (logsDataSourceSettings?.type) {
|
|
case 'loki':
|
|
dataLink = getLinkForLoki(span, traceToLogsOptions, logsDataSourceSettings);
|
|
break;
|
|
case 'grafana-splunk-datasource':
|
|
dataLink = getLinkForSplunk(span, traceToLogsOptions, logsDataSourceSettings);
|
|
break;
|
|
}
|
|
|
|
if (dataLink) {
|
|
const link = mapInternalLinkToExplore({
|
|
link: dataLink,
|
|
internalLink: dataLink.internal!,
|
|
scopedVars: {},
|
|
range: getTimeRangeFromSpan(
|
|
span,
|
|
{
|
|
startMs: traceToLogsOptions.spanStartTimeShift
|
|
? rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift)
|
|
: 0,
|
|
endMs: traceToLogsOptions.spanEndTimeShift
|
|
? rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift)
|
|
: 0,
|
|
},
|
|
isSplunkDS
|
|
),
|
|
field: {} as Field,
|
|
onClickFn: splitOpenFn,
|
|
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
|
});
|
|
|
|
links.logLinks = [
|
|
{
|
|
href: link.href,
|
|
onClick: link.onClick,
|
|
content: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
|
|
},
|
|
];
|
|
}
|
|
}
|
|
|
|
// Get metrics links
|
|
if (metricsDataSourceSettings && traceToMetricsOptions?.queries) {
|
|
const defaultQuery = `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`;
|
|
|
|
links.metricLinks = [];
|
|
for (const query of traceToMetricsOptions.queries) {
|
|
const dataLink: DataLink<PromQuery> = {
|
|
title: metricsDataSourceSettings.name,
|
|
url: '',
|
|
internal: {
|
|
datasourceUid: metricsDataSourceSettings.uid,
|
|
datasourceName: metricsDataSourceSettings.name,
|
|
query: {
|
|
expr: query.query || defaultQuery,
|
|
refId: 'A',
|
|
},
|
|
},
|
|
};
|
|
|
|
const link = mapInternalLinkToExplore({
|
|
link: dataLink,
|
|
internalLink: dataLink.internal!,
|
|
scopedVars: {},
|
|
range: getTimeRangeFromSpan(span, {
|
|
startMs: 0,
|
|
endMs: 0,
|
|
}),
|
|
field: {} as Field,
|
|
onClickFn: splitOpenFn,
|
|
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
|
});
|
|
|
|
links.metricLinks.push({
|
|
title: query?.name,
|
|
href: link.href,
|
|
onClick: link.onClick,
|
|
content: <Icon name="chart-line" title="Explore metrics for this span" />,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Get trace links
|
|
if (span.references && createFocusSpanLink) {
|
|
for (const reference of span.references) {
|
|
// Ignore parent-child links
|
|
if (reference.refType === 'CHILD_OF') {
|
|
continue;
|
|
}
|
|
|
|
const link = createFocusSpanLink(reference.traceID, reference.spanID);
|
|
|
|
links.traceLinks!.push({
|
|
href: link.href,
|
|
title: reference.span ? reference.span.operationName : 'View linked span',
|
|
content: <Icon name="link" title="View linked span" />,
|
|
onClick: link.onClick,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (span.subsidiarilyReferencedBy && createFocusSpanLink) {
|
|
for (const reference of span.subsidiarilyReferencedBy) {
|
|
const link = createFocusSpanLink(reference.traceID, reference.spanID);
|
|
|
|
links.traceLinks!.push({
|
|
href: link.href,
|
|
title: reference.span ? reference.span.operationName : 'View linked span',
|
|
content: <Icon name="link" title="View linked span" />,
|
|
onClick: link.onClick,
|
|
});
|
|
}
|
|
}
|
|
|
|
return links;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Default keys to use when there are no configured tags.
|
|
*/
|
|
const defaultKeys = ['cluster', 'hostname', 'namespace', 'pod'];
|
|
function getLinkForLoki(span: TraceSpan, options: TraceToLogsOptions, dataSourceSettings: DataSourceInstanceSettings) {
|
|
const { tags: keys, filterByTraceID, filterBySpanID, mapTagNamesEnabled, mappedTags } = options;
|
|
|
|
// In order, try to use mapped tags -> tags -> default tags
|
|
const keysToCheck = mapTagNamesEnabled && mappedTags?.length ? mappedTags : keys?.length ? keys : defaultKeys;
|
|
// Build tag portion of query
|
|
const tags = [...span.process.tags, ...span.tags].reduce((acc, tag) => {
|
|
if (mapTagNamesEnabled) {
|
|
const keyValue = (keysToCheck as KeyValue[]).find((keyValue: KeyValue) => keyValue.key === tag.key);
|
|
if (keyValue) {
|
|
acc.push(`${keyValue.value ? keyValue.value : keyValue.key}="${tag.value}"`);
|
|
}
|
|
} else {
|
|
if ((keysToCheck as string[]).includes(tag.key)) {
|
|
acc.push(`${tag.key}="${tag.value}"`);
|
|
}
|
|
}
|
|
return acc;
|
|
}, [] as string[]);
|
|
|
|
// If no tags found, return undefined to prevent an invalid Loki query
|
|
if (!tags.length) {
|
|
return undefined;
|
|
}
|
|
let expr = `{${tags.join(', ')}}`;
|
|
if (filterByTraceID && span.traceID) {
|
|
expr += ` |="${span.traceID}"`;
|
|
}
|
|
if (filterBySpanID && span.spanID) {
|
|
expr += ` |="${span.spanID}"`;
|
|
}
|
|
|
|
const dataLink: DataLink<LokiQuery> = {
|
|
title: dataSourceSettings.name,
|
|
url: '',
|
|
internal: {
|
|
datasourceUid: dataSourceSettings.uid,
|
|
datasourceName: dataSourceSettings.name,
|
|
query: {
|
|
expr: expr,
|
|
refId: '',
|
|
},
|
|
},
|
|
};
|
|
|
|
return dataLink;
|
|
}
|
|
|
|
function getLinkForSplunk(
|
|
span: TraceSpan,
|
|
options: TraceToLogsOptions,
|
|
dataSourceSettings: DataSourceInstanceSettings
|
|
) {
|
|
const { tags: keys, filterByTraceID, filterBySpanID, mapTagNamesEnabled, mappedTags } = options;
|
|
|
|
// In order, try to use mapped tags -> tags -> default tags
|
|
const keysToCheck = mapTagNamesEnabled && mappedTags?.length ? mappedTags : keys?.length ? keys : defaultKeys;
|
|
// Build tag portion of query
|
|
const tags = [...span.process.tags, ...span.tags].reduce((acc, tag) => {
|
|
if (mapTagNamesEnabled) {
|
|
const keyValue = (keysToCheck as KeyValue[]).find((keyValue: KeyValue) => keyValue.key === tag.key);
|
|
if (keyValue) {
|
|
acc.push(`${keyValue.value ? keyValue.value : keyValue.key}="${tag.value}"`);
|
|
}
|
|
} else {
|
|
if ((keysToCheck as string[]).includes(tag.key)) {
|
|
acc.push(`${tag.key}="${tag.value}"`);
|
|
}
|
|
}
|
|
return acc;
|
|
}, [] as string[]);
|
|
|
|
let query = '';
|
|
if (tags.length > 0) {
|
|
query += `${tags.join(' ')}`;
|
|
}
|
|
if (filterByTraceID && span.traceID) {
|
|
query += ` "${span.traceID}"`;
|
|
}
|
|
if (filterBySpanID && span.spanID) {
|
|
query += ` "${span.spanID}"`;
|
|
}
|
|
|
|
const dataLink: DataLink<DataQuery> = {
|
|
title: dataSourceSettings.name,
|
|
url: '',
|
|
internal: {
|
|
datasourceUid: dataSourceSettings.uid,
|
|
datasourceName: dataSourceSettings.name,
|
|
query: {
|
|
query: query,
|
|
refId: '',
|
|
},
|
|
},
|
|
} as DataLink<DataQuery>;
|
|
|
|
return dataLink;
|
|
}
|
|
|
|
/**
|
|
* Gets a time range from the span.
|
|
*/
|
|
function getTimeRangeFromSpan(
|
|
span: TraceSpan,
|
|
timeShift: { startMs: number; endMs: number } = { startMs: 0, endMs: 0 },
|
|
isSplunkDS = false
|
|
): TimeRange {
|
|
const adjustedStartTime = Math.floor(span.startTime / 1000 + timeShift.startMs);
|
|
const from = dateTime(adjustedStartTime);
|
|
const spanEndMs = (span.startTime + span.duration) / 1000;
|
|
let adjustedEndTime = Math.floor(spanEndMs + timeShift.endMs);
|
|
|
|
// Splunk requires a time interval of >= 1s, rather than >=1ms like Loki timerange in below elseif block
|
|
if (isSplunkDS && adjustedEndTime - adjustedStartTime < 1000) {
|
|
adjustedEndTime = adjustedStartTime + 1000;
|
|
} else if (adjustedStartTime === adjustedEndTime) {
|
|
// Because we can only pass milliseconds in the url we need to check if they equal.
|
|
// We need end time to be later than start time
|
|
adjustedEndTime++;
|
|
}
|
|
|
|
const to = dateTime(adjustedEndTime);
|
|
|
|
// Beware that public/app/features/explore/state/main.ts SplitOpen fn uses the range from here. No matter what is in the url.
|
|
return {
|
|
from,
|
|
to,
|
|
raw: {
|
|
from,
|
|
to,
|
|
},
|
|
};
|
|
}
|