createSpanLink.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import { SpanLinks } from '@jaegertracing/jaeger-ui-components/src/types/links';
  2. import React from 'react';
  3. import {
  4. DataFrame,
  5. DataLink,
  6. DataQuery,
  7. DataSourceInstanceSettings,
  8. DataSourceJsonData,
  9. dateTime,
  10. Field,
  11. KeyValue,
  12. LinkModel,
  13. mapInternalLinkToExplore,
  14. rangeUtil,
  15. SplitOpen,
  16. TimeRange,
  17. } from '@grafana/data';
  18. import { getTemplateSrv } from '@grafana/runtime';
  19. import { Icon } from '@grafana/ui';
  20. import { SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components';
  21. import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
  22. import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
  23. import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
  24. import { PromQuery } from 'app/plugins/datasource/prometheus/types';
  25. import { LokiQuery } from '../../../plugins/datasource/loki/types';
  26. import { getFieldLinksForExplore } from '../utils/links';
  27. /**
  28. * This is a factory for the link creator. It returns the function mainly so it can return undefined in which case
  29. * the trace view won't create any links and to capture the datasource and split function making it easier to memoize
  30. * with useMemo.
  31. */
  32. export function createSpanLinkFactory({
  33. splitOpenFn,
  34. traceToLogsOptions,
  35. traceToMetricsOptions,
  36. dataFrame,
  37. createFocusSpanLink,
  38. }: {
  39. splitOpenFn: SplitOpen;
  40. traceToLogsOptions?: TraceToLogsOptions;
  41. traceToMetricsOptions?: TraceToMetricsOptions;
  42. dataFrame?: DataFrame;
  43. createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
  44. }): SpanLinkFunc | undefined {
  45. if (!dataFrame || dataFrame.fields.length === 1 || !dataFrame.fields.some((f) => Boolean(f.config.links?.length))) {
  46. // if the dataframe contains just a single blob of data (legacy format) or does not have any links configured,
  47. // let's try to use the old legacy path.
  48. return legacyCreateSpanLinkFactory(splitOpenFn, traceToLogsOptions, traceToMetricsOptions, createFocusSpanLink);
  49. } else {
  50. return function SpanLink(span: TraceSpan): SpanLinks | undefined {
  51. // We should be here only if there are some links in the dataframe
  52. const field = dataFrame.fields.find((f) => Boolean(f.config.links?.length))!;
  53. try {
  54. const links = getFieldLinksForExplore({
  55. field,
  56. rowIndex: span.dataFrameRowIndex!,
  57. splitOpenFn,
  58. range: getTimeRangeFromSpan(span),
  59. dataFrame,
  60. });
  61. return {
  62. logLinks: [
  63. {
  64. href: links[0].href,
  65. onClick: links[0].onClick,
  66. content: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
  67. },
  68. ],
  69. };
  70. } catch (error) {
  71. // It's fairly easy to crash here for example if data source defines wrong interpolation in the data link
  72. console.error(error);
  73. return undefined;
  74. }
  75. };
  76. }
  77. }
  78. function legacyCreateSpanLinkFactory(
  79. splitOpenFn: SplitOpen,
  80. traceToLogsOptions?: TraceToLogsOptions,
  81. traceToMetricsOptions?: TraceToMetricsOptions,
  82. createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>
  83. ) {
  84. let logsDataSourceSettings: DataSourceInstanceSettings<DataSourceJsonData> | undefined;
  85. if (traceToLogsOptions?.datasourceUid) {
  86. logsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToLogsOptions.datasourceUid);
  87. }
  88. const isSplunkDS = logsDataSourceSettings?.type === 'grafana-splunk-datasource';
  89. let metricsDataSourceSettings: DataSourceInstanceSettings<DataSourceJsonData> | undefined;
  90. if (traceToMetricsOptions?.datasourceUid) {
  91. metricsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToMetricsOptions.datasourceUid);
  92. }
  93. return function SpanLink(span: TraceSpan): SpanLinks {
  94. const links: SpanLinks = { traceLinks: [] };
  95. // This is reusing existing code from derived fields which may not be ideal match so some data is a bit faked at
  96. // the moment. Issue is that the trace itself isn't clearly mapped to dataFrame (right now it's just a json blob
  97. // inside a single field) so the dataLinks as config of that dataFrame abstraction breaks down a bit and we do
  98. // it manually here instead of leaving it for the data source to supply the config.
  99. let dataLink: DataLink<LokiQuery | DataQuery> | undefined = {} as DataLink<LokiQuery | DataQuery> | undefined;
  100. // Get logs link
  101. if (logsDataSourceSettings && traceToLogsOptions) {
  102. switch (logsDataSourceSettings?.type) {
  103. case 'loki':
  104. dataLink = getLinkForLoki(span, traceToLogsOptions, logsDataSourceSettings);
  105. break;
  106. case 'grafana-splunk-datasource':
  107. dataLink = getLinkForSplunk(span, traceToLogsOptions, logsDataSourceSettings);
  108. break;
  109. }
  110. if (dataLink) {
  111. const link = mapInternalLinkToExplore({
  112. link: dataLink,
  113. internalLink: dataLink.internal!,
  114. scopedVars: {},
  115. range: getTimeRangeFromSpan(
  116. span,
  117. {
  118. startMs: traceToLogsOptions.spanStartTimeShift
  119. ? rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift)
  120. : 0,
  121. endMs: traceToLogsOptions.spanEndTimeShift
  122. ? rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift)
  123. : 0,
  124. },
  125. isSplunkDS
  126. ),
  127. field: {} as Field,
  128. onClickFn: splitOpenFn,
  129. replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
  130. });
  131. links.logLinks = [
  132. {
  133. href: link.href,
  134. onClick: link.onClick,
  135. content: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
  136. },
  137. ];
  138. }
  139. }
  140. // Get metrics links
  141. if (metricsDataSourceSettings && traceToMetricsOptions?.queries) {
  142. const defaultQuery = `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`;
  143. links.metricLinks = [];
  144. for (const query of traceToMetricsOptions.queries) {
  145. const dataLink: DataLink<PromQuery> = {
  146. title: metricsDataSourceSettings.name,
  147. url: '',
  148. internal: {
  149. datasourceUid: metricsDataSourceSettings.uid,
  150. datasourceName: metricsDataSourceSettings.name,
  151. query: {
  152. expr: query.query || defaultQuery,
  153. refId: 'A',
  154. },
  155. },
  156. };
  157. const link = mapInternalLinkToExplore({
  158. link: dataLink,
  159. internalLink: dataLink.internal!,
  160. scopedVars: {},
  161. range: getTimeRangeFromSpan(span, {
  162. startMs: 0,
  163. endMs: 0,
  164. }),
  165. field: {} as Field,
  166. onClickFn: splitOpenFn,
  167. replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
  168. });
  169. links.metricLinks.push({
  170. title: query?.name,
  171. href: link.href,
  172. onClick: link.onClick,
  173. content: <Icon name="chart-line" title="Explore metrics for this span" />,
  174. });
  175. }
  176. }
  177. // Get trace links
  178. if (span.references && createFocusSpanLink) {
  179. for (const reference of span.references) {
  180. // Ignore parent-child links
  181. if (reference.refType === 'CHILD_OF') {
  182. continue;
  183. }
  184. const link = createFocusSpanLink(reference.traceID, reference.spanID);
  185. links.traceLinks!.push({
  186. href: link.href,
  187. title: reference.span ? reference.span.operationName : 'View linked span',
  188. content: <Icon name="link" title="View linked span" />,
  189. onClick: link.onClick,
  190. });
  191. }
  192. }
  193. if (span.subsidiarilyReferencedBy && createFocusSpanLink) {
  194. for (const reference of span.subsidiarilyReferencedBy) {
  195. const link = createFocusSpanLink(reference.traceID, reference.spanID);
  196. links.traceLinks!.push({
  197. href: link.href,
  198. title: reference.span ? reference.span.operationName : 'View linked span',
  199. content: <Icon name="link" title="View linked span" />,
  200. onClick: link.onClick,
  201. });
  202. }
  203. }
  204. return links;
  205. };
  206. }
  207. /**
  208. * Default keys to use when there are no configured tags.
  209. */
  210. const defaultKeys = ['cluster', 'hostname', 'namespace', 'pod'];
  211. function getLinkForLoki(span: TraceSpan, options: TraceToLogsOptions, dataSourceSettings: DataSourceInstanceSettings) {
  212. const { tags: keys, filterByTraceID, filterBySpanID, mapTagNamesEnabled, mappedTags } = options;
  213. // In order, try to use mapped tags -> tags -> default tags
  214. const keysToCheck = mapTagNamesEnabled && mappedTags?.length ? mappedTags : keys?.length ? keys : defaultKeys;
  215. // Build tag portion of query
  216. const tags = [...span.process.tags, ...span.tags].reduce((acc, tag) => {
  217. if (mapTagNamesEnabled) {
  218. const keyValue = (keysToCheck as KeyValue[]).find((keyValue: KeyValue) => keyValue.key === tag.key);
  219. if (keyValue) {
  220. acc.push(`${keyValue.value ? keyValue.value : keyValue.key}="${tag.value}"`);
  221. }
  222. } else {
  223. if ((keysToCheck as string[]).includes(tag.key)) {
  224. acc.push(`${tag.key}="${tag.value}"`);
  225. }
  226. }
  227. return acc;
  228. }, [] as string[]);
  229. // If no tags found, return undefined to prevent an invalid Loki query
  230. if (!tags.length) {
  231. return undefined;
  232. }
  233. let expr = `{${tags.join(', ')}}`;
  234. if (filterByTraceID && span.traceID) {
  235. expr += ` |="${span.traceID}"`;
  236. }
  237. if (filterBySpanID && span.spanID) {
  238. expr += ` |="${span.spanID}"`;
  239. }
  240. const dataLink: DataLink<LokiQuery> = {
  241. title: dataSourceSettings.name,
  242. url: '',
  243. internal: {
  244. datasourceUid: dataSourceSettings.uid,
  245. datasourceName: dataSourceSettings.name,
  246. query: {
  247. expr: expr,
  248. refId: '',
  249. },
  250. },
  251. };
  252. return dataLink;
  253. }
  254. function getLinkForSplunk(
  255. span: TraceSpan,
  256. options: TraceToLogsOptions,
  257. dataSourceSettings: DataSourceInstanceSettings
  258. ) {
  259. const { tags: keys, filterByTraceID, filterBySpanID, mapTagNamesEnabled, mappedTags } = options;
  260. // In order, try to use mapped tags -> tags -> default tags
  261. const keysToCheck = mapTagNamesEnabled && mappedTags?.length ? mappedTags : keys?.length ? keys : defaultKeys;
  262. // Build tag portion of query
  263. const tags = [...span.process.tags, ...span.tags].reduce((acc, tag) => {
  264. if (mapTagNamesEnabled) {
  265. const keyValue = (keysToCheck as KeyValue[]).find((keyValue: KeyValue) => keyValue.key === tag.key);
  266. if (keyValue) {
  267. acc.push(`${keyValue.value ? keyValue.value : keyValue.key}="${tag.value}"`);
  268. }
  269. } else {
  270. if ((keysToCheck as string[]).includes(tag.key)) {
  271. acc.push(`${tag.key}="${tag.value}"`);
  272. }
  273. }
  274. return acc;
  275. }, [] as string[]);
  276. let query = '';
  277. if (tags.length > 0) {
  278. query += `${tags.join(' ')}`;
  279. }
  280. if (filterByTraceID && span.traceID) {
  281. query += ` "${span.traceID}"`;
  282. }
  283. if (filterBySpanID && span.spanID) {
  284. query += ` "${span.spanID}"`;
  285. }
  286. const dataLink: DataLink<DataQuery> = {
  287. title: dataSourceSettings.name,
  288. url: '',
  289. internal: {
  290. datasourceUid: dataSourceSettings.uid,
  291. datasourceName: dataSourceSettings.name,
  292. query: {
  293. query: query,
  294. refId: '',
  295. },
  296. },
  297. } as DataLink<DataQuery>;
  298. return dataLink;
  299. }
  300. /**
  301. * Gets a time range from the span.
  302. */
  303. function getTimeRangeFromSpan(
  304. span: TraceSpan,
  305. timeShift: { startMs: number; endMs: number } = { startMs: 0, endMs: 0 },
  306. isSplunkDS = false
  307. ): TimeRange {
  308. const adjustedStartTime = Math.floor(span.startTime / 1000 + timeShift.startMs);
  309. const from = dateTime(adjustedStartTime);
  310. const spanEndMs = (span.startTime + span.duration) / 1000;
  311. let adjustedEndTime = Math.floor(spanEndMs + timeShift.endMs);
  312. // Splunk requires a time interval of >= 1s, rather than >=1ms like Loki timerange in below elseif block
  313. if (isSplunkDS && adjustedEndTime - adjustedStartTime < 1000) {
  314. adjustedEndTime = adjustedStartTime + 1000;
  315. } else if (adjustedStartTime === adjustedEndTime) {
  316. // Because we can only pass milliseconds in the url we need to check if they equal.
  317. // We need end time to be later than start time
  318. adjustedEndTime++;
  319. }
  320. const to = dateTime(adjustedEndTime);
  321. // Beware that public/app/features/explore/state/main.ts SplitOpen fn uses the range from here. No matter what is in the url.
  322. return {
  323. from,
  324. to,
  325. raw: {
  326. from,
  327. to,
  328. },
  329. };
  330. }