diff --git a/public/app/features/correlations/transformations.ts b/public/app/features/correlations/transformations.ts index 6754d299545..11320ad8a10 100644 --- a/public/app/features/correlations/transformations.ts +++ b/public/app/features/correlations/transformations.ts @@ -12,7 +12,9 @@ export const getTransformationVars = ( let transformVal: { [key: string]: string | boolean | null | undefined } = {}; if (transformation.type === SupportedTransformationType.Regex && transformation.expression) { const regexp = new RegExp(transformation.expression, 'gi'); - const matches = fieldValue.matchAll(regexp); + const stringFieldVal = typeof fieldValue === 'string' ? fieldValue : safeStringifyValue(fieldValue); + + const matches = stringFieldVal.matchAll(regexp); for (const match of matches) { if (match.groups) { transformVal = match.groups; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx index a1ef4b8296d..6d43873cadf 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.test.tsx @@ -17,8 +17,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { NONE, DURATION, TAG } from '../settings/SpanBarSettings'; -import { TraceSpan } from '../types'; -import { SpanLinks } from '../types/links'; +import { SpanLinkDef, TraceSpan } from '../types'; import SpanBarRow, { SpanBarRowProps } from './SpanBarRow'; @@ -111,11 +110,7 @@ describe('', () => { - ({ - traceLinks: [{ href: 'href' }, { href: 'href' }], - } as SpanLinks) - } + createSpanLink={() => [{ href: 'href' }, { href: 'href' }] as SpanLinkDef[]} /> ); expect(screen.getAllByTestId('SpanLinksMenu')).toHaveLength(1); @@ -142,11 +137,7 @@ describe('', () => { - ({ - traceLinks: [{ content: 'This span is referenced by another span', href: 'href' }], - } as SpanLinks) - } + createSpanLink={() => [{ content: 'This span is referenced by another span', href: 'href' }] as SpanLinkDef[]} /> ); expect(screen.getByRole('link', { name: 'This span is referenced by another span' })).toBeInTheDocument(); @@ -181,11 +172,7 @@ describe('', () => { - ({ - traceLinks: [{ href: 'href' }, { href: 'href' }], - } as SpanLinks) - } + createSpanLink={() => [{ href: 'href' }, { href: 'href' }] as SpanLinkDef[]} /> ); expect(screen.getAllByTestId('SpanLinksMenu')).toHaveLength(1); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx index 7449aae04fc..ce163c74168 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanBarRow.tsx @@ -22,7 +22,6 @@ import { Icon, stylesFactory, withTheme2 } from '@grafana/ui'; import { autoColor } from '../Theme'; import { DURATION, NONE, TAG } from '../settings/SpanBarSettings'; import { SpanBarOptions, SpanLinkFunc, TraceSpan, TNil } from '../types'; -import { SpanLinks } from '../types/links'; import SpanBar from './SpanBar'; import { SpanLinksMenu } from './SpanLinks'; @@ -404,14 +403,6 @@ export class UnthemedSpanBarRow extends React.PureComponent { hintClassName = styles.labelRight; } - const countLinks = (links?: SpanLinks): number => { - if (!links) { - return 0; - } - - return Object.values(links).reduce((count, arr) => count + arr.length, 0); - }; - return ( { {createSpanLink && (() => { const links = createSpanLink(span); - const count = countLinks(links); + const count = links?.length || 0; if (links && count === 1) { - const link = links.logLinks?.[0] ?? links.metricLinks?.[0] ?? links.traceLinks?.[0] ?? undefined; - if (!link) { + if (!links[0]) { return null; } return ( { - if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) { + if (!(event.ctrlKey || event.metaKey || event.shiftKey) && links[0].onClick) { event.preventDefault(); - link.onClick(event); + links[0].onClick(event); } } : undefined } > - {link.content} + {links[0].content} ); } else if (links && count > 1) { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index cac62dc44c0..a1f12e38130 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -24,6 +24,7 @@ import { autoColor } from '../../Theme'; import { Divider } from '../../common/Divider'; import LabeledList from '../../common/LabeledList'; import { SpanLinkFunc, TNil } from '../../types'; +import { SpanLinkType } from '../../types/links'; import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan, TraceSpanReference } from '../../types/trace'; import { uAlignIcon, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles'; import { TopOfViewRefType } from '../VirtualizedTraceView'; @@ -197,14 +198,15 @@ export default function SpanDetail(props: SpanDetailProps) { let logLinkButton: JSX.Element | undefined = undefined; if (createSpanLink) { const links = createSpanLink(span); - if (links?.logLinks) { + const logLinks = links?.filter((link) => link.type === SpanLinkType.Logs); + if (links && logLinks && logLinks.length > 0) { logLinkButton = ( { reportInteraction('grafana_traces_trace_view_span_link_clicked', { datasourceType: datasourceType, @@ -212,7 +214,7 @@ export default function SpanDetail(props: SpanDetailProps) { type: 'log', location: 'spanDetails', }); - links?.logLinks?.[0].onClick?.(event); + logLinks?.[0].onClick?.(event); }, }} buttonProps={{ icon: 'gf-logs' }} diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx index 666470f1242..7cf3ab67c25 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanLinks.tsx @@ -2,106 +2,48 @@ import { css } from '@emotion/css'; import React, { useState } from 'react'; import { config, reportInteraction } from '@grafana/runtime'; -import { useStyles2, MenuGroup, MenuItem, Icon, ContextMenu } from '@grafana/ui'; +import { useStyles2, MenuItem, Icon, ContextMenu } from '@grafana/ui'; -import { SpanLinks } from '../types/links'; +import { SpanLinkDef } from '../types/links'; interface SpanLinksProps { - links: SpanLinks; + links: SpanLinkDef[]; datasourceType: string; } const renderMenuItems = ( - links: SpanLinks, + links: SpanLinkDef[], styles: ReturnType, closeMenu: () => void, datasourceType: string ) => { - return ( - <> - {!!links.logLinks?.length ? ( - - {links.logLinks.map((link, i) => ( - { - reportInteraction('grafana_traces_trace_view_span_link_clicked', { - datasourceType: datasourceType, - grafana_version: config.buildInfo.version, - type: 'log', - location: 'menu', - }); - event?.preventDefault(); - link.onClick!(event); - closeMenu(); - } - : undefined - } - url={link.href} - className={styles.menuItem} - /> - ))} - - ) : null} - {!!links.metricLinks?.length ? ( - - {links.metricLinks.map((link, i) => ( - { - reportInteraction('grafana_traces_trace_view_span_link_clicked', { - datasourceType: datasourceType, - grafana_version: config.buildInfo.version, - type: 'metric', - location: 'menu', - }); - event?.preventDefault(); - link.onClick!(event); - closeMenu(); - } - : undefined - } - url={link.href} - className={styles.menuItem} - /> - ))} - - ) : null} - {!!links.traceLinks?.length ? ( - - {links.traceLinks.map((link, i) => ( - { - reportInteraction('grafana_traces_trace_view_span_link_clicked', { - datasourceType: datasourceType, - grafana_version: config.buildInfo.version, - type: 'trace', - location: 'menu', - }); - event?.preventDefault(); - link.onClick!(event); - closeMenu(); - } - : undefined - } - url={link.href} - className={styles.menuItem} - /> - ))} - - ) : null} - - ); + links.sort(function (linkA, linkB) { + return (linkA.title || 'link').toLowerCase().localeCompare((linkB.title || 'link').toLowerCase()); + }); + + return links.map((link, i) => ( + { + reportInteraction(`grafana_traces_trace_view_span_link_clicked`, { + datasourceType: datasourceType, + grafana_version: config.buildInfo.version, + type: link.type, + location: 'menu', + }); + event?.preventDefault(); + link.onClick!(event); + closeMenu(); + } + : undefined + } + url={link.href} + className={styles.menuItem} + /> + )); }; export const SpanLinksMenu = ({ links, datasourceType }: SpanLinksProps) => { @@ -130,7 +72,7 @@ export const SpanLinksMenu = ({ links, datasourceType }: SpanLinksProps) => { setIsMenuOpen(false)} renderMenuItems={() => renderMenuItems(links, styles, closeMenu, datasourceType)} - focusOnOpen={true} + focusOnOpen={false} x={menuPosition.x} y={menuPosition.y} /> diff --git a/public/app/features/explore/TraceView/components/types/links.ts b/public/app/features/explore/TraceView/components/types/links.ts index 8e375b2f0d3..3a2e5f05e0e 100644 --- a/public/app/features/explore/TraceView/components/types/links.ts +++ b/public/app/features/explore/TraceView/components/types/links.ts @@ -4,18 +4,20 @@ import { Field } from '@grafana/data'; import { TraceSpan } from './trace'; +export enum SpanLinkType { + Logs = 'log', + Traces = 'trace', + Metrics = 'metric', + Unknown = 'unknown', +} + export type SpanLinkDef = { href: string; onClick?: (event: unknown) => void; content: React.ReactNode; title?: string; field: Field; + type: SpanLinkType; }; -export type SpanLinks = { - logLinks?: SpanLinkDef[]; - traceLinks?: SpanLinkDef[]; - metricLinks?: SpanLinkDef[]; -}; - -export type SpanLinkFunc = (span: TraceSpan) => SpanLinks | undefined; +export type SpanLinkFunc = (span: TraceSpan) => SpanLinkDef[] | undefined; diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 0a25952d025..d9d9c35c55d 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -1,4 +1,11 @@ -import { DataSourceInstanceSettings, LinkModel, MutableDataFrame } from '@grafana/data'; +import { + DataSourceInstanceSettings, + LinkModel, + createDataFrame, + SupportedTransformationType, + DataLinkConfigOrigin, + FieldType, +} from '@grafana/data'; import { DataSourceSrv, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime'; import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -8,10 +15,17 @@ import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv'; import { TemplateSrv } from '../../templating/template_srv'; import { Trace, TraceSpan } from './components'; +import { SpanLinkType } from './components/types/links'; import { createSpanLinkFactory } from './createSpanLink'; const dummyTraceData = { duration: 10, traceID: 'trace1', traceName: 'test trace' } as unknown as Trace; -const dummyDataFrame = new MutableDataFrame({ fields: [{ name: 'traceId', values: ['trace1'] }] }); +const dummyDataFrame = createDataFrame({ fields: [{ name: 'traceId', values: ['trace1'] }] }); + +jest.mock('app/core/services/context_srv', () => ({ + contextSrv: { + hasAccessToExplore: () => true, + }, +})); describe('createSpanLinkFactory', () => { it('returns no links if there is no data source uid', () => { @@ -22,9 +36,8 @@ describe('createSpanLinkFactory', () => { dataFrame: dummyDataFrame, }); const links = createLink!(createTraceSpan()); - expect(links?.logLinks).toBeUndefined(); - expect(links?.metricLinks).toBeUndefined(); - expect(links?.traceLinks).toHaveLength(0); + expect(links).toBeDefined(); + expect(links).toHaveLength(0); }); describe('should return loki link', () => { @@ -43,8 +56,9 @@ describe('createSpanLinkFactory', () => { const createLink = setupSpanLinkFactory(); expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}]}' @@ -68,8 +82,9 @@ describe('createSpanLinkFactory', () => { }, }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{ip=\\"192.168.0.1\\"}","refId":""}]}' @@ -93,8 +108,9 @@ describe('createSpanLinkFactory', () => { }, }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{ip=\\"192.168.0.1\\", host=\\"host\\"}","refId":""}]}' @@ -119,8 +135,9 @@ describe('createSpanLinkFactory', () => { }, }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:01:00.000Z","to":"2020-10-14T01:01:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{hostname=\\"hostname1\\"}","refId":""}]}' @@ -136,8 +153,9 @@ describe('createSpanLinkFactory', () => { expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toBe( '/explore?left=' + JSON.stringify({ @@ -157,7 +175,7 @@ describe('createSpanLinkFactory', () => { const splitOpenFn = jest.fn(); const createLink = createSpanLinkFactory({ splitOpenFn, - dataFrame: new MutableDataFrame({ + dataFrame: createDataFrame({ fields: [ { name: 'traceID', values: ['testTraceId'] }, { @@ -172,8 +190,9 @@ describe('createSpanLinkFactory', () => { expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Unknown); expect(linkDef!.href).toBe('testSpanId'); }); @@ -197,8 +216,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{service=\\"serviceName\\", pod=\\"podName\\"}","refId":""}]}' @@ -226,8 +246,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{service.name=\\"serviceName\\", pod=\\"podName\\"}","refId":""}]}' @@ -251,7 +272,8 @@ describe('createSpanLinkFactory', () => { }, }) ); - expect(links?.logLinks).toBeUndefined(); + expect(links).toBeDefined(); + expect(links?.length).toEqual(0); }); it('interpolates span intrinsics', () => { @@ -260,8 +282,9 @@ describe('createSpanLinkFactory', () => { }); expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - expect(links?.logLinks).toBeDefined(); - expect(decodeURIComponent(links!.logLinks![0].href)).toContain('spanName=\\"operation\\"'); + expect(links).toBeDefined(); + expect(links![0].type).toBe(SpanLinkType.Logs); + expect(decodeURIComponent(links![0].href)).toContain('spanName=\\"operation\\"'); }); }); @@ -289,8 +312,9 @@ describe('createSpanLinkFactory', () => { }); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toContain(`${encodeURIComponent('datasource":"splunkUID","queries":[{"query"')}`); expect(linkDef!.href).not.toContain(`${encodeURIComponent('datasource":"splunkUID","queries":[{"expr"')}`); }); @@ -301,8 +325,9 @@ describe('createSpanLinkFactory', () => { }); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toContain( `${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}` ); @@ -321,8 +346,9 @@ describe('createSpanLinkFactory', () => { expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}]}' @@ -344,8 +370,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}]}' @@ -370,8 +397,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"hostname=\\"hostname1\\" ip=\\"192.168.0.1\\"","refId":""}]}' @@ -399,8 +427,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"service=\\"serviceName\\" pod=\\"podName\\"","refId":""}]}' @@ -435,9 +464,9 @@ describe('createSpanLinkFactory', () => { expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - const linkDef = links?.metricLinks?.[0]; - + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Metrics); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}' @@ -458,7 +487,8 @@ describe('createSpanLinkFactory', () => { expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - expect(links?.metricLinks).toBeUndefined(); + expect(links).toBeDefined(); + expect(links?.length).toEqual(0); }); it('returns multiple queries including default', () => { @@ -479,11 +509,12 @@ describe('createSpanLinkFactory', () => { expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - expect(links?.metricLinks).toBeDefined(); - expect(links?.metricLinks).toHaveLength(3); + expect(links).toBeDefined(); + expect(links).toHaveLength(3); - const namedLink = links?.metricLinks?.[0]; + const namedLink = links?.[0]; expect(namedLink).toBeDefined(); + expect(namedLink?.type).toBe(SpanLinkType.Metrics); expect(namedLink!.title).toBe('Named Query'); expect(namedLink!.href).toBe( `/explore?left=${encodeURIComponent( @@ -491,8 +522,9 @@ describe('createSpanLinkFactory', () => { )}` ); - const defaultLink = links?.metricLinks?.[1]; + const defaultLink = links?.[1]; expect(defaultLink).toBeDefined(); + expect(defaultLink?.type).toBe(SpanLinkType.Metrics); expect(defaultLink!.title).toBe('defaultQuery'); expect(defaultLink!.href).toBe( `/explore?left=${encodeURIComponent( @@ -500,8 +532,9 @@ describe('createSpanLinkFactory', () => { )}` ); - const unnamedQuery = links?.metricLinks?.[2]; + const unnamedQuery = links?.[2]; expect(unnamedQuery).toBeDefined(); + expect(unnamedQuery?.type).toBe(SpanLinkType.Metrics); expect(unnamedQuery!.title).toBeUndefined(); expect(unnamedQuery!.href).toBe( `/explore?left=${encodeURIComponent( @@ -526,9 +559,9 @@ describe('createSpanLinkFactory', () => { expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - const linkDef = links?.metricLinks?.[0]; - + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Metrics); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T00:00:00.000Z","to":"2020-10-14T02:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}' @@ -566,7 +599,8 @@ describe('createSpanLinkFactory', () => { }) ); expect(links).toBeDefined(); - expect(links!.metricLinks![0]!.href).toBe( + expect(links![0].type).toBe(SpanLinkType.Metrics); + expect(links![0].href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"metric{job=\\"tns/app\\", pod=\\"sample-pod\\", job=\\"tns/app\\", pod=\\"sample-pod\\"}[5m]","refId":"A"}]}' )}` @@ -587,7 +621,7 @@ describe('createSpanLinkFactory', () => { createTraceSpan({ references: [{ refType: 'CHILD_OF', spanID: 'parent', traceID: 'traceID' }] }) ); - const traceLinks = links?.traceLinks; + const traceLinks = links; expect(traceLinks).toBeDefined(); expect(traceLinks).toHaveLength(0); }); @@ -609,9 +643,12 @@ describe('createSpanLinkFactory', () => { }) ); - const traceLinks = links?.traceLinks; + const traceLinks = links; expect(traceLinks).toBeDefined(); expect(traceLinks).toHaveLength(2); + expect(traceLinks![0].type).toBe(SpanLinkType.Traces); + expect(traceLinks![1].type).toBe(SpanLinkType.Traces); + expect(traceLinks![0]).toEqual( expect.objectContaining({ href: 'traceID-span1', @@ -651,8 +688,9 @@ describe('createSpanLinkFactory', () => { }); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toContain( `datasource":"${searchUID}","queries":[{"query":"cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]` ); @@ -664,8 +702,9 @@ describe('createSpanLinkFactory', () => { }); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toContain( `${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}` ); @@ -687,8 +726,9 @@ describe('createSpanLinkFactory', () => { expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` @@ -715,8 +755,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toBe( `/explore?left={"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"searchUID","queries":[{"query":"\\"7946b05c2e2e4e5a\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` ); @@ -739,8 +780,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"ip:\\"192.168.0.1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` @@ -768,8 +810,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"hostname:\\"hostname1\\" AND ip:\\"192.168.0.1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` @@ -800,8 +843,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"service:\\"serviceName\\" AND pod:\\"podName\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` @@ -834,8 +878,9 @@ describe('createSpanLinkFactory', () => { }); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toContain( `datasource":"${searchUID}","queries":[{"query":"cluster=\\"cluster1\\" AND hostname=\\"hostname1\\"","refId":""}]` ); @@ -847,8 +892,9 @@ describe('createSpanLinkFactory', () => { }); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toContain( `${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}` ); @@ -870,8 +916,9 @@ describe('createSpanLinkFactory', () => { expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster=\\"cluster1\\" AND hostname=\\"hostname1\\"","refId":""}]}` @@ -898,8 +945,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toBe( `/explore?left={"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"searchUID","queries":[{"query":"\\"7946b05c2e2e4e5a\\"","refId":""}]}` ); @@ -922,8 +970,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}]}` @@ -951,8 +1000,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"hostname=\\"hostname1\\" AND ip=\\"192.168.0.1\\"","refId":""}]}` @@ -983,8 +1033,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"service=\\"serviceName\\" AND pod=\\"podName\\"","refId":""}]}` @@ -1027,8 +1078,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toContain( '"queries":' + JSON.stringify([{ expr: '{service="serviceName", pod="podName"} |="serviceName" |="trace1"', refId: '' }]) @@ -1043,7 +1095,8 @@ describe('createSpanLinkFactory', () => { }); expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - expect(links?.logLinks).toBeUndefined(); + expect(links).toBeDefined(); + expect(links?.length).toEqual(0); }); }); @@ -1071,8 +1124,9 @@ describe('createSpanLinkFactory', () => { }); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toContain(`${encodeURIComponent('datasource":"falconLogScaleUID","queries":[{"lsql"')}`); }); @@ -1086,8 +1140,9 @@ describe('createSpanLinkFactory', () => { expect(createLink).toBeDefined(); const links = createLink!(createTraceSpan()); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"cluster=\\"cluster1\\" OR hostname=\\"hostname1\\" or \\"7946b05c2e2e4e5a\\" or \\"6605c7b08e715d6c\\"","refId":""}]}' @@ -1109,8 +1164,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"ip=\\"192.168.0.1\\"","refId":""}]}' @@ -1135,8 +1191,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"hostname=\\"hostname1\\" OR ip=\\"192.168.0.1\\"","refId":""}]}' @@ -1164,8 +1221,9 @@ describe('createSpanLinkFactory', () => { }) ); - const linkDef = links?.logLinks?.[0]; + const linkDef = links?.[0]; expect(linkDef).toBeDefined(); + expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"service=\\"serviceName\\" OR pod=\\"podName\\"","refId":""}]}' @@ -1175,6 +1233,47 @@ describe('createSpanLinkFactory', () => { }); }); +describe('dataFrame links', () => { + beforeAll(() => { + setDataSourceSrv({ + getInstanceSettings() { + return { uid: 'loki1_uid', name: 'loki1', type: 'loki' } as unknown as DataSourceInstanceSettings; + }, + } as unknown as DataSourceSrv); + + setLinkSrv(new LinkSrv()); + setTemplateSrv(new TemplateSrv()); + }); + + it('creates multiple span links for the dataframe links', () => { + const multiLinkDataFrame = createMultiLinkDataFrame(); + const splitOpenFn = jest.fn(); + const createLink = createSpanLinkFactory({ + splitOpenFn, + dataFrame: multiLinkDataFrame, + trace: dummyTraceData, + }); + + const links = createLink!(createTraceSpan()); + expect(links).toBeDefined(); + expect(links?.length).toEqual(3); + expect(links![0].href).toBe('testSpanId'); + expect(links![0].type).toBe(SpanLinkType.Unknown); + expect(links![1].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"message":"SELECT * FROM superhero WHERE name=host"}]}' + )}` + ); + expect(links![1].type).toBe(SpanLinkType.Unknown); + expect(links![2].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"go_memstats_heap_inuse_bytes{job=\'host\'}"}]}' + )}` + ); + expect(links![2].type).toBe(SpanLinkType.Unknown); + }); +}); + function setupSpanLinkFactory(options: Partial = {}, datasourceUid = 'lokiUid') { const splitOpenFn = jest.fn(); return createSpanLinkFactory({ @@ -1232,3 +1331,68 @@ function createTraceSpan(overrides: Partial = {}) { ...overrides, } as TraceSpan; } + +function createMultiLinkDataFrame() { + return createDataFrame({ + fields: [ + { name: 'traceID', values: ['testTraceId'] }, + { + name: 'spanID', + config: { links: [{ title: 'link', url: '${__data.fields.spanID}' }] }, + values: ['testSpanId'], + }, + { + name: 'tags', + type: FieldType.other, + config: { + links: [ + { + internal: { + query: { + message: 'SELECT * FROM superhero WHERE name=${job}', + }, + datasourceUid: 'loki1_uid', + datasourceName: 'loki1', + transformations: [ + { + type: SupportedTransformationType.Regex, + expression: '{(?=[^\\}]*\\bkey":"host")[^\\}]*\\bvalue":"(.*?)".*}', + mapValue: 'job', + }, + ], + }, + url: '', + title: 'Test', + origin: DataLinkConfigOrigin.Correlations, + }, + { + internal: { + query: { + expr: "go_memstats_heap_inuse_bytes{job='${job}'}", + }, + datasourceUid: 'loki1_uid', + datasourceName: 'loki1', + transformations: [ + { + type: SupportedTransformationType.Regex, + expression: '{(?=[^\\}]*\\bkey":"host")[^\\}]*\\bvalue":"(.*?)".*}', + mapValue: 'job', + }, + ], + }, + url: '', + title: 'Test2', + origin: DataLinkConfigOrigin.Correlations, + }, + ], + }, + values: [ + { + key: 'host', + value: 'host', + }, + ], + }, + ], + }); +} diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index d6cf1ce1dcb..26f1ecb1c1e 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -23,10 +23,10 @@ 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, getVariableUsageInfo } from '../utils/links'; +import { ExploreFieldLinkModel, getFieldLinksForExplore, getVariableUsageInfo } from '../utils/links'; -import { SpanLinkFunc, Trace, TraceSpan } from './components'; -import { SpanLinks } from './components/types/links'; +import { SpanLinkDef, SpanLinkFunc, Trace, TraceSpan } from './components'; +import { SpanLinkType } from './components/types/links'; /** * This is a factory for the link creator. It returns the function mainly so it can return undefined in which case @@ -54,60 +54,62 @@ export function createSpanLinkFactory({ let scopedVars = scopedVarsFromTrace(trace); const hasLinks = dataFrame.fields.some((f) => Boolean(f.config.links?.length)); - const legacyFormat = dataFrame.fields.length === 1; - if (legacyFormat || !hasLinks) { - // 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. - // TODO: This was mainly a backward compatibility thing but at this point can probably be removed. - return legacyCreateSpanLinkFactory( - splitOpenFn, - // We need this to make the types happy but for this branch of code it does not matter which field we supply. - dataFrame.fields[0], - traceToLogsOptions, - traceToMetricsOptions, - createFocusSpanLink, - scopedVars - ); - } + const createSpanLinks = legacyCreateSpanLinkFactory( + splitOpenFn, + // We need this to make the types happy but for this branch of code it does not matter which field we supply. + dataFrame.fields[0], + traceToLogsOptions, + traceToMetricsOptions, + createFocusSpanLink, + scopedVars + ); - if (hasLinks) { - return function SpanLink(span: TraceSpan): SpanLinks | undefined { + return function SpanLink(span: TraceSpan): SpanLinkDef[] | undefined { + let spanLinks = createSpanLinks(span); + + if (hasLinks) { scopedVars = { ...scopedVars, ...scopedVarsFromSpan(span), }; // We should be here only if there are some links in the dataframe - const field = dataFrame.fields.find((f) => Boolean(f.config.links?.length))!; + const fields = dataFrame.fields.filter((f) => Boolean(f.config.links?.length))!; try { - const links = getFieldLinksForExplore({ - field, - rowIndex: span.dataFrameRowIndex!, - splitOpenFn, - range: getTimeRangeFromSpan(span), - dataFrame, - vars: scopedVars, + let links: ExploreFieldLinkModel[] = []; + fields.forEach((field) => { + const fieldLinksForExplore = getFieldLinksForExplore({ + field, + rowIndex: span.dataFrameRowIndex!, + splitOpenFn, + range: getTimeRangeFromSpan(span), + dataFrame, + vars: scopedVars, + }); + links = links.concat(fieldLinksForExplore); }); - return { - logLinks: [ - { - href: links[0].href, - onClick: links[0].onClick, - content: , - field: links[0].origin, - }, - ], - }; + const newSpanLinks: SpanLinkDef[] = links.map((link) => { + return { + title: link.title, + href: link.href, + onClick: link.onClick, + content: , + field: link.origin, + type: SpanLinkType.Unknown, + }; + }); + + spanLinks.push.apply(spanLinks, newSpanLinks); } 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; + return spanLinks; } - }; - } + } - return undefined; + return spanLinks; + }; } /** @@ -134,12 +136,12 @@ function legacyCreateSpanLinkFactory( metricsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToMetricsOptions.datasourceUid); } - return function SpanLink(span: TraceSpan): SpanLinks { + return function SpanLink(span: TraceSpan): SpanLinkDef[] { scopedVars = { ...scopedVars, ...scopedVarsFromSpan(span), }; - const links: SpanLinks = { traceLinks: [] }; + const links: SpanLinkDef[] = []; let query: DataQuery | undefined; let tags = ''; @@ -216,21 +218,20 @@ function legacyCreateSpanLinkFactory( replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), }); - links.logLinks = [ - { - href: link.href, - onClick: link.onClick, - content: , - field, - }, - ]; + links.push({ + href: link.href, + title: 'Related logs', + onClick: link.onClick, + content: , + field, + type: SpanLinkType.Logs, + }); } } } // Get metrics links if (metricsDataSourceSettings && traceToMetricsOptions?.queries) { - links.metricLinks = []; for (const query of traceToMetricsOptions.queries) { const expr = buildMetricsQuery(query, traceToMetricsOptions?.tags || [], span); const dataLink: DataLink = { @@ -263,12 +264,13 @@ function legacyCreateSpanLinkFactory( replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), }); - links.metricLinks.push({ + links.push({ title: query?.name, href: link.href, onClick: link.onClick, content: , field, + type: SpanLinkType.Metrics, }); } } @@ -283,12 +285,13 @@ function legacyCreateSpanLinkFactory( const link = createFocusSpanLink(reference.traceID, reference.spanID); - links.traceLinks!.push({ + links!.push({ href: link.href, title: reference.span ? reference.span.operationName : 'View linked span', content: , onClick: link.onClick, field: link.origin, + type: SpanLinkType.Traces, }); } } @@ -297,12 +300,13 @@ function legacyCreateSpanLinkFactory( for (const reference of span.subsidiarilyReferencedBy) { const link = createFocusSpanLink(reference.traceID, reference.spanID); - links.traceLinks!.push({ + links!.push({ href: link.href, title: reference.span ? reference.span.operationName : 'View linked span', content: , onClick: link.onClick, field: link.origin, + type: SpanLinkType.Traces, }); } } diff --git a/public/app/features/explore/utils/links.test.ts b/public/app/features/explore/utils/links.test.ts index 1108e240b47..587a62baa81 100644 --- a/public/app/features/explore/utils/links.test.ts +++ b/public/app/features/explore/utils/links.test.ts @@ -358,6 +358,61 @@ describe('explore links utils', () => { ); }); + it('returns internal links within a result consistent with trace data', () => { + const transformationLink: DataLink = { + title: '', + url: '', + internal: { + query: { query: 'http_requests{env=${msg}}' }, + datasourceUid: 'uid_1', + datasourceName: 'test_ds', + transformations: [ + { + type: SupportedTransformationType.Regex, + expression: '{(?=[^\\}]*\\bkey":"keyA")[^\\}]*\\bvalue":"(.*?)".*}', + field: 'serviceTags', + mapValue: 'msg', + }, + ], + }, + }; + + const { field, range, dataFrame } = setup(transformationLink, true, { + name: 'serviceTags', + type: FieldType.other, + values: [ + [ + { value: 'broccoli', key: 'keyA' }, + { value: 'apple', key: 'keyB' }, + ], + [ + { key: 'keyA', value: 'cauliflower' }, + { value: 'durian', key: 'keyB' }, + ], + ], + config: { + links: [transformationLink], + }, + }); + + const links = [ + getFieldLinksForExplore({ field, rowIndex: 0, range, dataFrame }), + getFieldLinksForExplore({ field, rowIndex: 1, range, dataFrame }), + ]; + expect(links[0]).toHaveLength(1); + expect(links[0][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{env=broccoli}"}]}' + )}` + ); + expect(links[1]).toHaveLength(1); + expect(links[1][0].href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"http_requests{env=cauliflower}"}]}' + )}` + ); + }); + it('returns internal links with logfmt with stringified booleans', () => { const transformationLink: DataLink = { title: '', @@ -684,7 +739,7 @@ const ROW_WITH_NULL_VALUE = { value: null, index: 1 }; function setup( link: DataLink, hasAccess = true, - fieldOverride?: Field, + fieldOverride?: Field | null>, // key/value array for traceView fields dataFrameOtherFieldOverride?: Field[] ) { setLinkSrv({