Correlations: Enable traceView formatted links (#67160)

* Enable correlations links for traceView formatted fields

* Add other links to the span UI

* Show both “legacy” and dataframe links

* bandaid test

* Remove special path logic, stringify all non strings before applying regex

* Fix test

* Do not create new instance of factory for every call, change header to correlations

* Get links from more than one field

* Remove categories

* Fix google cloud link tests

* Add test for multiple internal links

* Remove changes to datasources provisioning

* Added sorting, changed log link title
This commit is contained in:
Kristina 2023-05-16 07:47:15 -05:00 committed by GitHub
parent f478504bc9
commit 0db397b8be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 398 additions and 250 deletions

View File

@ -12,7 +12,9 @@ export const getTransformationVars = (
let transformVal: { [key: string]: string | boolean | null | undefined } = {}; let transformVal: { [key: string]: string | boolean | null | undefined } = {};
if (transformation.type === SupportedTransformationType.Regex && transformation.expression) { if (transformation.type === SupportedTransformationType.Regex && transformation.expression) {
const regexp = new RegExp(transformation.expression, 'gi'); 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) { for (const match of matches) {
if (match.groups) { if (match.groups) {
transformVal = match.groups; transformVal = match.groups;

View File

@ -17,8 +17,7 @@ import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import { NONE, DURATION, TAG } from '../settings/SpanBarSettings'; import { NONE, DURATION, TAG } from '../settings/SpanBarSettings';
import { TraceSpan } from '../types'; import { SpanLinkDef, TraceSpan } from '../types';
import { SpanLinks } from '../types/links';
import SpanBarRow, { SpanBarRowProps } from './SpanBarRow'; import SpanBarRow, { SpanBarRowProps } from './SpanBarRow';
@ -111,11 +110,7 @@ describe('<SpanBarRow>', () => {
<SpanBarRow <SpanBarRow
{...(props as unknown as SpanBarRowProps)} {...(props as unknown as SpanBarRowProps)}
span={span} span={span}
createSpanLink={() => createSpanLink={() => [{ href: 'href' }, { href: 'href' }] as SpanLinkDef[]}
({
traceLinks: [{ href: 'href' }, { href: 'href' }],
} as SpanLinks)
}
/> />
); );
expect(screen.getAllByTestId('SpanLinksMenu')).toHaveLength(1); expect(screen.getAllByTestId('SpanLinksMenu')).toHaveLength(1);
@ -142,11 +137,7 @@ describe('<SpanBarRow>', () => {
<SpanBarRow <SpanBarRow
{...(props as unknown as SpanBarRowProps)} {...(props as unknown as SpanBarRowProps)}
span={span} span={span}
createSpanLink={() => createSpanLink={() => [{ content: 'This span is referenced by another span', href: 'href' }] as SpanLinkDef[]}
({
traceLinks: [{ content: 'This span is referenced by another span', href: 'href' }],
} as SpanLinks)
}
/> />
); );
expect(screen.getByRole('link', { name: 'This span is referenced by another span' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'This span is referenced by another span' })).toBeInTheDocument();
@ -181,11 +172,7 @@ describe('<SpanBarRow>', () => {
<SpanBarRow <SpanBarRow
{...(props as unknown as SpanBarRowProps)} {...(props as unknown as SpanBarRowProps)}
span={span} span={span}
createSpanLink={() => createSpanLink={() => [{ href: 'href' }, { href: 'href' }] as SpanLinkDef[]}
({
traceLinks: [{ href: 'href' }, { href: 'href' }],
} as SpanLinks)
}
/> />
); );
expect(screen.getAllByTestId('SpanLinksMenu')).toHaveLength(1); expect(screen.getAllByTestId('SpanLinksMenu')).toHaveLength(1);

View File

@ -22,7 +22,6 @@ import { Icon, stylesFactory, withTheme2 } from '@grafana/ui';
import { autoColor } from '../Theme'; import { autoColor } from '../Theme';
import { DURATION, NONE, TAG } from '../settings/SpanBarSettings'; import { DURATION, NONE, TAG } from '../settings/SpanBarSettings';
import { SpanBarOptions, SpanLinkFunc, TraceSpan, TNil } from '../types'; import { SpanBarOptions, SpanLinkFunc, TraceSpan, TNil } from '../types';
import { SpanLinks } from '../types/links';
import SpanBar from './SpanBar'; import SpanBar from './SpanBar';
import { SpanLinksMenu } from './SpanLinks'; import { SpanLinksMenu } from './SpanLinks';
@ -404,14 +403,6 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
hintClassName = styles.labelRight; hintClassName = styles.labelRight;
} }
const countLinks = (links?: SpanLinks): number => {
if (!links) {
return 0;
}
return Object.values(links).reduce((count, arr) => count + arr.length, 0);
};
return ( return (
<TimelineRow <TimelineRow
className={cx( className={cx(
@ -490,32 +481,31 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
{createSpanLink && {createSpanLink &&
(() => { (() => {
const links = createSpanLink(span); const links = createSpanLink(span);
const count = countLinks(links); const count = links?.length || 0;
if (links && count === 1) { if (links && count === 1) {
const link = links.logLinks?.[0] ?? links.metricLinks?.[0] ?? links.traceLinks?.[0] ?? undefined; if (!links[0]) {
if (!link) {
return null; return null;
} }
return ( return (
<a <a
href={link.href} href={links[0].href}
// Needs to have target otherwise preventDefault would not work due to angularRouter. // Needs to have target otherwise preventDefault would not work due to angularRouter.
target={'_blank'} target={'_blank'}
style={{ marginRight: '5px' }} style={{ marginRight: '5px' }}
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={ onClick={
link.onClick links[0].onClick
? (event) => { ? (event) => {
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) { if (!(event.ctrlKey || event.metaKey || event.shiftKey) && links[0].onClick) {
event.preventDefault(); event.preventDefault();
link.onClick(event); links[0].onClick(event);
} }
} }
: undefined : undefined
} }
> >
{link.content} {links[0].content}
</a> </a>
); );
} else if (links && count > 1) { } else if (links && count > 1) {

View File

@ -24,6 +24,7 @@ import { autoColor } from '../../Theme';
import { Divider } from '../../common/Divider'; import { Divider } from '../../common/Divider';
import LabeledList from '../../common/LabeledList'; import LabeledList from '../../common/LabeledList';
import { SpanLinkFunc, TNil } from '../../types'; import { SpanLinkFunc, TNil } from '../../types';
import { SpanLinkType } from '../../types/links';
import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan, TraceSpanReference } from '../../types/trace'; import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan, TraceSpanReference } from '../../types/trace';
import { uAlignIcon, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles'; import { uAlignIcon, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles';
import { TopOfViewRefType } from '../VirtualizedTraceView'; import { TopOfViewRefType } from '../VirtualizedTraceView';
@ -197,14 +198,15 @@ export default function SpanDetail(props: SpanDetailProps) {
let logLinkButton: JSX.Element | undefined = undefined; let logLinkButton: JSX.Element | undefined = undefined;
if (createSpanLink) { if (createSpanLink) {
const links = createSpanLink(span); const links = createSpanLink(span);
if (links?.logLinks) { const logLinks = links?.filter((link) => link.type === SpanLinkType.Logs);
if (links && logLinks && logLinks.length > 0) {
logLinkButton = ( logLinkButton = (
<DataLinkButton <DataLinkButton
link={{ link={{
...links.logLinks[0], ...logLinks[0],
title: 'Logs for this span', title: 'Logs for this span',
target: '_blank', target: '_blank',
origin: links.logLinks[0].field, origin: logLinks[0].field,
onClick: (event: React.MouseEvent) => { onClick: (event: React.MouseEvent) => {
reportInteraction('grafana_traces_trace_view_span_link_clicked', { reportInteraction('grafana_traces_trace_view_span_link_clicked', {
datasourceType: datasourceType, datasourceType: datasourceType,
@ -212,7 +214,7 @@ export default function SpanDetail(props: SpanDetailProps) {
type: 'log', type: 'log',
location: 'spanDetails', location: 'spanDetails',
}); });
links?.logLinks?.[0].onClick?.(event); logLinks?.[0].onClick?.(event);
}, },
}} }}
buttonProps={{ icon: 'gf-logs' }} buttonProps={{ icon: 'gf-logs' }}

View File

@ -2,106 +2,48 @@ import { css } from '@emotion/css';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { config, reportInteraction } from '@grafana/runtime'; 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 { interface SpanLinksProps {
links: SpanLinks; links: SpanLinkDef[];
datasourceType: string; datasourceType: string;
} }
const renderMenuItems = ( const renderMenuItems = (
links: SpanLinks, links: SpanLinkDef[],
styles: ReturnType<typeof getStyles>, styles: ReturnType<typeof getStyles>,
closeMenu: () => void, closeMenu: () => void,
datasourceType: string datasourceType: string
) => { ) => {
return ( links.sort(function (linkA, linkB) {
<> return (linkA.title || 'link').toLowerCase().localeCompare((linkB.title || 'link').toLowerCase());
{!!links.logLinks?.length ? ( });
<MenuGroup label="Logs">
{links.logLinks.map((link, i) => ( return links.map((link, i) => (
<MenuItem <MenuItem
key={i} key={i}
label="Logs for this span" label={link.title || 'Link'}
onClick={ onClick={
link.onClick link.onClick
? (event) => { ? (event) => {
reportInteraction('grafana_traces_trace_view_span_link_clicked', { reportInteraction(`grafana_traces_trace_view_span_link_clicked`, {
datasourceType: datasourceType, datasourceType: datasourceType,
grafana_version: config.buildInfo.version, grafana_version: config.buildInfo.version,
type: 'log', type: link.type,
location: 'menu', location: 'menu',
}); });
event?.preventDefault(); event?.preventDefault();
link.onClick!(event); link.onClick!(event);
closeMenu(); closeMenu();
} }
: undefined : undefined
} }
url={link.href} url={link.href}
className={styles.menuItem} className={styles.menuItem}
/> />
))} ));
</MenuGroup>
) : null}
{!!links.metricLinks?.length ? (
<MenuGroup label="Metrics">
{links.metricLinks.map((link, i) => (
<MenuItem
key={i}
label={link.title ?? 'Metrics for this span'}
onClick={
link.onClick
? (event) => {
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}
/>
))}
</MenuGroup>
) : null}
{!!links.traceLinks?.length ? (
<MenuGroup label="Traces">
{links.traceLinks.map((link, i) => (
<MenuItem
key={i}
label={link.title ?? 'View linked span'}
onClick={
link.onClick
? (event) => {
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}
/>
))}
</MenuGroup>
) : null}
</>
);
}; };
export const SpanLinksMenu = ({ links, datasourceType }: SpanLinksProps) => { export const SpanLinksMenu = ({ links, datasourceType }: SpanLinksProps) => {
@ -130,7 +72,7 @@ export const SpanLinksMenu = ({ links, datasourceType }: SpanLinksProps) => {
<ContextMenu <ContextMenu
onClose={() => setIsMenuOpen(false)} onClose={() => setIsMenuOpen(false)}
renderMenuItems={() => renderMenuItems(links, styles, closeMenu, datasourceType)} renderMenuItems={() => renderMenuItems(links, styles, closeMenu, datasourceType)}
focusOnOpen={true} focusOnOpen={false}
x={menuPosition.x} x={menuPosition.x}
y={menuPosition.y} y={menuPosition.y}
/> />

View File

@ -4,18 +4,20 @@ import { Field } from '@grafana/data';
import { TraceSpan } from './trace'; import { TraceSpan } from './trace';
export enum SpanLinkType {
Logs = 'log',
Traces = 'trace',
Metrics = 'metric',
Unknown = 'unknown',
}
export type SpanLinkDef = { export type SpanLinkDef = {
href: string; href: string;
onClick?: (event: unknown) => void; onClick?: (event: unknown) => void;
content: React.ReactNode; content: React.ReactNode;
title?: string; title?: string;
field: Field; field: Field;
type: SpanLinkType;
}; };
export type SpanLinks = { export type SpanLinkFunc = (span: TraceSpan) => SpanLinkDef[] | undefined;
logLinks?: SpanLinkDef[];
traceLinks?: SpanLinkDef[];
metricLinks?: SpanLinkDef[];
};
export type SpanLinkFunc = (span: TraceSpan) => SpanLinks | undefined;

View File

@ -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 { DataSourceSrv, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; 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 { TemplateSrv } from '../../templating/template_srv';
import { Trace, TraceSpan } from './components'; import { Trace, TraceSpan } from './components';
import { SpanLinkType } from './components/types/links';
import { createSpanLinkFactory } from './createSpanLink'; import { createSpanLinkFactory } from './createSpanLink';
const dummyTraceData = { duration: 10, traceID: 'trace1', traceName: 'test trace' } as unknown as Trace; 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', () => { describe('createSpanLinkFactory', () => {
it('returns no links if there is no data source uid', () => { it('returns no links if there is no data source uid', () => {
@ -22,9 +36,8 @@ describe('createSpanLinkFactory', () => {
dataFrame: dummyDataFrame, dataFrame: dummyDataFrame,
}); });
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
expect(links?.logLinks).toBeUndefined(); expect(links).toBeDefined();
expect(links?.metricLinks).toBeUndefined(); expect(links).toHaveLength(0);
expect(links?.traceLinks).toHaveLength(0);
}); });
describe('should return loki link', () => { describe('should return loki link', () => {
@ -43,8 +56,9 @@ describe('createSpanLinkFactory', () => {
const createLink = setupSpanLinkFactory(); const createLink = setupSpanLinkFactory();
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(decodeURIComponent(linkDef!.href)).toBe( expect(decodeURIComponent(linkDef!.href)).toBe(
'/explore?left=' + '/explore?left=' +
JSON.stringify({ JSON.stringify({
@ -157,7 +175,7 @@ describe('createSpanLinkFactory', () => {
const splitOpenFn = jest.fn(); const splitOpenFn = jest.fn();
const createLink = createSpanLinkFactory({ const createLink = createSpanLinkFactory({
splitOpenFn, splitOpenFn,
dataFrame: new MutableDataFrame({ dataFrame: createDataFrame({
fields: [ fields: [
{ name: 'traceID', values: ['testTraceId'] }, { name: 'traceID', values: ['testTraceId'] },
{ {
@ -172,8 +190,9 @@ describe('createSpanLinkFactory', () => {
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Unknown);
expect(linkDef!.href).toBe('testSpanId'); 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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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', () => { it('interpolates span intrinsics', () => {
@ -260,8 +282,9 @@ describe('createSpanLinkFactory', () => {
}); });
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
expect(links?.logLinks).toBeDefined(); expect(links).toBeDefined();
expect(decodeURIComponent(links!.logLinks![0].href)).toContain('spanName=\\"operation\\"'); 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 links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toContain(`${encodeURIComponent('datasource":"splunkUID","queries":[{"query"')}`); expect(linkDef!.href).toContain(`${encodeURIComponent('datasource":"splunkUID","queries":[{"query"')}`);
expect(linkDef!.href).not.toContain(`${encodeURIComponent('datasource":"splunkUID","queries":[{"expr"')}`); expect(linkDef!.href).not.toContain(`${encodeURIComponent('datasource":"splunkUID","queries":[{"expr"')}`);
}); });
@ -301,8 +325,9 @@ describe('createSpanLinkFactory', () => {
}); });
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toContain( expect(linkDef!.href).toContain(
`${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}` `${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(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.metricLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Metrics);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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"}]}' '{"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(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
expect(links?.metricLinks).toBeUndefined(); expect(links).toBeDefined();
expect(links?.length).toEqual(0);
}); });
it('returns multiple queries including default', () => { it('returns multiple queries including default', () => {
@ -479,11 +509,12 @@ describe('createSpanLinkFactory', () => {
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
expect(links?.metricLinks).toBeDefined(); expect(links).toBeDefined();
expect(links?.metricLinks).toHaveLength(3); expect(links).toHaveLength(3);
const namedLink = links?.metricLinks?.[0]; const namedLink = links?.[0];
expect(namedLink).toBeDefined(); expect(namedLink).toBeDefined();
expect(namedLink?.type).toBe(SpanLinkType.Metrics);
expect(namedLink!.title).toBe('Named Query'); expect(namedLink!.title).toBe('Named Query');
expect(namedLink!.href).toBe( expect(namedLink!.href).toBe(
`/explore?left=${encodeURIComponent( `/explore?left=${encodeURIComponent(
@ -491,8 +522,9 @@ describe('createSpanLinkFactory', () => {
)}` )}`
); );
const defaultLink = links?.metricLinks?.[1]; const defaultLink = links?.[1];
expect(defaultLink).toBeDefined(); expect(defaultLink).toBeDefined();
expect(defaultLink?.type).toBe(SpanLinkType.Metrics);
expect(defaultLink!.title).toBe('defaultQuery'); expect(defaultLink!.title).toBe('defaultQuery');
expect(defaultLink!.href).toBe( expect(defaultLink!.href).toBe(
`/explore?left=${encodeURIComponent( `/explore?left=${encodeURIComponent(
@ -500,8 +532,9 @@ describe('createSpanLinkFactory', () => {
)}` )}`
); );
const unnamedQuery = links?.metricLinks?.[2]; const unnamedQuery = links?.[2];
expect(unnamedQuery).toBeDefined(); expect(unnamedQuery).toBeDefined();
expect(unnamedQuery?.type).toBe(SpanLinkType.Metrics);
expect(unnamedQuery!.title).toBeUndefined(); expect(unnamedQuery!.title).toBeUndefined();
expect(unnamedQuery!.href).toBe( expect(unnamedQuery!.href).toBe(
`/explore?left=${encodeURIComponent( `/explore?left=${encodeURIComponent(
@ -526,9 +559,9 @@ describe('createSpanLinkFactory', () => {
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.metricLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Metrics);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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"}]}' '{"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).toBeDefined();
expect(links!.metricLinks![0]!.href).toBe( expect(links![0].type).toBe(SpanLinkType.Metrics);
expect(links![0].href).toBe(
`/explore?left=${encodeURIComponent( `/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"}]}' '{"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' }] }) createTraceSpan({ references: [{ refType: 'CHILD_OF', spanID: 'parent', traceID: 'traceID' }] })
); );
const traceLinks = links?.traceLinks; const traceLinks = links;
expect(traceLinks).toBeDefined(); expect(traceLinks).toBeDefined();
expect(traceLinks).toHaveLength(0); expect(traceLinks).toHaveLength(0);
}); });
@ -609,9 +643,12 @@ describe('createSpanLinkFactory', () => {
}) })
); );
const traceLinks = links?.traceLinks; const traceLinks = links;
expect(traceLinks).toBeDefined(); expect(traceLinks).toBeDefined();
expect(traceLinks).toHaveLength(2); expect(traceLinks).toHaveLength(2);
expect(traceLinks![0].type).toBe(SpanLinkType.Traces);
expect(traceLinks![1].type).toBe(SpanLinkType.Traces);
expect(traceLinks![0]).toEqual( expect(traceLinks![0]).toEqual(
expect.objectContaining({ expect.objectContaining({
href: 'traceID-span1', href: 'traceID-span1',
@ -651,8 +688,9 @@ describe('createSpanLinkFactory', () => {
}); });
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(decodeURIComponent(linkDef!.href)).toContain( expect(decodeURIComponent(linkDef!.href)).toContain(
`datasource":"${searchUID}","queries":[{"query":"cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]` `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 links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toContain( expect(linkDef!.href).toContain(
`${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}` `${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(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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"}]}]}` `{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(decodeURIComponent(linkDef!.href)).toBe( 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"}]}]}` `/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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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"}]}]}` `{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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"}]}]}` `{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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"}]}]}` `{"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 links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(decodeURIComponent(linkDef!.href)).toContain( expect(decodeURIComponent(linkDef!.href)).toContain(
`datasource":"${searchUID}","queries":[{"query":"cluster=\\"cluster1\\" AND hostname=\\"hostname1\\"","refId":""}]` `datasource":"${searchUID}","queries":[{"query":"cluster=\\"cluster1\\" AND hostname=\\"hostname1\\"","refId":""}]`
); );
@ -847,8 +892,9 @@ describe('createSpanLinkFactory', () => {
}); });
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toContain( expect(linkDef!.href).toContain(
`${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}` `${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(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}` `{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(decodeURIComponent(linkDef!.href)).toBe( 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":""}]}` `/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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}` `{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}` `{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}` `{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(decodeURIComponent(linkDef!.href)).toContain( expect(decodeURIComponent(linkDef!.href)).toContain(
'"queries":' + '"queries":' +
JSON.stringify([{ expr: '{service="serviceName", pod="podName"} |="serviceName" |="trace1"', refId: '' }]) JSON.stringify([{ expr: '{service="serviceName", pod="podName"} |="serviceName" |="trace1"', refId: '' }])
@ -1043,7 +1095,8 @@ describe('createSpanLinkFactory', () => {
}); });
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); 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 links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toContain(`${encodeURIComponent('datasource":"falconLogScaleUID","queries":[{"lsql"')}`); expect(linkDef!.href).toContain(`${encodeURIComponent('datasource":"falconLogScaleUID","queries":[{"lsql"')}`);
}); });
@ -1086,8 +1140,9 @@ describe('createSpanLinkFactory', () => {
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan()); const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0]; const linkDef = links?.[0];
expect(linkDef).toBeDefined(); expect(linkDef).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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).toBeDefined();
expect(linkDef?.type).toBe(SpanLinkType.Logs);
expect(linkDef!.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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<TraceToLogsOptionsV2> = {}, datasourceUid = 'lokiUid') { function setupSpanLinkFactory(options: Partial<TraceToLogsOptionsV2> = {}, datasourceUid = 'lokiUid') {
const splitOpenFn = jest.fn(); const splitOpenFn = jest.fn();
return createSpanLinkFactory({ return createSpanLinkFactory({
@ -1232,3 +1331,68 @@ function createTraceSpan(overrides: Partial<TraceSpan> = {}) {
...overrides, ...overrides,
} as TraceSpan; } 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',
},
],
},
],
});
}

View File

@ -23,10 +23,10 @@ import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { PromQuery } from 'app/plugins/datasource/prometheus/types'; import { PromQuery } from 'app/plugins/datasource/prometheus/types';
import { LokiQuery } from '../../../plugins/datasource/loki/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 { SpanLinkDef, SpanLinkFunc, Trace, TraceSpan } from './components';
import { SpanLinks } from './components/types/links'; 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 * 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); let scopedVars = scopedVarsFromTrace(trace);
const hasLinks = dataFrame.fields.some((f) => Boolean(f.config.links?.length)); const hasLinks = dataFrame.fields.some((f) => Boolean(f.config.links?.length));
const legacyFormat = dataFrame.fields.length === 1;
if (legacyFormat || !hasLinks) { const createSpanLinks = legacyCreateSpanLinkFactory(
// if the dataframe contains just a single blob of data (legacy format) or does not have any links configured, splitOpenFn,
// let's try to use the old legacy path. // We need this to make the types happy but for this branch of code it does not matter which field we supply.
// TODO: This was mainly a backward compatibility thing but at this point can probably be removed. dataFrame.fields[0],
return legacyCreateSpanLinkFactory( traceToLogsOptions,
splitOpenFn, traceToMetricsOptions,
// We need this to make the types happy but for this branch of code it does not matter which field we supply. createFocusSpanLink,
dataFrame.fields[0], scopedVars
traceToLogsOptions, );
traceToMetricsOptions,
createFocusSpanLink,
scopedVars
);
}
if (hasLinks) { return function SpanLink(span: TraceSpan): SpanLinkDef[] | undefined {
return function SpanLink(span: TraceSpan): SpanLinks | undefined { let spanLinks = createSpanLinks(span);
if (hasLinks) {
scopedVars = { scopedVars = {
...scopedVars, ...scopedVars,
...scopedVarsFromSpan(span), ...scopedVarsFromSpan(span),
}; };
// We should be here only if there are some links in the dataframe // 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 { try {
const links = getFieldLinksForExplore({ let links: ExploreFieldLinkModel[] = [];
field, fields.forEach((field) => {
rowIndex: span.dataFrameRowIndex!, const fieldLinksForExplore = getFieldLinksForExplore({
splitOpenFn, field,
range: getTimeRangeFromSpan(span), rowIndex: span.dataFrameRowIndex!,
dataFrame, splitOpenFn,
vars: scopedVars, range: getTimeRangeFromSpan(span),
dataFrame,
vars: scopedVars,
});
links = links.concat(fieldLinksForExplore);
}); });
return { const newSpanLinks: SpanLinkDef[] = links.map((link) => {
logLinks: [ return {
{ title: link.title,
href: links[0].href, href: link.href,
onClick: links[0].onClick, onClick: link.onClick,
content: <Icon name="gf-logs" title="Explore the logs for this in split view" />, content: <Icon name="link" title={link.title || 'Link'} />,
field: links[0].origin, field: link.origin,
}, type: SpanLinkType.Unknown,
], };
}; });
spanLinks.push.apply(spanLinks, newSpanLinks);
} catch (error) { } catch (error) {
// It's fairly easy to crash here for example if data source defines wrong interpolation in the data link // It's fairly easy to crash here for example if data source defines wrong interpolation in the data link
console.error(error); console.error(error);
return undefined; return spanLinks;
} }
}; }
}
return undefined; return spanLinks;
};
} }
/** /**
@ -134,12 +136,12 @@ function legacyCreateSpanLinkFactory(
metricsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToMetricsOptions.datasourceUid); metricsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToMetricsOptions.datasourceUid);
} }
return function SpanLink(span: TraceSpan): SpanLinks { return function SpanLink(span: TraceSpan): SpanLinkDef[] {
scopedVars = { scopedVars = {
...scopedVars, ...scopedVars,
...scopedVarsFromSpan(span), ...scopedVarsFromSpan(span),
}; };
const links: SpanLinks = { traceLinks: [] }; const links: SpanLinkDef[] = [];
let query: DataQuery | undefined; let query: DataQuery | undefined;
let tags = ''; let tags = '';
@ -216,21 +218,20 @@ function legacyCreateSpanLinkFactory(
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
}); });
links.logLinks = [ links.push({
{ href: link.href,
href: link.href, title: 'Related logs',
onClick: link.onClick, onClick: link.onClick,
content: <Icon name="gf-logs" title="Explore the logs for this in split view" />, content: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
field, field,
}, type: SpanLinkType.Logs,
]; });
} }
} }
} }
// Get metrics links // Get metrics links
if (metricsDataSourceSettings && traceToMetricsOptions?.queries) { if (metricsDataSourceSettings && traceToMetricsOptions?.queries) {
links.metricLinks = [];
for (const query of traceToMetricsOptions.queries) { for (const query of traceToMetricsOptions.queries) {
const expr = buildMetricsQuery(query, traceToMetricsOptions?.tags || [], span); const expr = buildMetricsQuery(query, traceToMetricsOptions?.tags || [], span);
const dataLink: DataLink<PromQuery> = { const dataLink: DataLink<PromQuery> = {
@ -263,12 +264,13 @@ function legacyCreateSpanLinkFactory(
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
}); });
links.metricLinks.push({ links.push({
title: query?.name, title: query?.name,
href: link.href, href: link.href,
onClick: link.onClick, onClick: link.onClick,
content: <Icon name="chart-line" title="Explore metrics for this span" />, content: <Icon name="chart-line" title="Explore metrics for this span" />,
field, field,
type: SpanLinkType.Metrics,
}); });
} }
} }
@ -283,12 +285,13 @@ function legacyCreateSpanLinkFactory(
const link = createFocusSpanLink(reference.traceID, reference.spanID); const link = createFocusSpanLink(reference.traceID, reference.spanID);
links.traceLinks!.push({ links!.push({
href: link.href, href: link.href,
title: reference.span ? reference.span.operationName : 'View linked span', title: reference.span ? reference.span.operationName : 'View linked span',
content: <Icon name="link" title="View linked span" />, content: <Icon name="link" title="View linked span" />,
onClick: link.onClick, onClick: link.onClick,
field: link.origin, field: link.origin,
type: SpanLinkType.Traces,
}); });
} }
} }
@ -297,12 +300,13 @@ function legacyCreateSpanLinkFactory(
for (const reference of span.subsidiarilyReferencedBy) { for (const reference of span.subsidiarilyReferencedBy) {
const link = createFocusSpanLink(reference.traceID, reference.spanID); const link = createFocusSpanLink(reference.traceID, reference.spanID);
links.traceLinks!.push({ links!.push({
href: link.href, href: link.href,
title: reference.span ? reference.span.operationName : 'View linked span', title: reference.span ? reference.span.operationName : 'View linked span',
content: <Icon name="link" title="View linked span" />, content: <Icon name="link" title="View linked span" />,
onClick: link.onClick, onClick: link.onClick,
field: link.origin, field: link.origin,
type: SpanLinkType.Traces,
}); });
} }
} }

View File

@ -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', () => { it('returns internal links with logfmt with stringified booleans', () => {
const transformationLink: DataLink = { const transformationLink: DataLink = {
title: '', title: '',
@ -684,7 +739,7 @@ const ROW_WITH_NULL_VALUE = { value: null, index: 1 };
function setup( function setup(
link: DataLink, link: DataLink,
hasAccess = true, hasAccess = true,
fieldOverride?: Field<string | null>, fieldOverride?: Field<string | Array<{ key: string; value: string }> | null>, // key/value array for traceView fields
dataFrameOtherFieldOverride?: Field[] dataFrameOtherFieldOverride?: Field[]
) { ) {
setLinkSrv({ setLinkSrv({