diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx
new file mode 100644
index 00000000000..27fbf1cc3fd
--- /dev/null
+++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx
@@ -0,0 +1,121 @@
+import { css } from '@emotion/css';
+import React, { useState } from 'react';
+
+import { useStyles2, MenuGroup, MenuItem, Icon, ContextMenu } from '@grafana/ui';
+
+import { SpanLinks } from '../types/links';
+
+interface SpanLinksProps {
+ links: SpanLinks;
+}
+
+const renderMenuItems = (links: SpanLinks, styles: ReturnType
, closeMenu: () => void) => {
+ return (
+ <>
+ {!!links.logLinks?.length ? (
+
+ {links.logLinks.map((link, i) => (
+ {
+ if (link.onClick) {
+ link.onClick(e);
+ }
+ closeMenu();
+ }}
+ url={link.href}
+ className={styles.menuItem}
+ />
+ ))}
+
+ ) : null}
+ {!!links.metricLinks?.length ? (
+
+ {links.metricLinks.map((link, i) => (
+ {
+ if (link.onClick) {
+ link.onClick(e);
+ }
+ closeMenu();
+ }}
+ url={link.href}
+ className={styles.menuItem}
+ />
+ ))}
+
+ ) : null}
+ {!!links.traceLinks?.length ? (
+
+ {links.traceLinks.map((link, i) => (
+ {
+ if (link.onClick) {
+ link.onClick(e);
+ }
+ closeMenu();
+ }}
+ url={link.href}
+ className={styles.menuItem}
+ />
+ ))}
+
+ ) : null}
+ >
+ );
+};
+
+export const SpanLinksMenu = ({ links }: SpanLinksProps) => {
+ const styles = useStyles2(getStyles);
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
+
+ const closeMenu = () => setIsMenuOpen(false);
+
+ return (
+ <>
+ {
+ setIsMenuOpen(true);
+ setMenuPosition({
+ x: e.pageX,
+ y: e.pageY,
+ });
+ }}
+ className={styles.button}
+ >
+
+
+
+ {isMenuOpen ? (
+ setIsMenuOpen(false)}
+ renderMenuItems={() => renderMenuItems(links, styles, closeMenu)}
+ focusOnOpen={true}
+ x={menuPosition.x}
+ y={menuPosition.y}
+ />
+ ) : null}
+ >
+ );
+};
+
+const getStyles = () => {
+ return {
+ button: css`
+ background: transparent;
+ border: none;
+ padding: 0;
+ margin: 0 3px 0 0;
+ `,
+ menuItem: css`
+ max-width: 60ch;
+ overflow: hidden;
+ `,
+ };
+};
diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx
index bf3411015f8..bb71a941b29 100644
--- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx
+++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx
@@ -387,7 +387,6 @@ export class UnthemedVirtualizedTraceView extends React.Component void;
content: React.ReactNode;
+ title?: string;
};
-export type SpanLinkFunc = (span: TraceSpan) => SpanLinkDef | undefined;
+export type SpanLinks = {
+ logLinks?: SpanLinkDef[];
+ traceLinks?: SpanLinkDef[];
+ metricLinks?: SpanLinkDef[];
+};
+
+export type SpanLinkFunc = (span: TraceSpan) => SpanLinks | undefined;
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 5080805e73a..bc60b9b5ca9 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -248,5 +248,11 @@ var (
RequiresDevMode: true,
FrontendOnly: true,
},
+ {
+ Name: "traceToMetrics",
+ Description: "Enable trace to metrics links",
+ State: FeatureStateAlpha,
+ FrontendOnly: true,
+ },
}
)
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index ded1ce2e065..8d21cf68d13 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -182,4 +182,8 @@ const (
// FlagAzureMonitorExperimentalUI
// Use grafana-experimental UI in Azure Monitor
FlagAzureMonitorExperimentalUI = "azureMonitorExperimentalUI"
+
+ // FlagTraceToMetrics
+ // Enable trace to metrics links
+ FlagTraceToMetrics = "traceToMetrics"
)
diff --git a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx
index bfafdfee442..0dd4f62c50a 100644
--- a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx
+++ b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx
@@ -40,7 +40,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
Trace to logs
- Trace to logs lets you navigate from a trace span to the selected data source's log.
+ Trace to logs lets you navigate from a trace span to the selected data source's logs.
diff --git a/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx b/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx
new file mode 100644
index 00000000000..fc450315580
--- /dev/null
+++ b/public/app/core/components/TraceToMetrics/TraceToMetricsSettings.tsx
@@ -0,0 +1,80 @@
+import { css } from '@emotion/css';
+import React from 'react';
+
+import {
+ DataSourceJsonData,
+ DataSourcePluginOptionsEditorProps,
+ GrafanaTheme,
+ updateDatasourcePluginJsonDataOption,
+} from '@grafana/data';
+import { DataSourcePicker } from '@grafana/runtime';
+import { Button, InlineField, InlineFieldRow, useStyles } from '@grafana/ui';
+
+export interface TraceToMetricsOptions {
+ datasourceUid?: string;
+}
+
+export interface TraceToMetricsData extends DataSourceJsonData {
+ tracesToMetrics?: TraceToMetricsOptions;
+}
+
+interface Props extends DataSourcePluginOptionsEditorProps {}
+
+export function TraceToMetricsSettings({ options, onOptionsChange }: Props) {
+ const styles = useStyles(getStyles);
+
+ return (
+
+
Trace to metrics
+
+
+ Trace to metrics lets you navigate from a trace span to the selected data source.
+
+
+
+
+
+ updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
+ ...options.jsonData.tracesToMetrics,
+ datasourceUid: ds.uid,
+ })
+ }
+ />
+
+ {options.jsonData.tracesToMetrics?.datasourceUid ? (
+ {
+ updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
+ ...options.jsonData.tracesToMetrics,
+ datasourceUid: undefined,
+ });
+ }}
+ >
+ Clear
+
+ ) : null}
+
+
+ );
+}
+
+const getStyles = (theme: GrafanaTheme) => ({
+ infoText: css`
+ padding-bottom: ${theme.spacing.md};
+ color: ${theme.colors.textSemiWeak};
+ `,
+ row: css`
+ label: row;
+ align-items: baseline;
+ `,
+});
diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx
index ae36bb1b8d9..c0e09e3356f 100644
--- a/public/app/features/explore/TraceView/TraceView.tsx
+++ b/public/app/features/explore/TraceView/TraceView.tsx
@@ -20,6 +20,7 @@ import { getTemplateSrv } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { Trace, TracePageHeader, TraceTimelineViewer, TTraceTimeline } from '@jaegertracing/jaeger-ui-components';
import { TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
+import { TraceToMetricsData } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { StoreState } from 'app/types';
@@ -117,12 +118,20 @@ export function TraceView(props: Props) {
[childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, props.traceProp?.traceID]
);
- const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(datasource?.name)?.jsonData as TraceToLogsData)
- ?.tracesToLogs;
+ const instanceSettings = getDatasourceSrv().getInstanceSettings(datasource?.name);
+ const traceToLogsOptions = (instanceSettings?.jsonData as TraceToLogsData)?.tracesToLogs;
+ const traceToMetricsOptions = (instanceSettings?.jsonData as TraceToMetricsData)?.tracesToMetrics;
+
const createSpanLink = useMemo(
() =>
- createSpanLinkFactory({ splitOpenFn: props.splitOpenFn!, traceToLogsOptions, dataFrame: props.dataFrames[0] }),
- [props.splitOpenFn, traceToLogsOptions, props.dataFrames]
+ createSpanLinkFactory({
+ splitOpenFn: props.splitOpenFn!,
+ traceToLogsOptions,
+ traceToMetricsOptions,
+ dataFrame: props.dataFrames[0],
+ createFocusSpanLink,
+ }),
+ [props.splitOpenFn, traceToLogsOptions, traceToMetricsOptions, props.dataFrames, createFocusSpanLink]
);
const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]);
const timeZone = useSelector((state: StoreState) => getTimeZone(state.user));
diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts
index e88ce728b73..89d2e05c16a 100644
--- a/public/app/features/explore/TraceView/createSpanLink.test.ts
+++ b/public/app/features/explore/TraceView/createSpanLink.test.ts
@@ -1,6 +1,7 @@
import { DataSourceInstanceSettings, MutableDataFrame } from '@grafana/data';
import { setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
+import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { TraceToLogsOptions } from '../../../core/components/TraceToLogs/TraceToLogsSettings';
import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv';
@@ -9,10 +10,13 @@ import { TemplateSrv } from '../../templating/template_srv';
import { createSpanLinkFactory } from './createSpanLink';
describe('createSpanLinkFactory', () => {
- it('returns undefined if there is no data source uid', () => {
+ it('returns no links if there is no data source uid', () => {
const splitOpenFn = jest.fn();
const createLink = createSpanLinkFactory({ splitOpenFn: splitOpenFn });
- expect(createLink).not.toBeDefined();
+ const links = createLink!(createTraceSpan());
+ expect(links?.logLinks).toBeUndefined();
+ expect(links?.metricLinks).toBeUndefined();
+ expect(links?.traceLinks).toHaveLength(0);
});
describe('should return loki link', () => {
@@ -30,7 +34,9 @@ describe('createSpanLinkFactory', () => {
it('with default keys when tags not configured', () => {
const createLink = setupSpanLinkFactory();
expect(createLink).toBeDefined();
- const linkDef = createLink!(createTraceSpan());
+ const links = createLink!(createTraceSpan());
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}],"panelsState":{}}'
@@ -43,7 +49,7 @@ describe('createSpanLinkFactory', () => {
tags: ['ip', 'newTag'],
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(
+ const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@@ -54,6 +60,8 @@ describe('createSpanLinkFactory', () => {
},
})
);
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{ip=\\"192.168.0.1\\"}","refId":""}],"panelsState":{}}'
@@ -66,7 +74,7 @@ describe('createSpanLinkFactory', () => {
tags: ['ip', 'host'],
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(
+ const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@@ -77,6 +85,8 @@ describe('createSpanLinkFactory', () => {
},
})
);
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{ip=\\"192.168.0.1\\", host=\\"host\\"}","refId":""}],"panelsState":{}}'
@@ -90,7 +100,7 @@ describe('createSpanLinkFactory', () => {
spanEndTimeShift: '1m',
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(
+ const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@@ -101,6 +111,8 @@ describe('createSpanLinkFactory', () => {
},
})
);
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:01:00.000Z","to":"2020-10-14T01:01:01.000Z"},"datasource":"loki1","queries":[{"expr":"{hostname=\\"hostname1\\"}","refId":""}],"panelsState":{}}'
@@ -114,8 +126,10 @@ describe('createSpanLinkFactory', () => {
filterByTraceID: true,
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(createTraceSpan());
+ const links = createLink!(createTraceSpan());
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"} |=\\"7946b05c2e2e4e5a\\" |=\\"6605c7b08e715d6c\\"","refId":""}],"panelsState":{}}'
@@ -139,8 +153,10 @@ describe('createSpanLinkFactory', () => {
}),
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(createTraceSpan());
+ const links = createLink!(createTraceSpan());
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe('testSpanId');
});
@@ -153,7 +169,7 @@ describe('createSpanLinkFactory', () => {
],
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(
+ const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@@ -164,6 +180,9 @@ describe('createSpanLinkFactory', () => {
},
})
);
+
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{service=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}'
@@ -180,7 +199,7 @@ describe('createSpanLinkFactory', () => {
],
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(
+ const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@@ -191,6 +210,9 @@ describe('createSpanLinkFactory', () => {
},
})
);
+
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{service.name=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}'
@@ -203,7 +225,7 @@ describe('createSpanLinkFactory', () => {
tags: [],
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(
+ const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@@ -214,7 +236,7 @@ describe('createSpanLinkFactory', () => {
},
})
);
- expect(linkDef).toBeUndefined();
+ expect(links?.logLinks).toBeUndefined();
});
});
@@ -236,7 +258,10 @@ describe('createSpanLinkFactory', () => {
const createLink = setupSpanLinkFactory({
datasourceUid: splunkUID,
});
- const linkDef = createLink!(createTraceSpan());
+ const links = createLink!(createTraceSpan());
+
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toContain(`${encodeURIComponent('datasource":"Splunk 8","queries":[{"query"')}`);
expect(linkDef!.href).not.toContain(`${encodeURIComponent('datasource":"Splunk 8","queries":[{"expr"')}`);
});
@@ -245,7 +270,10 @@ describe('createSpanLinkFactory', () => {
const createLink = setupSpanLinkFactory({
datasourceUid: splunkUID,
});
- const linkDef = createLink!(createTraceSpan());
+ const links = createLink!(createTraceSpan());
+
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toContain(
`${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}`
);
@@ -262,8 +290,10 @@ describe('createSpanLinkFactory', () => {
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(createTraceSpan());
+ const links = createLink!(createTraceSpan());
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}],"panelsState":{}}'
@@ -276,7 +306,7 @@ describe('createSpanLinkFactory', () => {
tags: ['ip'],
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(
+ const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@@ -285,6 +315,8 @@ describe('createSpanLinkFactory', () => {
})
);
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}],"panelsState":{}}'
@@ -297,7 +329,7 @@ describe('createSpanLinkFactory', () => {
tags: ['ip', 'hostname'],
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(
+ const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@@ -309,6 +341,8 @@ describe('createSpanLinkFactory', () => {
})
);
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"hostname=\\"hostname1\\" ip=\\"192.168.0.1\\"","refId":""}],"panelsState":{}}'
@@ -325,7 +359,7 @@ describe('createSpanLinkFactory', () => {
],
});
expect(createLink).toBeDefined();
- const linkDef = createLink!(
+ const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@@ -336,6 +370,9 @@ describe('createSpanLinkFactory', () => {
},
})
);
+
+ const linkDef = links?.logLinks?.[0];
+ expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"service=\\"serviceName\\" pod=\\"podName\\"","refId":""}],"panelsState":{}}'
@@ -343,6 +380,94 @@ describe('createSpanLinkFactory', () => {
);
});
});
+
+ describe('should return metric link', () => {
+ beforeAll(() => {
+ setDataSourceSrv({
+ getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined {
+ return { uid: 'prom1', name: 'prom1', type: 'prometheus' } as any;
+ },
+ } as any);
+
+ setLinkSrv(new LinkSrv());
+ setTemplateSrv(new TemplateSrv());
+ });
+
+ it('returns query with span', () => {
+ const splitOpenFn = jest.fn();
+ const createLink = createSpanLinkFactory({
+ splitOpenFn,
+ traceToMetricsOptions: {
+ datasourceUid: 'prom1',
+ },
+ });
+ expect(createLink).toBeDefined();
+
+ const links = createLink!(createTraceSpan());
+ const linkDef = links?.metricLinks?.[0];
+
+ expect(linkDef).toBeDefined();
+ expect(linkDef!.href).toBe(
+ `/explore?left=${encodeURIComponent(
+ '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1","queries":[{"expr":"histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation=\\"operation\\"}[5m])) by (le))","refId":""}],"panelsState":{}}'
+ )}`
+ );
+ });
+ });
+
+ describe('should return span links', () => {
+ beforeAll(() => {
+ setDataSourceSrv(new DatasourceSrv());
+ setLinkSrv(new LinkSrv());
+ setTemplateSrv(new TemplateSrv());
+ });
+
+ it('ignores parent span link', () => {
+ const createLink = setupSpanLinkFactory();
+ expect(createLink).toBeDefined();
+ const links = createLink!(
+ createTraceSpan({ references: [{ refType: 'CHILD_OF', spanID: 'parent', traceID: 'traceID' }] })
+ );
+
+ const traceLinks = links?.traceLinks;
+ expect(traceLinks).toBeDefined();
+ expect(traceLinks).toHaveLength(0);
+ });
+
+ it('returns links for references and subsidiarilyReferencedBy references', () => {
+ const createLink = setupSpanLinkFactory();
+ expect(createLink).toBeDefined();
+ const links = createLink!(
+ createTraceSpan({
+ references: [
+ {
+ refType: 'FOLLOWS_FROM',
+ spanID: 'span1',
+ traceID: 'traceID',
+ span: { operationName: 'SpanName' } as any,
+ },
+ ],
+ subsidiarilyReferencedBy: [{ refType: 'FOLLOWS_FROM', spanID: 'span3', traceID: 'traceID2' }],
+ })
+ );
+
+ const traceLinks = links?.traceLinks;
+ expect(traceLinks).toBeDefined();
+ expect(traceLinks).toHaveLength(2);
+ expect(traceLinks![0]).toEqual(
+ expect.objectContaining({
+ href: 'traceID-span1',
+ title: 'SpanName',
+ })
+ );
+ expect(traceLinks![1]).toEqual(
+ expect.objectContaining({
+ href: 'traceID2-span3',
+ title: 'View linked span',
+ })
+ );
+ });
+ });
});
function setupSpanLinkFactory(options: Partial = {}, datasourceUid = 'lokiUid') {
@@ -353,6 +478,11 @@ function setupSpanLinkFactory(options: Partial = {}, datasou
datasourceUid,
...options,
},
+ createFocusSpanLink: (traceId, spanId) => {
+ return {
+ href: `${traceId}-${spanId}`,
+ } as any;
+ },
});
}
diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx
index a036eeb05d8..9eb6e5f075a 100644
--- a/public/app/features/explore/TraceView/createSpanLink.tsx
+++ b/public/app/features/explore/TraceView/createSpanLink.tsx
@@ -1,3 +1,4 @@
+import { SpanLinks } from '@jaegertracing/jaeger-ui-components/src/types/links';
import React from 'react';
import {
@@ -5,6 +6,7 @@ import {
DataLink,
DataQuery,
DataSourceInstanceSettings,
+ DataSourceJsonData,
dateTime,
Field,
KeyValue,
@@ -16,9 +18,11 @@ import {
} from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { Icon } from '@grafana/ui';
-import { SpanLinkDef, SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components';
+import { SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components';
import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
+import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+import { PromQuery } from 'app/plugins/datasource/prometheus/types';
import { LokiQuery } from '../../../plugins/datasource/loki/types';
import { getFieldLinksForExplore } from '../utils/links';
@@ -31,18 +35,22 @@ import { getFieldLinksForExplore } from '../utils/links';
export function createSpanLinkFactory({
splitOpenFn,
traceToLogsOptions,
+ traceToMetricsOptions,
dataFrame,
+ createFocusSpanLink,
}: {
splitOpenFn: SplitOpen;
traceToLogsOptions?: TraceToLogsOptions;
+ traceToMetricsOptions?: TraceToMetricsOptions;
dataFrame?: DataFrame;
+ createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel;
}): SpanLinkFunc | undefined {
if (!dataFrame || dataFrame.fields.length === 1 || !dataFrame.fields.some((f) => Boolean(f.config.links?.length))) {
// if the dataframe contains just a single blob of data (legacy format) or does not have any links configured,
// let's try to use the old legacy path.
- return legacyCreateSpanLinkFactory(splitOpenFn, traceToLogsOptions);
+ return legacyCreateSpanLinkFactory(splitOpenFn, traceToLogsOptions, traceToMetricsOptions, createFocusSpanLink);
} else {
- return function SpanLink(span: TraceSpan): SpanLinkDef | undefined {
+ return function SpanLink(span: TraceSpan): SpanLinks | undefined {
// We should be here only if there are some links in the dataframe
const field = dataFrame.fields.find((f) => Boolean(f.config.links?.length))!;
try {
@@ -55,9 +63,13 @@ export function createSpanLinkFactory({
});
return {
- href: links[0].href,
- onClick: links[0].onClick,
- content: ,
+ logLinks: [
+ {
+ href: links[0].href,
+ onClick: links[0].onClick,
+ content: ,
+ },
+ ],
};
} catch (error) {
// It's fairly easy to crash here for example if data source defines wrong interpolation in the data link
@@ -68,65 +80,144 @@ export function createSpanLinkFactory({
}
}
-function legacyCreateSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions?: TraceToLogsOptions) {
- // We should return if dataSourceUid is undefined otherwise getInstanceSettings would return testDataSource.
- if (!traceToLogsOptions?.datasourceUid) {
- return undefined;
+function legacyCreateSpanLinkFactory(
+ splitOpenFn: SplitOpen,
+ traceToLogsOptions?: TraceToLogsOptions,
+ traceToMetricsOptions?: TraceToMetricsOptions,
+ createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel
+) {
+ let logsDataSourceSettings: DataSourceInstanceSettings | undefined;
+ const isSplunkDS = logsDataSourceSettings?.type === 'grafana-splunk-datasource';
+ if (traceToLogsOptions?.datasourceUid) {
+ logsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToLogsOptions.datasourceUid);
}
- const dataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToLogsOptions.datasourceUid);
- const isSplunkDS = dataSourceSettings?.type === 'grafana-splunk-datasource';
-
- if (!dataSourceSettings) {
- return undefined;
+ let metricsDataSourceSettings: DataSourceInstanceSettings | undefined;
+ if (traceToMetricsOptions?.datasourceUid) {
+ metricsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToMetricsOptions.datasourceUid);
}
- return function SpanLink(span: TraceSpan): SpanLinkDef | undefined {
+ return function SpanLink(span: TraceSpan): SpanLinks {
+ const links: SpanLinks = { traceLinks: [] };
// This is reusing existing code from derived fields which may not be ideal match so some data is a bit faked at
// the moment. Issue is that the trace itself isn't clearly mapped to dataFrame (right now it's just a json blob
// inside a single field) so the dataLinks as config of that dataFrame abstraction breaks down a bit and we do
// it manually here instead of leaving it for the data source to supply the config.
let dataLink: DataLink | undefined = {} as DataLink | undefined;
- let link: LinkModel;
- switch (dataSourceSettings?.type) {
- case 'loki':
- dataLink = getLinkForLoki(span, traceToLogsOptions, dataSourceSettings);
- if (!dataLink) {
- return undefined;
- }
- break;
- case 'grafana-splunk-datasource':
- dataLink = getLinkForSplunk(span, traceToLogsOptions, dataSourceSettings);
- break;
- default:
- return undefined;
+ // Get logs link
+ if (logsDataSourceSettings && traceToLogsOptions) {
+ switch (logsDataSourceSettings?.type) {
+ case 'loki':
+ dataLink = getLinkForLoki(span, traceToLogsOptions, logsDataSourceSettings);
+ break;
+ case 'grafana-splunk-datasource':
+ dataLink = getLinkForSplunk(span, traceToLogsOptions, logsDataSourceSettings);
+ break;
+ }
+
+ if (dataLink) {
+ const link = mapInternalLinkToExplore({
+ link: dataLink,
+ internalLink: dataLink.internal!,
+ scopedVars: {},
+ range: getTimeRangeFromSpan(
+ span,
+ {
+ startMs: traceToLogsOptions.spanStartTimeShift
+ ? rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift)
+ : 0,
+ endMs: traceToLogsOptions.spanEndTimeShift
+ ? rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift)
+ : 0,
+ },
+ isSplunkDS
+ ),
+ field: {} as Field,
+ onClickFn: splitOpenFn,
+ replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
+ });
+
+ links.logLinks = [
+ {
+ href: link.href,
+ onClick: link.onClick,
+ content: ,
+ },
+ ];
+ }
}
- link = mapInternalLinkToExplore({
- link: dataLink,
- internalLink: dataLink?.internal!,
- scopedVars: {},
- range: getTimeRangeFromSpan(
- span,
- {
- startMs: traceToLogsOptions.spanStartTimeShift
- ? rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift)
- : 0,
- endMs: traceToLogsOptions.spanEndTimeShift ? rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift) : 0,
+ // Get metrics links
+ if (metricsDataSourceSettings && traceToMetricsOptions) {
+ const dataLink: DataLink = {
+ title: metricsDataSourceSettings.name,
+ url: '',
+ internal: {
+ datasourceUid: metricsDataSourceSettings.uid,
+ datasourceName: metricsDataSourceSettings.name,
+ query: {
+ expr: `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`,
+ refId: '',
+ },
},
- isSplunkDS
- ),
- field: {} as Field,
- onClickFn: splitOpenFn,
- replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
- });
+ };
- return {
- href: link.href,
- onClick: link.onClick,
- content: ,
- };
+ const link = mapInternalLinkToExplore({
+ link: dataLink,
+ internalLink: dataLink.internal!,
+ scopedVars: {},
+ range: getTimeRangeFromSpan(span, {
+ startMs: 0,
+ endMs: 0,
+ }),
+ field: {} as Field,
+ onClickFn: splitOpenFn,
+ replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
+ });
+
+ links.metricLinks = [
+ {
+ href: link.href,
+ onClick: link.onClick,
+ content: ,
+ },
+ ];
+ }
+
+ // Get trace links
+ if (span.references && createFocusSpanLink) {
+ for (const reference of span.references) {
+ // Ignore parent-child links
+ if (reference.refType === 'CHILD_OF') {
+ continue;
+ }
+
+ const link = createFocusSpanLink(reference.traceID, reference.spanID);
+
+ links.traceLinks!.push({
+ href: link.href,
+ title: reference.span ? reference.span.operationName : 'View linked span',
+ content: ,
+ onClick: link.onClick,
+ });
+ }
+ }
+
+ if (span.subsidiarilyReferencedBy && createFocusSpanLink) {
+ for (const reference of span.subsidiarilyReferencedBy) {
+ const link = createFocusSpanLink(reference.traceID, reference.spanID);
+
+ links.traceLinks!.push({
+ href: link.href,
+ title: reference.span ? reference.span.operationName : 'View linked span',
+ content: ,
+ onClick: link.onClick,
+ });
+ }
+ }
+
+ return links;
};
}
diff --git a/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx b/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx
index 114a7ef8a3e..46fcb204394 100644
--- a/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx
+++ b/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx
@@ -1,9 +1,11 @@
import React from 'react';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
+import { config } from '@grafana/runtime';
import { DataSourceHttpSettings } from '@grafana/ui';
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
+import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
export type Props = DataSourcePluginOptionsEditorProps;
@@ -20,6 +22,13 @@ export const ConfigEditor: React.FC = ({ options, onOptionsChange }) => {
+
+ {config.featureToggles.traceToMetrics ? (
+
+
+
+ ) : null}
+
diff --git a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
index fc21868589b..618c0bc28a3 100644
--- a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
+++ b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
@@ -5,6 +5,7 @@ import { config } from '@grafana/runtime';
import { DataSourceHttpSettings } from '@grafana/ui';
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
+import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
import { LokiSearchSettings } from './LokiSearchSettings';
import { SearchSettings } from './SearchSettings';
@@ -25,19 +26,29 @@ export const ConfigEditor: React.FC = ({ options, onOptionsChange }) => {
+
+ {config.featureToggles.traceToMetrics ? (
+
+
+
+ ) : null}
+
{config.featureToggles.tempoServiceGraph && (
)}
+
{config.featureToggles.tempoSearch && (
)}
+
+
diff --git a/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx b/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx
index 8e5712ef9f7..b5c40fc8464 100644
--- a/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx
+++ b/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx
@@ -39,19 +39,21 @@ export function ServiceGraphSettings({ options, onOptionsChange }: Props) {
}
/>
- {
- updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'serviceMap', {
- datasourceUid: undefined,
- });
- }}
- >
- Clear
-
+ {options.jsonData.serviceMap?.datasourceUid ? (
+ {
+ updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'serviceMap', {
+ datasourceUid: undefined,
+ });
+ }}
+ >
+ Clear
+
+ ) : null}
);
diff --git a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx
index 267dabcf9fc..fc876e5a91e 100644
--- a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx
+++ b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx
@@ -1,9 +1,11 @@
import React from 'react';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
+import { config } from '@grafana/runtime';
import { DataSourceHttpSettings } from '@grafana/ui';
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
+import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
export type Props = DataSourcePluginOptionsEditorProps;
@@ -21,6 +23,12 @@ export const ConfigEditor: React.FC