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; }): 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: , }, ], }; } 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 ) { let logsDataSourceSettings: DataSourceInstanceSettings | undefined; if (traceToLogsOptions?.datasourceUid) { logsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToLogsOptions.datasourceUid); } const isSplunkDS = logsDataSourceSettings?.type === 'grafana-splunk-datasource'; let metricsDataSourceSettings: DataSourceInstanceSettings | 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 | undefined = {} as DataLink | 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: , }, ]; } } // 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 = { 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: , }); } } // 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: , 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: , 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 = { 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 = { title: dataSourceSettings.name, url: '', internal: { datasourceUid: dataSourceSettings.uid, datasourceName: dataSourceSettings.name, query: { query: query, refId: '', }, }, } as DataLink; 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, }, }; }