mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -06:00
Tempo: Trace to logs custom query with interpolation (#61702)
This commit is contained in:
parent
931dcda559
commit
a414b07991
@ -697,13 +697,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "10"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
|
||||
],
|
||||
"packages/grafana-data/src/utils/dataLinks.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"packages/grafana-data/src/utils/dataLinks.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"packages/grafana-data/src/utils/datasource.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@ -1717,10 +1712,6 @@ exports[`better eslint`] = {
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/TimelineHeaderRow/TimelineViewingLayer.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@ -3894,14 +3885,7 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/features/explore/TraceView/createSpanLink.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "7"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "8"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/explore/spec/helper/setup.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
|
@ -642,7 +642,7 @@ describe('getLinksSupplier', () => {
|
||||
);
|
||||
|
||||
const links = supplier({ valueRowIndex: 0 });
|
||||
const encodeURIParams = `{"datasource":"${datasourceUid}","queries":["12345"],"panelsState":{}}`;
|
||||
const encodeURIParams = `{"datasource":"${datasourceUid}","queries":["12345"]}`;
|
||||
expect(links.length).toBe(1);
|
||||
expect(links[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataLink, FieldType } from '../types';
|
||||
import { DataLink, FieldType, TimeRange } from '../types';
|
||||
import { ArrayVector } from '../vector';
|
||||
|
||||
import { mapInternalLinkToExplore } from './dataLinks';
|
||||
@ -19,7 +19,7 @@ describe('mapInternalLinkToExplore', () => {
|
||||
link: dataLink,
|
||||
internalLink: dataLink.internal,
|
||||
scopedVars: {},
|
||||
range: {} as any,
|
||||
range: {} as unknown as TimeRange,
|
||||
field: {
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
@ -32,9 +32,7 @@ describe('mapInternalLinkToExplore', () => {
|
||||
expect(link).toEqual(
|
||||
expect.objectContaining({
|
||||
title: 'dsName',
|
||||
href: `/explore?left=${encodeURIComponent(
|
||||
'{"datasource":"uid","queries":[{"query":"12344"}],"panelsState":{}}'
|
||||
)}`,
|
||||
href: `/explore?left=${encodeURIComponent('{"datasource":"uid","queries":[{"query":"12344"}]}')}`,
|
||||
onClick: undefined,
|
||||
})
|
||||
);
|
||||
@ -62,7 +60,7 @@ describe('mapInternalLinkToExplore', () => {
|
||||
link: dataLink,
|
||||
internalLink: dataLink.internal!,
|
||||
scopedVars: {},
|
||||
range: {} as any,
|
||||
range: {} as unknown as TimeRange,
|
||||
field: {
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
@ -82,4 +80,56 @@ describe('mapInternalLinkToExplore', () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('interpolates query correctly', () => {
|
||||
const dataLink = {
|
||||
url: '',
|
||||
title: '',
|
||||
internal: {
|
||||
datasourceUid: 'uid',
|
||||
datasourceName: 'dsName',
|
||||
query: {
|
||||
query: '$var $var',
|
||||
// Should not interpolate keys
|
||||
$var: 'foo',
|
||||
nested: {
|
||||
something: '$var',
|
||||
},
|
||||
num: 1,
|
||||
arr: ['$var', 'non var'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const link = mapInternalLinkToExplore({
|
||||
link: dataLink,
|
||||
internalLink: dataLink.internal,
|
||||
scopedVars: {
|
||||
var1: { text: '', value: 'val1' },
|
||||
},
|
||||
range: {} as unknown as TimeRange,
|
||||
field: {
|
||||
name: 'test',
|
||||
type: FieldType.number,
|
||||
config: {},
|
||||
values: new ArrayVector([2]),
|
||||
},
|
||||
replaceVariables: (val, scopedVars) => val.replace(/\$var/g, scopedVars!['var1'].value),
|
||||
});
|
||||
|
||||
expect(decodeURIComponent(link.href)).toEqual(
|
||||
`/explore?left=${JSON.stringify({
|
||||
datasource: 'uid',
|
||||
queries: [
|
||||
{
|
||||
query: 'val1 val1',
|
||||
$var: 'foo',
|
||||
nested: { something: 'val1' },
|
||||
num: 1,
|
||||
arr: ['val1', 'non var'],
|
||||
},
|
||||
],
|
||||
})}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -88,30 +88,38 @@ function generateInternalHref<T extends DataQuery = any>(
|
||||
);
|
||||
}
|
||||
|
||||
function interpolateObject<T extends object>(
|
||||
object: T | undefined,
|
||||
function interpolateObject<T>(
|
||||
obj: T | undefined,
|
||||
scopedVars: ScopedVars,
|
||||
replaceVariables: InterpolateFunction
|
||||
): T | undefined {
|
||||
if (!obj) {
|
||||
return obj;
|
||||
}
|
||||
if (typeof obj === 'string') {
|
||||
// @ts-ignore this is complaining we are returning string, but we are checking if obj is a string so should be fine.
|
||||
return replaceVariables(obj, scopedVars);
|
||||
}
|
||||
const copy = JSON.parse(JSON.stringify(obj));
|
||||
return interpolateObjectRecursive(copy, scopedVars, replaceVariables);
|
||||
}
|
||||
|
||||
function interpolateObjectRecursive<T extends Object>(
|
||||
obj: T,
|
||||
scopedVars: ScopedVars,
|
||||
replaceVariables: InterpolateFunction
|
||||
): T {
|
||||
let stringifiedQuery = '';
|
||||
try {
|
||||
stringifiedQuery = JSON.stringify(object || {});
|
||||
} catch (err) {
|
||||
// should not happen and not much to do about this, possibly something non stringifiable in the query
|
||||
console.error(err);
|
||||
for (const k of Object.keys(obj)) {
|
||||
// Honestly not sure how to type this to make TS happy.
|
||||
// @ts-ignore
|
||||
if (typeof obj[k] === 'string') {
|
||||
// @ts-ignore
|
||||
obj[k] = replaceVariables(obj[k], scopedVars);
|
||||
// @ts-ignore
|
||||
} else if (typeof obj[k] === 'object' && obj[k] !== null) {
|
||||
// @ts-ignore
|
||||
obj[k] = interpolateObjectRecursive(obj[k], scopedVars, replaceVariables);
|
||||
}
|
||||
}
|
||||
|
||||
// Replace any variables inside the query. This may not be the safest as it can also replace keys etc so may not
|
||||
// actually work with every datasource query right now.
|
||||
stringifiedQuery = replaceVariables(stringifiedQuery, scopedVars);
|
||||
|
||||
let replacedQuery = {} as T;
|
||||
try {
|
||||
replacedQuery = JSON.parse(stringifiedQuery);
|
||||
} catch (err) {
|
||||
// again should not happen and not much to do about this, probably some issue with how we replaced the variables.
|
||||
console.error(stringifiedQuery, err);
|
||||
}
|
||||
|
||||
return replacedQuery;
|
||||
return obj;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import IoLink from 'react-icons/lib/io/link';
|
||||
|
||||
import { dateTimeFormat, GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { DataLinkButton, TextArea, useStyles2 } from '@grafana/ui';
|
||||
import { Button, DataLinkButton, TextArea, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { autoColor } from '../../Theme';
|
||||
import { Divider } from '../../common/Divider';
|
||||
@ -194,22 +194,48 @@ export default function SpanDetail(props: SpanDetailProps) {
|
||||
: []),
|
||||
];
|
||||
const styles = useStyles2(getStyles);
|
||||
const links = createSpanLink?.(span);
|
||||
const focusSpanLink = createFocusSpanLink(traceID, spanID);
|
||||
const logLink = links?.logLinks?.[0]
|
||||
? {
|
||||
...links?.logLinks?.[0],
|
||||
onClick: (event: React.MouseEvent) => {
|
||||
reportInteraction('grafana_traces_trace_view_span_link_clicked', {
|
||||
datasourceType: datasourceType,
|
||||
type: 'log',
|
||||
location: 'spanDetails',
|
||||
});
|
||||
links?.logLinks?.[0].onClick!(event);
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
let logLinkButton: JSX.Element | undefined = undefined;
|
||||
if (createSpanLink) {
|
||||
const links = createSpanLink(span);
|
||||
if (links?.logLinks) {
|
||||
logLinkButton = (
|
||||
<DataLinkButton
|
||||
link={{
|
||||
...links.logLinks[0],
|
||||
title: 'Logs for this span',
|
||||
target: '_blank',
|
||||
origin: links.logLinks[0].field,
|
||||
onClick: (event: React.MouseEvent) => {
|
||||
reportInteraction('grafana_traces_trace_view_span_link_clicked', {
|
||||
datasourceType: datasourceType,
|
||||
type: 'log',
|
||||
location: 'spanDetails',
|
||||
});
|
||||
links?.logLinks?.[0].onClick?.(event);
|
||||
},
|
||||
}}
|
||||
buttonProps={{ icon: 'gf-logs' }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
logLinkButton = (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={'gf-logs'}
|
||||
disabled
|
||||
tooltip={
|
||||
'We did not match any variables between the link and this span. Check your configuration or this span attributes.'
|
||||
}
|
||||
>
|
||||
Logs for this span
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const focusSpanLink = createFocusSpanLink(traceID, spanID);
|
||||
return (
|
||||
<div data-testid="span-detail-component">
|
||||
<div className={styles.header}>
|
||||
@ -218,11 +244,7 @@ export default function SpanDetail(props: SpanDetailProps) {
|
||||
<LabeledList className={ubTxRightAlign} divider={true} items={overviewItems} />
|
||||
</div>
|
||||
</div>
|
||||
{links?.logLinks?.[0] ? (
|
||||
<>
|
||||
<DataLinkButton link={{ ...logLink, title: 'Logs for this span' } as any} buttonProps={{ icon: 'gf-logs' }} />
|
||||
</>
|
||||
) : null}
|
||||
{logLinkButton}
|
||||
<Divider className={ubMy1} type={'horizontal'} />
|
||||
<div>
|
||||
<div>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Field } from '@grafana/data';
|
||||
|
||||
import { TraceSpan } from './trace';
|
||||
|
||||
export type SpanLinkDef = {
|
||||
@ -7,6 +9,7 @@ export type SpanLinkDef = {
|
||||
onClick?: (event: any) => void;
|
||||
content: React.ReactNode;
|
||||
title?: string;
|
||||
field: Field;
|
||||
};
|
||||
|
||||
export type SpanLinks = {
|
||||
|
@ -1,26 +1,16 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2, KeyValue } from '@grafana/data';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SegmentInput, useStyles2, InlineLabel, Icon } from '@grafana/ui';
|
||||
|
||||
const EQ_WIDTH = 3; // = 24px in inline label
|
||||
|
||||
interface Props {
|
||||
values: Array<KeyValue<string>>;
|
||||
onChange: (values: Array<KeyValue<string>>) => void;
|
||||
values: Array<{ key: string; value?: string }>;
|
||||
onChange: (values: Array<{ key: string; value?: string }>) => void;
|
||||
id?: string;
|
||||
keyPlaceholder?: string;
|
||||
valuePlaceholder?: string;
|
||||
}
|
||||
|
||||
const KeyValueInput = ({
|
||||
values,
|
||||
onChange,
|
||||
id,
|
||||
keyPlaceholder = 'Key',
|
||||
valuePlaceholder = 'Value (optional)',
|
||||
}: Props) => {
|
||||
export const TagMappingInput = ({ values, onChange, id }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
@ -30,7 +20,7 @@ const KeyValueInput = ({
|
||||
<div className={styles.pair} key={idx}>
|
||||
<SegmentInput
|
||||
id={`${id}-key-${idx}`}
|
||||
placeholder={keyPlaceholder}
|
||||
placeholder={'Tag name'}
|
||||
value={value.key}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
@ -43,13 +33,13 @@ const KeyValueInput = ({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<InlineLabel aria-label="equals" className={styles.operator} width={EQ_WIDTH}>
|
||||
=
|
||||
<InlineLabel aria-label="equals" className={styles.operator}>
|
||||
as
|
||||
</InlineLabel>
|
||||
<SegmentInput
|
||||
id={`${id}-value-${idx}`}
|
||||
placeholder={valuePlaceholder}
|
||||
value={value.value}
|
||||
placeholder={'New name (optional)'}
|
||||
value={value.value || ''}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
values.map((v, i) => {
|
||||
@ -95,8 +85,6 @@ const KeyValueInput = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyValueInput;
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
@ -110,5 +98,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
`,
|
||||
operator: css`
|
||||
color: ${theme.v1.palette.orange};
|
||||
width: auto;
|
||||
`,
|
||||
});
|
@ -0,0 +1,123 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceSettings } from '@grafana/data';
|
||||
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
|
||||
import { TraceToLogsData, TraceToLogsSettings } from './TraceToLogsSettings';
|
||||
|
||||
const defaultOptionsOldFormat: DataSourceSettings<TraceToLogsData> = {
|
||||
jsonData: {
|
||||
tracesToLogs: {
|
||||
datasourceUid: 'loki1_uid',
|
||||
tags: ['someTag'],
|
||||
mapTagNamesEnabled: false,
|
||||
spanStartTimeShift: '1m',
|
||||
spanEndTimeShift: '1m',
|
||||
filterByTraceID: true,
|
||||
filterBySpanID: true,
|
||||
},
|
||||
},
|
||||
} as unknown as DataSourceSettings<TraceToLogsData>;
|
||||
|
||||
const defaultOptionsNewFormat: DataSourceSettings<TraceToLogsData> = {
|
||||
jsonData: {
|
||||
tracesToLogsV2: {
|
||||
datasourceUid: 'loki1_uid',
|
||||
tags: [{ key: 'someTag', value: 'newName' }],
|
||||
spanStartTimeShift: '1m',
|
||||
spanEndTimeShift: '1m',
|
||||
filterByTraceID: true,
|
||||
filterBySpanID: true,
|
||||
customQuery: true,
|
||||
query: '{${__tags}}',
|
||||
},
|
||||
},
|
||||
} as unknown as DataSourceSettings<TraceToLogsData>;
|
||||
|
||||
const lokiSettings = {
|
||||
uid: 'loki1_uid',
|
||||
name: 'loki1',
|
||||
type: 'loki',
|
||||
meta: { info: { logos: { small: '' } } },
|
||||
} as unknown as DataSourceInstanceSettings;
|
||||
|
||||
describe('TraceToLogsSettings', () => {
|
||||
beforeAll(() => {
|
||||
setDataSourceSrv({
|
||||
getList() {
|
||||
return [lokiSettings];
|
||||
},
|
||||
getInstanceSettings() {
|
||||
return lokiSettings;
|
||||
},
|
||||
} as unknown as DataSourceSrv);
|
||||
});
|
||||
|
||||
it('should render old format without error', () => {
|
||||
expect(() =>
|
||||
render(<TraceToLogsSettings options={defaultOptionsOldFormat} onOptionsChange={() => {}} />)
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should render new format without error', () => {
|
||||
expect(() =>
|
||||
render(<TraceToLogsSettings options={defaultOptionsNewFormat} onOptionsChange={() => {}} />)
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should render and transform data from old format correctly', () => {
|
||||
render(<TraceToLogsSettings options={defaultOptionsOldFormat} onOptionsChange={() => {}} />);
|
||||
expect(screen.getByText('someTag')).toBeInTheDocument();
|
||||
expect((screen.getByLabelText('Use custom query') as HTMLInputElement).checked).toBeFalsy();
|
||||
expect((screen.getByLabelText('Filter by trace ID') as HTMLInputElement).checked).toBeTruthy();
|
||||
expect((screen.getByLabelText('Filter by span ID') as HTMLInputElement).checked).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders old mapped tags correctly', () => {
|
||||
const options = {
|
||||
...defaultOptionsOldFormat,
|
||||
jsonData: {
|
||||
...defaultOptionsOldFormat.jsonData,
|
||||
tracesToLogs: {
|
||||
...defaultOptionsOldFormat.jsonData.tracesToLogs,
|
||||
tags: undefined,
|
||||
mappedTags: [{ key: 'someTag', value: 'withNewName' }],
|
||||
mapTagNamesEnabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
render(<TraceToLogsSettings options={options} onOptionsChange={() => {}} />);
|
||||
expect(screen.getByText('someTag')).toBeInTheDocument();
|
||||
expect(screen.getByText('withNewName')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('transforms old format to new on change', async () => {
|
||||
const changeMock = jest.fn();
|
||||
render(<TraceToLogsSettings options={defaultOptionsOldFormat} onOptionsChange={changeMock} />);
|
||||
const checkBox = screen.getByLabelText('Filter by trace ID');
|
||||
await userEvent.click(checkBox);
|
||||
expect(changeMock.mock.calls[0]).toEqual([
|
||||
{
|
||||
jsonData: {
|
||||
tracesToLogs: undefined,
|
||||
tracesToLogsV2: {
|
||||
customQuery: false,
|
||||
datasourceUid: 'loki1_uid',
|
||||
filterBySpanID: true,
|
||||
filterByTraceID: false,
|
||||
spanEndTimeShift: '1m',
|
||||
spanStartTimeShift: '1m',
|
||||
tags: [
|
||||
{
|
||||
key: 'someTag',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -1,23 +1,22 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
DataSourceJsonData,
|
||||
DataSourceInstanceSettings,
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
GrafanaTheme2,
|
||||
KeyValue,
|
||||
updateDatasourcePluginJsonDataOption,
|
||||
} from '@grafana/data';
|
||||
import { DataSourcePicker } from '@grafana/runtime';
|
||||
import { InlineField, InlineFieldRow, Input, TagsInput, useStyles2, InlineSwitch } from '@grafana/ui';
|
||||
import { InlineField, InlineFieldRow, Input, useStyles2, InlineSwitch } from '@grafana/ui';
|
||||
|
||||
import KeyValueInput from './KeyValueInput';
|
||||
import { TagMappingInput } from './TagMappingInput';
|
||||
|
||||
// @deprecated use getTraceToLogsOptions to get the v2 version of this config from jsonData
|
||||
export interface TraceToLogsOptions {
|
||||
datasourceUid?: string;
|
||||
tags?: string[];
|
||||
mappedTags?: Array<KeyValue<string>>;
|
||||
mappedTags?: Array<{ key: string; value?: string }>;
|
||||
mapTagNamesEnabled?: boolean;
|
||||
spanStartTimeShift?: string;
|
||||
spanEndTimeShift?: string;
|
||||
@ -26,8 +25,45 @@ export interface TraceToLogsOptions {
|
||||
lokiSearch?: boolean; // legacy
|
||||
}
|
||||
|
||||
export interface TraceToLogsOptionsV2 {
|
||||
datasourceUid?: string;
|
||||
tags?: Array<{ key: string; value?: string }>;
|
||||
spanStartTimeShift?: string;
|
||||
spanEndTimeShift?: string;
|
||||
filterByTraceID?: boolean;
|
||||
filterBySpanID?: boolean;
|
||||
query?: string;
|
||||
customQuery: boolean;
|
||||
}
|
||||
|
||||
export interface TraceToLogsData extends DataSourceJsonData {
|
||||
tracesToLogs?: TraceToLogsOptions;
|
||||
tracesToLogsV2?: TraceToLogsOptionsV2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets new version of the traceToLogs config from the json data either returning directly or transforming the old
|
||||
* version to new and returning that.
|
||||
*/
|
||||
export function getTraceToLogsOptions(data?: TraceToLogsData): TraceToLogsOptionsV2 | undefined {
|
||||
if (data?.tracesToLogsV2) {
|
||||
return data.tracesToLogsV2;
|
||||
}
|
||||
if (!data?.tracesToLogs) {
|
||||
return undefined;
|
||||
}
|
||||
const traceToLogs: TraceToLogsOptionsV2 = {
|
||||
customQuery: false,
|
||||
};
|
||||
traceToLogs.datasourceUid = data.tracesToLogs.datasourceUid;
|
||||
traceToLogs.tags = data.tracesToLogs.mapTagNamesEnabled
|
||||
? data.tracesToLogs.mappedTags
|
||||
: data.tracesToLogs.tags?.map((tag) => ({ key: tag }));
|
||||
traceToLogs.filterByTraceID = data.tracesToLogs.filterByTraceID;
|
||||
traceToLogs.filterBySpanID = data.tracesToLogs.filterBySpanID;
|
||||
traceToLogs.spanStartTimeShift = data.tracesToLogs.spanStartTimeShift;
|
||||
traceToLogs.spanEndTimeShift = data.tracesToLogs.spanEndTimeShift;
|
||||
return traceToLogs;
|
||||
}
|
||||
|
||||
interface Props extends DataSourcePluginOptionsEditorProps<TraceToLogsData> {}
|
||||
@ -41,6 +77,31 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
|
||||
'grafana-opensearch-datasource', // external
|
||||
];
|
||||
|
||||
const traceToLogs = useMemo(
|
||||
(): TraceToLogsOptionsV2 => getTraceToLogsOptions(options.jsonData) || { customQuery: false },
|
||||
[options.jsonData]
|
||||
);
|
||||
const { query = '', tags, customQuery } = traceToLogs;
|
||||
|
||||
const updateTracesToLogs = useCallback(
|
||||
(value: Partial<TraceToLogsOptionsV2>) => {
|
||||
// Cannot use updateDatasourcePluginJsonDataOption here as we need to update 2 keys, and they would overwrite each
|
||||
// other as updateDatasourcePluginJsonDataOption isn't synchronized
|
||||
onOptionsChange({
|
||||
...options,
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
tracesToLogsV2: {
|
||||
...traceToLogs,
|
||||
...value,
|
||||
},
|
||||
tracesToLogs: undefined,
|
||||
},
|
||||
});
|
||||
},
|
||||
[onOptionsChange, options, traceToLogs]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={css({ width: '100%' })}>
|
||||
<h3 className="page-heading">Trace to logs</h3>
|
||||
@ -54,171 +115,143 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
|
||||
<DataSourcePicker
|
||||
inputId="trace-to-logs-data-source-picker"
|
||||
filter={(ds) => supportedDataSourceTypes.includes(ds.type)}
|
||||
current={options.jsonData.tracesToLogs?.datasourceUid}
|
||||
current={traceToLogs.datasourceUid}
|
||||
noDefault={true}
|
||||
width={40}
|
||||
onChange={(ds: DataSourceInstanceSettings) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
...options.jsonData.tracesToLogs,
|
||||
updateTracesToLogs({
|
||||
datasourceUid: ds.uid,
|
||||
tags: options.jsonData.tracesToLogs?.tags,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
||||
{options.jsonData.tracesToLogs?.mapTagNamesEnabled ? (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
tooltip="Tags that will be used in the Loki query. Default tags: 'cluster', 'hostname', 'namespace', 'pod'"
|
||||
label="Tags"
|
||||
labelWidth={26}
|
||||
>
|
||||
<KeyValueInput
|
||||
keyPlaceholder="Tag"
|
||||
values={
|
||||
options.jsonData.tracesToLogs?.mappedTags ??
|
||||
options.jsonData.tracesToLogs?.tags?.map((tag) => ({ key: tag })) ??
|
||||
[]
|
||||
}
|
||||
onChange={(v) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
...options.jsonData.tracesToLogs,
|
||||
mappedTags: v,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
) : (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
tooltip="Tags that will be used in the Loki query. Default tags: 'cluster', 'hostname', 'namespace', 'pod'"
|
||||
label="Tags"
|
||||
labelWidth={26}
|
||||
>
|
||||
<TagsInput
|
||||
tags={options.jsonData.tracesToLogs?.tags}
|
||||
width={40}
|
||||
onChange={(tags) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
...options.jsonData.tracesToLogs,
|
||||
tags: tags,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<TimeRangeShift
|
||||
type={'start'}
|
||||
value={traceToLogs.spanStartTimeShift || ''}
|
||||
onChange={(val) => updateTracesToLogs({ spanStartTimeShift: val })}
|
||||
/>
|
||||
<TimeRangeShift
|
||||
type={'end'}
|
||||
value={traceToLogs.spanEndTimeShift || ''}
|
||||
onChange={(val) => updateTracesToLogs({ spanEndTimeShift: val })}
|
||||
/>
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
tooltip="Tags that will be used in the query. Default tags: 'cluster', 'hostname', 'namespace', 'pod'"
|
||||
label="Tags"
|
||||
labelWidth={26}
|
||||
>
|
||||
<TagMappingInput values={tags ?? []} onChange={(v) => updateTracesToLogs({ tags: v })} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
||||
<IdFilter
|
||||
disabled={customQuery}
|
||||
type={'trace'}
|
||||
id={'filterByTraceID'}
|
||||
value={Boolean(traceToLogs.filterByTraceID)}
|
||||
onChange={(val) => updateTracesToLogs({ filterByTraceID: val })}
|
||||
/>
|
||||
<IdFilter
|
||||
disabled={customQuery}
|
||||
type={'span'}
|
||||
id={'filterBySpanID'}
|
||||
value={Boolean(traceToLogs.filterBySpanID)}
|
||||
onChange={(val) => updateTracesToLogs({ filterBySpanID: val })}
|
||||
/>
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
tooltip="Use custom query with possibility to interpolate variables from the trace or span."
|
||||
label="Use custom query"
|
||||
labelWidth={26}
|
||||
>
|
||||
<InlineSwitch
|
||||
id={'customQuerySwitch'}
|
||||
value={customQuery}
|
||||
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
|
||||
updateTracesToLogs({ customQuery: event.currentTarget.checked })
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
||||
{customQuery && (
|
||||
<InlineField
|
||||
label="Query"
|
||||
labelWidth={26}
|
||||
tooltip="The query that will run when navigating from a trace to logs data source. Interpolate tags using the `$__tags` keyword."
|
||||
grow
|
||||
>
|
||||
<Input
|
||||
label="Query"
|
||||
type="text"
|
||||
allowFullScreen
|
||||
value={query}
|
||||
onChange={(e) => updateTracesToLogs({ query: e.currentTarget.value })}
|
||||
/>
|
||||
</InlineField>
|
||||
)}
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Map tag names"
|
||||
labelWidth={26}
|
||||
grow
|
||||
tooltip="Map trace tag names to log label names. Ex: k8s.pod.name -> pod"
|
||||
>
|
||||
<InlineSwitch
|
||||
id="mapTagNames"
|
||||
value={options.jsonData.tracesToLogs?.mapTagNamesEnabled ?? false}
|
||||
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
...options.jsonData.tracesToLogs,
|
||||
mapTagNamesEnabled: event.currentTarget.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Span start time shift"
|
||||
labelWidth={26}
|
||||
grow
|
||||
tooltip="Shifts the start time of the span. Default 0 (Time units can be used here, for example: 5s, 1m, 3h)"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="1h"
|
||||
width={40}
|
||||
onChange={(v) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
...options.jsonData.tracesToLogs,
|
||||
spanStartTimeShift: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
value={options.jsonData.tracesToLogs?.spanStartTimeShift || ''}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Span end time shift"
|
||||
labelWidth={26}
|
||||
grow
|
||||
tooltip="Shifts the end time of the span. Default 0 Time units can be used here, for example: 5s, 1m, 3h"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="1h"
|
||||
width={40}
|
||||
onChange={(v) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
...options.jsonData.tracesToLogs,
|
||||
spanEndTimeShift: v.currentTarget.value,
|
||||
})
|
||||
}
|
||||
value={options.jsonData.tracesToLogs?.spanEndTimeShift || ''}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Filter by Trace ID"
|
||||
labelWidth={26}
|
||||
grow
|
||||
tooltip="Filters logs by Trace ID. Appends '|=<trace id>' to the query."
|
||||
>
|
||||
<InlineSwitch
|
||||
id="filterByTraceID"
|
||||
value={options.jsonData.tracesToLogs?.filterByTraceID}
|
||||
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
...options.jsonData.tracesToLogs,
|
||||
filterByTraceID: event.currentTarget.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label="Filter by Span ID"
|
||||
labelWidth={26}
|
||||
grow
|
||||
tooltip="Filters logs by Span ID. Appends '|=<span id>' to the query."
|
||||
>
|
||||
<InlineSwitch
|
||||
id="filterBySpanID"
|
||||
value={options.jsonData.tracesToLogs?.filterBySpanID}
|
||||
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
|
||||
...options.jsonData.tracesToLogs,
|
||||
filterBySpanID: event.currentTarget.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface IdFilterProps {
|
||||
type: 'trace' | 'span';
|
||||
id: string;
|
||||
value: boolean;
|
||||
onChange: (val: boolean) => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
function IdFilter(props: IdFilterProps) {
|
||||
return (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
disabled={props.disabled}
|
||||
label={`Filter by ${props.type} ID`}
|
||||
labelWidth={26}
|
||||
grow
|
||||
tooltip={`Filters logs by ${props.type} ID`}
|
||||
>
|
||||
<InlineSwitch
|
||||
id={props.id}
|
||||
value={props.value}
|
||||
onChange={(event: React.SyntheticEvent<HTMLInputElement>) => props.onChange(event.currentTarget.checked)}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
);
|
||||
}
|
||||
|
||||
interface TimeRangeShiftProps {
|
||||
type: 'start' | 'end';
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
}
|
||||
function TimeRangeShift(props: TimeRangeShiftProps) {
|
||||
return (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label={`Span ${props.type} time shift`}
|
||||
labelWidth={26}
|
||||
grow
|
||||
tooltip={`Shifts the ${props.type} time of the span. Default 0 Time units can be used here, for example: 5s, 1m, 3h`}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="1h"
|
||||
width={40}
|
||||
onChange={(e) => props.onChange(e.currentTarget.value)}
|
||||
value={props.value}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
infoText: css`
|
||||
padding-bottom: ${theme.spacing(2)};
|
||||
|
@ -5,17 +5,16 @@ import {
|
||||
DataSourceJsonData,
|
||||
DataSourcePluginOptionsEditorProps,
|
||||
GrafanaTheme2,
|
||||
KeyValue,
|
||||
updateDatasourcePluginJsonDataOption,
|
||||
} from '@grafana/data';
|
||||
import { DataSourcePicker } from '@grafana/runtime';
|
||||
import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import KeyValueInput from '../TraceToLogs/KeyValueInput';
|
||||
import { TagMappingInput } from '../TraceToLogs/TagMappingInput';
|
||||
|
||||
export interface TraceToMetricsOptions {
|
||||
datasourceUid?: string;
|
||||
tags?: Array<KeyValue<string>>;
|
||||
tags?: Array<{ key: string; value: string }>;
|
||||
queries: TraceToMetricQuery[];
|
||||
spanStartTimeShift?: string;
|
||||
spanEndTimeShift?: string;
|
||||
@ -79,8 +78,7 @@ export function TraceToMetricsSettings({ options, onOptionsChange }: Props) {
|
||||
|
||||
<InlineFieldRow>
|
||||
<InlineField tooltip="Tags that will be used in the metrics query." label="Tags" labelWidth={26}>
|
||||
<KeyValueInput
|
||||
keyPlaceholder="Tag"
|
||||
<TagMappingInput
|
||||
values={options.jsonData.tracesToMetrics?.tags ?? []}
|
||||
onChange={(v) =>
|
||||
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
TraceTimelineViewer,
|
||||
TTraceTimeline,
|
||||
} from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { getTraceToLogsOptions, 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';
|
||||
@ -121,7 +121,7 @@ export function TraceView(props: Props) {
|
||||
);
|
||||
|
||||
const instanceSettings = getDatasourceSrv().getInstanceSettings(datasource?.name);
|
||||
const traceToLogsOptions = (instanceSettings?.jsonData as TraceToLogsData)?.tracesToLogs;
|
||||
const traceToLogsOptions = getTraceToLogsOptions(instanceSettings?.jsonData as TraceToLogsData);
|
||||
const traceToMetricsOptions = (instanceSettings?.jsonData as TraceToMetricsData)?.tracesToMetrics;
|
||||
const spanBarOptions: SpanBarOptionsData | undefined = instanceSettings?.jsonData;
|
||||
|
||||
@ -133,8 +133,9 @@ export function TraceView(props: Props) {
|
||||
traceToMetricsOptions,
|
||||
dataFrame: props.dataFrames[0],
|
||||
createFocusSpanLink,
|
||||
trace: traceProp,
|
||||
}),
|
||||
[props.splitOpenFn, traceToLogsOptions, traceToMetricsOptions, props.dataFrames, createFocusSpanLink]
|
||||
[props.splitOpenFn, traceToLogsOptions, traceToMetricsOptions, props.dataFrames, createFocusSpanLink, traceProp]
|
||||
);
|
||||
const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]);
|
||||
const timeZone = useSelector((state) => getTimeZone(state.user));
|
||||
|
@ -1,19 +1,26 @@
|
||||
import { DataSourceInstanceSettings, LinkModel, MutableDataFrame } from '@grafana/data';
|
||||
import { DataSourceSrv, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
|
||||
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
import { Trace, TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
import { TraceToLogsOptions } from '../../../core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { TraceToLogsOptionsV2 } from '../../../core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv';
|
||||
import { TemplateSrv } from '../../templating/template_srv';
|
||||
|
||||
import { createSpanLinkFactory } from './createSpanLink';
|
||||
|
||||
const dummyTraceData = { duration: 10, traceID: 'trace1', traceName: 'test trace' } as unknown as Trace;
|
||||
const dummyDataFrame = new MutableDataFrame({ fields: [{ name: 'traceId', values: ['trace1'] }] });
|
||||
|
||||
describe('createSpanLinkFactory', () => {
|
||||
it('returns no links if there is no data source uid', () => {
|
||||
const splitOpenFn = jest.fn();
|
||||
const createLink = createSpanLinkFactory({ splitOpenFn: splitOpenFn });
|
||||
const createLink = createSpanLinkFactory({
|
||||
splitOpenFn: splitOpenFn,
|
||||
trace: dummyTraceData,
|
||||
dataFrame: dummyDataFrame,
|
||||
});
|
||||
const links = createLink!(createTraceSpan());
|
||||
expect(links?.logLinks).toBeUndefined();
|
||||
expect(links?.metricLinks).toBeUndefined();
|
||||
@ -40,14 +47,14 @@ describe('createSpanLinkFactory', () => {
|
||||
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_uid","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}],"panelsState":{}}'
|
||||
'{"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":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('with tags that passed in and without tags that are not in the span', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: ['ip', 'newTag'],
|
||||
tags: [{ key: 'ip' }, { key: 'newTag' }],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const links = createLink!(
|
||||
@ -65,14 +72,14 @@ describe('createSpanLinkFactory', () => {
|
||||
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_uid","queries":[{"expr":"{ip=\\"192.168.0.1\\"}","refId":""}],"panelsState":{}}'
|
||||
'{"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":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('from tags and process tags as well', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: ['ip', 'host'],
|
||||
tags: [{ key: 'ip' }, { key: 'host' }],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const links = createLink!(
|
||||
@ -90,7 +97,7 @@ describe('createSpanLinkFactory', () => {
|
||||
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_uid","queries":[{"expr":"{ip=\\"192.168.0.1\\", host=\\"host\\"}","refId":""}],"panelsState":{}}'
|
||||
'{"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":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -116,7 +123,7 @@ describe('createSpanLinkFactory', () => {
|
||||
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_uid","queries":[{"expr":"{hostname=\\"hostname1\\"}","refId":""}],"panelsState":{}}'
|
||||
'{"range":{"from":"2020-10-14T01:01:00.000Z","to":"2020-10-14T01:01:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{hostname=\\"hostname1\\"}","refId":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -131,10 +138,18 @@ 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_uid","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"} |=\\"7946b05c2e2e4e5a\\" |=\\"6605c7b08e715d6c\\"","refId":""}],"panelsState":{}}'
|
||||
)}`
|
||||
expect(decodeURIComponent(linkDef!.href)).toBe(
|
||||
'/explore?left=' +
|
||||
JSON.stringify({
|
||||
range: { from: '2020-10-14T01:00:00.000Z', to: '2020-10-14T01:00:01.000Z' },
|
||||
datasource: 'loki1_uid',
|
||||
queries: [
|
||||
{
|
||||
expr: '{cluster="cluster1", hostname="hostname1"} |="7946b05c2e2e4e5a" |="6605c7b08e715d6c"',
|
||||
refId: '',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@ -152,6 +167,7 @@ describe('createSpanLinkFactory', () => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
trace: dummyTraceData,
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const links = createLink!(createTraceSpan());
|
||||
@ -163,8 +179,7 @@ describe('createSpanLinkFactory', () => {
|
||||
|
||||
it('handles renamed tags', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
mapTagNamesEnabled: true,
|
||||
mappedTags: [
|
||||
tags: [
|
||||
{ key: 'service.name', value: 'service' },
|
||||
{ key: 'k8s.pod.name', value: 'pod' },
|
||||
],
|
||||
@ -186,15 +201,14 @@ describe('createSpanLinkFactory', () => {
|
||||
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_uid","queries":[{"expr":"{service=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}'
|
||||
'{"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":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles incomplete renamed tags', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
mapTagNamesEnabled: true,
|
||||
mappedTags: [
|
||||
tags: [
|
||||
{ key: 'service.name', value: '' },
|
||||
{ key: 'k8s.pod.name', value: 'pod' },
|
||||
],
|
||||
@ -216,7 +230,7 @@ describe('createSpanLinkFactory', () => {
|
||||
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_uid","queries":[{"expr":"{service.name=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}'
|
||||
'{"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":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -239,6 +253,16 @@ describe('createSpanLinkFactory', () => {
|
||||
);
|
||||
expect(links?.logLinks).toBeUndefined();
|
||||
});
|
||||
|
||||
it('interpolates span intrinsics', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: [{ key: 'name', value: 'spanName' }],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const links = createLink!(createTraceSpan());
|
||||
expect(links?.logLinks).toBeDefined();
|
||||
expect(decodeURIComponent(links!.logLinks![0].href)).toContain('spanName=\\"operation\\"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return splunk link', () => {
|
||||
@ -301,14 +325,14 @@ describe('createSpanLinkFactory', () => {
|
||||
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":"splunkUID","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}],"panelsState":{}}'
|
||||
'{"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":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should format one tag correctly', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: ['ip'],
|
||||
tags: [{ key: 'ip' }],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const links = createLink!(
|
||||
@ -324,14 +348,14 @@ describe('createSpanLinkFactory', () => {
|
||||
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":"splunkUID","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}],"panelsState":{}}'
|
||||
'{"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":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should format multiple tags correctly', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: ['ip', 'hostname'],
|
||||
tags: [{ key: 'ip' }, { key: 'hostname' }],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const links = createLink!(
|
||||
@ -350,15 +374,14 @@ describe('createSpanLinkFactory', () => {
|
||||
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":"splunkUID","queries":[{"query":"hostname=\\"hostname1\\" ip=\\"192.168.0.1\\"","refId":""}],"panelsState":{}}'
|
||||
'{"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":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles renamed tags', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
mapTagNamesEnabled: true,
|
||||
mappedTags: [
|
||||
tags: [
|
||||
{ key: 'service.name', value: 'service' },
|
||||
{ key: 'k8s.pod.name', value: 'pod' },
|
||||
],
|
||||
@ -380,7 +403,7 @@ describe('createSpanLinkFactory', () => {
|
||||
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":"splunkUID","queries":[{"query":"service=\\"serviceName\\" pod=\\"podName\\"","refId":""}],"panelsState":{}}'
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"service=\\"serviceName\\" pod=\\"podName\\"","refId":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -406,6 +429,8 @@ describe('createSpanLinkFactory', () => {
|
||||
datasourceUid: 'prom1Uid',
|
||||
queries: [{ query: 'customQuery' }],
|
||||
},
|
||||
trace: dummyTraceData,
|
||||
dataFrame: dummyDataFrame,
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
|
||||
@ -415,7 +440,7 @@ describe('createSpanLinkFactory', () => {
|
||||
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":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}],"panelsState":{}}'
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -427,6 +452,8 @@ describe('createSpanLinkFactory', () => {
|
||||
traceToMetricsOptions: {
|
||||
datasourceUid: 'prom1',
|
||||
} as TraceToMetricsOptions,
|
||||
trace: dummyTraceData,
|
||||
dataFrame: dummyDataFrame,
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
|
||||
@ -446,6 +473,8 @@ describe('createSpanLinkFactory', () => {
|
||||
{ query: 'no_name_here' },
|
||||
],
|
||||
},
|
||||
trace: dummyTraceData,
|
||||
dataFrame: dummyDataFrame,
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
|
||||
@ -458,7 +487,7 @@ describe('createSpanLinkFactory', () => {
|
||||
expect(namedLink!.title).toBe('Named Query');
|
||||
expect(namedLink!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}],"panelsState":{}}'
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}'
|
||||
)}`
|
||||
);
|
||||
|
||||
@ -467,7 +496,7 @@ describe('createSpanLinkFactory', () => {
|
||||
expect(defaultLink!.title).toBe('defaultQuery');
|
||||
expect(defaultLink!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation=\\"operation\\"}[5m])) by (le))","refId":"A"}],"panelsState":{}}'
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation=\\"operation\\"}[5m])) by (le))","refId":"A"}]}'
|
||||
)}`
|
||||
);
|
||||
|
||||
@ -476,7 +505,7 @@ describe('createSpanLinkFactory', () => {
|
||||
expect(unnamedQuery!.title).toBeUndefined();
|
||||
expect(unnamedQuery!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"no_name_here","refId":"A"}],"panelsState":{}}'
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"no_name_here","refId":"A"}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -491,6 +520,8 @@ describe('createSpanLinkFactory', () => {
|
||||
spanStartTimeShift: '-1h',
|
||||
spanEndTimeShift: '1h',
|
||||
},
|
||||
trace: dummyTraceData,
|
||||
dataFrame: dummyDataFrame,
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
|
||||
@ -500,7 +531,7 @@ describe('createSpanLinkFactory', () => {
|
||||
expect(linkDef).toBeDefined();
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T00:00:00.000Z","to":"2020-10-14T02:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}],"panelsState":{}}'
|
||||
'{"range":{"from":"2020-10-14T00:00:00.000Z","to":"2020-10-14T02:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"customQuery","refId":"A"}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -518,6 +549,8 @@ describe('createSpanLinkFactory', () => {
|
||||
{ key: 'k8s.pod', value: 'pod' },
|
||||
],
|
||||
},
|
||||
trace: dummyTraceData,
|
||||
dataFrame: dummyDataFrame,
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
|
||||
@ -535,7 +568,7 @@ describe('createSpanLinkFactory', () => {
|
||||
expect(links).toBeDefined();
|
||||
expect(links!.metricLinks![0]!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1Uid","queries":[{"expr":"metric{job=\\"tns/app\\", pod=\\"sample-pod\\", job=\\"tns/app\\", pod=\\"sample-pod\\"}[5m]","refId":"A"}],"panelsState":{}}'
|
||||
'{"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"}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -620,10 +653,8 @@ describe('createSpanLinkFactory', () => {
|
||||
|
||||
const linkDef = links?.logLinks?.[0];
|
||||
expect(linkDef).toBeDefined();
|
||||
expect(linkDef!.href).toContain(
|
||||
encodeURIComponent(
|
||||
`datasource":"${searchUID}","queries":[{"query":"cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]`
|
||||
)
|
||||
expect(decodeURIComponent(linkDef!.href)).toContain(
|
||||
`datasource":"${searchUID}","queries":[{"query":"cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]`
|
||||
);
|
||||
});
|
||||
|
||||
@ -660,7 +691,7 @@ describe('createSpanLinkFactory', () => {
|
||||
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":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}],"panelsState":{}}`
|
||||
`{"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"}]}]}`
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -686,17 +717,15 @@ 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":"searchUID","queries":[{"query":"\\"7946b05c2e2e4e5a\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}],"panelsState":{}}`
|
||||
)}`
|
||||
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"}]}]}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should format one tag correctly', () => {
|
||||
const createLink = setupSpanLinkFactory(
|
||||
{
|
||||
tags: ['ip'],
|
||||
tags: [{ key: 'ip' }],
|
||||
},
|
||||
searchUID
|
||||
);
|
||||
@ -714,7 +743,7 @@ describe('createSpanLinkFactory', () => {
|
||||
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":"${searchUID}","queries":[{"query":"ip:\\"192.168.0.1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}],"panelsState":{}}`
|
||||
`{"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"}]}]}`
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -722,7 +751,7 @@ describe('createSpanLinkFactory', () => {
|
||||
it('should format multiple tags correctly', () => {
|
||||
const createLink = setupSpanLinkFactory(
|
||||
{
|
||||
tags: ['ip', 'hostname'],
|
||||
tags: [{ key: 'ip' }, { key: 'hostname' }],
|
||||
},
|
||||
searchUID
|
||||
);
|
||||
@ -743,7 +772,7 @@ describe('createSpanLinkFactory', () => {
|
||||
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":"${searchUID}","queries":[{"query":"hostname:\\"hostname1\\" AND ip:\\"192.168.0.1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}],"panelsState":{}}`
|
||||
`{"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"}]}]}`
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -751,8 +780,7 @@ describe('createSpanLinkFactory', () => {
|
||||
it('handles renamed tags', () => {
|
||||
const createLink = setupSpanLinkFactory(
|
||||
{
|
||||
mapTagNamesEnabled: true,
|
||||
mappedTags: [
|
||||
tags: [
|
||||
{ key: 'service.name', value: 'service' },
|
||||
{ key: 'k8s.pod.name', value: 'pod' },
|
||||
],
|
||||
@ -776,18 +804,73 @@ describe('createSpanLinkFactory', () => {
|
||||
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":"${searchUID}","queries":[{"query":"service:\\"serviceName\\" AND pod:\\"podName\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}],"panelsState":{}}`
|
||||
`{"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"}]}]}`
|
||||
)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom query', () => {
|
||||
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('interpolates custom query correctly', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: [
|
||||
{ key: 'service.name', value: 'service' },
|
||||
{ key: 'k8s.pod.name', value: 'pod' },
|
||||
],
|
||||
customQuery: true,
|
||||
query: '{${__tags}} |="${__span.tags["service.name"]}" |="${__trace.traceId}"',
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const links = createLink!(
|
||||
createTraceSpan({
|
||||
process: {
|
||||
serviceName: 'service',
|
||||
tags: [
|
||||
{ key: 'service.name', value: 'serviceName' },
|
||||
{ key: 'k8s.pod.name', value: 'podName' },
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const linkDef = links?.logLinks?.[0];
|
||||
expect(linkDef).toBeDefined();
|
||||
expect(decodeURIComponent(linkDef!.href)).toContain(
|
||||
'"queries":' +
|
||||
JSON.stringify([{ expr: '{service="serviceName", pod="podName"} |="serviceName" |="trace1"', refId: '' }])
|
||||
);
|
||||
});
|
||||
|
||||
it('does not return a link if variables are not matched', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: [{ key: 'service.name', value: 'service' }],
|
||||
customQuery: true,
|
||||
query: '{${__tags}} |="${__span.tags["service.name"]}" |="${__trace.id}"',
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const links = createLink!(createTraceSpan());
|
||||
expect(links?.logLinks).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupSpanLinkFactory(options: Partial<TraceToLogsOptions> = {}, datasourceUid = 'lokiUid') {
|
||||
function setupSpanLinkFactory(options: Partial<TraceToLogsOptionsV2> = {}, datasourceUid = 'lokiUid') {
|
||||
const splitOpenFn = jest.fn();
|
||||
return createSpanLinkFactory({
|
||||
splitOpenFn,
|
||||
traceToLogsOptions: {
|
||||
customQuery: false,
|
||||
datasourceUid,
|
||||
...options,
|
||||
},
|
||||
@ -796,6 +879,8 @@ function setupSpanLinkFactory(options: Partial<TraceToLogsOptions> = {}, datasou
|
||||
href: `${traceId}-${spanId}`,
|
||||
} as unknown as LinkModel;
|
||||
},
|
||||
trace: dummyTraceData,
|
||||
dataFrame: dummyDataFrame,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { SpanLinks } from '@jaegertracing/jaeger-ui-components/src/types/links';
|
||||
import { property } from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
@ -8,23 +9,24 @@ import {
|
||||
DataSourceJsonData,
|
||||
dateTime,
|
||||
Field,
|
||||
KeyValue,
|
||||
LinkModel,
|
||||
mapInternalLinkToExplore,
|
||||
rangeUtil,
|
||||
ScopedVars,
|
||||
SplitOpen,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
import { Icon } from '@grafana/ui';
|
||||
import { SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { SpanLinkFunc, Trace, TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToLogsOptionsV2 } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
|
||||
import { TraceToMetricQuery, 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 { variableRegex } from '../../variables/utils';
|
||||
import { getFieldLinksForExplore } from '../utils/links';
|
||||
|
||||
/**
|
||||
@ -38,19 +40,44 @@ export function createSpanLinkFactory({
|
||||
traceToMetricsOptions,
|
||||
dataFrame,
|
||||
createFocusSpanLink,
|
||||
trace,
|
||||
}: {
|
||||
splitOpenFn: SplitOpen;
|
||||
traceToLogsOptions?: TraceToLogsOptions;
|
||||
traceToLogsOptions?: TraceToLogsOptionsV2;
|
||||
traceToMetricsOptions?: TraceToMetricsOptions;
|
||||
dataFrame?: DataFrame;
|
||||
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
|
||||
trace: Trace;
|
||||
}): SpanLinkFunc | undefined {
|
||||
if (!dataFrame || dataFrame.fields.length === 1 || !dataFrame.fields.some((f) => Boolean(f.config.links?.length))) {
|
||||
if (!dataFrame) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let scopedVars = scopedVarsFromTrace(trace);
|
||||
const hasLinks = dataFrame.fields.some((f) => Boolean(f.config.links?.length));
|
||||
const legacyFormat = dataFrame.fields.length === 1;
|
||||
|
||||
if (legacyFormat || !hasLinks) {
|
||||
// if the dataframe contains just a single blob of data (legacy format) or does not have any links configured,
|
||||
// let's try to use the old legacy path.
|
||||
return legacyCreateSpanLinkFactory(splitOpenFn, traceToLogsOptions, traceToMetricsOptions, createFocusSpanLink);
|
||||
} else {
|
||||
// TODO: This was mainly a backward compatibility thing but at this point can probably be removed.
|
||||
return legacyCreateSpanLinkFactory(
|
||||
splitOpenFn,
|
||||
// We need this to make the types happy but for this branch of code it does not matter which field we supply.
|
||||
dataFrame.fields[0],
|
||||
traceToLogsOptions,
|
||||
traceToMetricsOptions,
|
||||
createFocusSpanLink,
|
||||
scopedVars
|
||||
);
|
||||
}
|
||||
|
||||
if (hasLinks) {
|
||||
return function SpanLink(span: TraceSpan): SpanLinks | undefined {
|
||||
scopedVars = {
|
||||
...scopedVars,
|
||||
...scopedVarsFromSpan(span),
|
||||
};
|
||||
// We should be here only if there are some links in the dataframe
|
||||
const field = dataFrame.fields.find((f) => Boolean(f.config.links?.length))!;
|
||||
try {
|
||||
@ -60,6 +87,7 @@ export function createSpanLinkFactory({
|
||||
splitOpenFn,
|
||||
range: getTimeRangeFromSpan(span),
|
||||
dataFrame,
|
||||
vars: scopedVars,
|
||||
});
|
||||
|
||||
return {
|
||||
@ -68,6 +96,7 @@ export function createSpanLinkFactory({
|
||||
href: links[0].href,
|
||||
onClick: links[0].onClick,
|
||||
content: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
|
||||
field: links[0].origin,
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -78,13 +107,22 @@ export function createSpanLinkFactory({
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default keys to use when there are no configured tags.
|
||||
*/
|
||||
const defaultKeys = ['cluster', 'hostname', 'namespace', 'pod'].map((k) => ({ key: k }));
|
||||
|
||||
function legacyCreateSpanLinkFactory(
|
||||
splitOpenFn: SplitOpen,
|
||||
traceToLogsOptions?: TraceToLogsOptions,
|
||||
field: Field,
|
||||
traceToLogsOptions?: TraceToLogsOptionsV2,
|
||||
traceToMetricsOptions?: TraceToMetricsOptions,
|
||||
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>
|
||||
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>,
|
||||
scopedVars?: ScopedVars
|
||||
) {
|
||||
let logsDataSourceSettings: DataSourceInstanceSettings<DataSourceJsonData> | undefined;
|
||||
if (traceToLogsOptions?.datasourceUid) {
|
||||
@ -98,59 +136,89 @@ function legacyCreateSpanLinkFactory(
|
||||
}
|
||||
|
||||
return function SpanLink(span: TraceSpan): SpanLinks {
|
||||
scopedVars = {
|
||||
...scopedVars,
|
||||
...scopedVarsFromSpan(span),
|
||||
};
|
||||
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;
|
||||
let query: DataQuery | undefined;
|
||||
let tags = '';
|
||||
|
||||
// Get logs link
|
||||
// TODO: This should eventually move into specific data sources and added to the data frame as we no longer use the
|
||||
// deprecated blob format and we can map the link easily in data frame.
|
||||
if (logsDataSourceSettings && traceToLogsOptions) {
|
||||
const customQuery = traceToLogsOptions.customQuery ? traceToLogsOptions.query : undefined;
|
||||
const tagsToUse = traceToLogsOptions.tags || defaultKeys;
|
||||
switch (logsDataSourceSettings?.type) {
|
||||
case 'loki':
|
||||
dataLink = getLinkForLoki(span, traceToLogsOptions, logsDataSourceSettings);
|
||||
tags = getFormattedTags(span, tagsToUse);
|
||||
query = getQueryForLoki(span, traceToLogsOptions, tags, customQuery);
|
||||
break;
|
||||
case 'grafana-splunk-datasource':
|
||||
dataLink = getLinkForSplunk(span, traceToLogsOptions, logsDataSourceSettings);
|
||||
tags = getFormattedTags(span, tagsToUse, { joinBy: ' ' });
|
||||
query = getQueryForSplunk(span, traceToLogsOptions, tags, customQuery);
|
||||
break;
|
||||
case 'elasticsearch':
|
||||
dataLink = getLinkForElasticsearchOrOpensearch(span, traceToLogsOptions, logsDataSourceSettings);
|
||||
break;
|
||||
case 'grafana-opensearch-datasource':
|
||||
dataLink = getLinkForElasticsearchOrOpensearch(span, traceToLogsOptions, logsDataSourceSettings);
|
||||
tags = getFormattedTags(span, tagsToUse, { labelValueSign: ':', joinBy: ' AND ' });
|
||||
query = getQueryForElasticsearchOrOpensearch(span, traceToLogsOptions, tags, customQuery);
|
||||
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: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
|
||||
// query can be false in case the simple UI tag mapping is used but none of them are present in the span.
|
||||
// For custom query, this is always defined and we check if the interpolation matched all variables later on.
|
||||
if (query) {
|
||||
const dataLink: DataLink = {
|
||||
title: logsDataSourceSettings.name,
|
||||
url: '',
|
||||
internal: {
|
||||
datasourceUid: logsDataSourceSettings.uid,
|
||||
datasourceName: logsDataSourceSettings.name,
|
||||
query,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
scopedVars = {
|
||||
...scopedVars,
|
||||
__tags: {
|
||||
text: 'Tags',
|
||||
value: tags,
|
||||
},
|
||||
};
|
||||
|
||||
// Check if all variables are defined and don't show if they aren't. This is usually handled by the
|
||||
// getQueryFor* functions but this is for case of custom query supplied by the user.
|
||||
if (dataLinkHasAllVariablesDefined(dataLink.internal!.query, scopedVars)) {
|
||||
const link = mapInternalLinkToExplore({
|
||||
link: dataLink,
|
||||
internalLink: dataLink.internal!,
|
||||
scopedVars: 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: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
|
||||
field,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +226,7 @@ function legacyCreateSpanLinkFactory(
|
||||
if (metricsDataSourceSettings && traceToMetricsOptions?.queries) {
|
||||
links.metricLinks = [];
|
||||
for (const query of traceToMetricsOptions.queries) {
|
||||
const expr = buildMetricsQuery(query, traceToMetricsOptions?.tags, span);
|
||||
const expr = buildMetricsQuery(query, traceToMetricsOptions?.tags || [], span);
|
||||
const dataLink: DataLink<PromQuery> = {
|
||||
title: metricsDataSourceSettings.name,
|
||||
url: '',
|
||||
@ -194,6 +262,7 @@ function legacyCreateSpanLinkFactory(
|
||||
href: link.href,
|
||||
onClick: link.onClick,
|
||||
content: <Icon name="chart-line" title="Explore metrics for this span" />,
|
||||
field,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -213,6 +282,7 @@ function legacyCreateSpanLinkFactory(
|
||||
title: reference.span ? reference.span.operationName : 'View linked span',
|
||||
content: <Icon name="link" title="View linked span" />,
|
||||
onClick: link.onClick,
|
||||
field: link.origin,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -226,6 +296,7 @@ function legacyCreateSpanLinkFactory(
|
||||
title: reference.span ? reference.span.operationName : 'View linked span',
|
||||
content: <Icon name="link" title="View linked span" />,
|
||||
onClick: link.onClick,
|
||||
field: link.origin,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -234,56 +305,34 @@ function legacyCreateSpanLinkFactory(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Default keys to use when there are no configured tags.
|
||||
*/
|
||||
const defaultKeys = ['cluster', 'hostname', 'namespace', 'pod'];
|
||||
function getLinkForLoki(span: TraceSpan, options: TraceToLogsOptions, dataSourceSettings: DataSourceInstanceSettings) {
|
||||
const { tags: keys, filterByTraceID, filterBySpanID, mapTagNamesEnabled, mappedTags } = options;
|
||||
function getQueryForLoki(
|
||||
span: TraceSpan,
|
||||
options: TraceToLogsOptionsV2,
|
||||
tags: string,
|
||||
customQuery?: string
|
||||
): LokiQuery | undefined {
|
||||
const { filterByTraceID, filterBySpanID } = options;
|
||||
|
||||
// In order, try to use mapped tags -> tags -> default tags
|
||||
const keysToCheck = mapTagNamesEnabled && mappedTags?.length ? mappedTags : keys?.length ? keys : defaultKeys;
|
||||
// Build tag portion of query
|
||||
const tags = [...span.process.tags, ...span.tags].reduce((acc, tag) => {
|
||||
if (mapTagNamesEnabled) {
|
||||
const keyValue = (keysToCheck as KeyValue[]).find((keyValue: KeyValue) => keyValue.key === tag.key);
|
||||
if (keyValue) {
|
||||
acc.push(`${keyValue.value ? keyValue.value : keyValue.key}="${tag.value}"`);
|
||||
}
|
||||
} else {
|
||||
if ((keysToCheck as string[]).includes(tag.key)) {
|
||||
acc.push(`${tag.key}="${tag.value}"`);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
if (customQuery) {
|
||||
return { expr: customQuery, refId: '' };
|
||||
}
|
||||
|
||||
// If no tags found, return undefined to prevent an invalid Loki query
|
||||
if (!tags.length) {
|
||||
if (!tags) {
|
||||
return undefined;
|
||||
}
|
||||
let expr = `{${tags.join(', ')}}`;
|
||||
|
||||
let expr = '{${__tags}}';
|
||||
if (filterByTraceID && span.traceID) {
|
||||
expr += ` |="${span.traceID}"`;
|
||||
expr += ' |="${__span.traceId}"';
|
||||
}
|
||||
if (filterBySpanID && span.spanID) {
|
||||
expr += ` |="${span.spanID}"`;
|
||||
expr += ' |="${__span.spanId}"';
|
||||
}
|
||||
|
||||
const dataLink: DataLink<LokiQuery> = {
|
||||
title: dataSourceSettings.name,
|
||||
url: '',
|
||||
internal: {
|
||||
datasourceUid: dataSourceSettings.uid,
|
||||
datasourceName: dataSourceSettings.name,
|
||||
query: {
|
||||
expr: expr,
|
||||
refId: '',
|
||||
},
|
||||
},
|
||||
return {
|
||||
expr: expr,
|
||||
refId: '',
|
||||
};
|
||||
|
||||
return dataLink;
|
||||
}
|
||||
|
||||
// we do not have access to the dataquery type for opensearch,
|
||||
@ -296,114 +345,93 @@ interface ElasticsearchOrOpensearchQuery extends DataQuery {
|
||||
}>;
|
||||
}
|
||||
|
||||
function getLinkForElasticsearchOrOpensearch(
|
||||
function getQueryForElasticsearchOrOpensearch(
|
||||
span: TraceSpan,
|
||||
options: TraceToLogsOptions,
|
||||
dataSourceSettings: DataSourceInstanceSettings
|
||||
) {
|
||||
const { tags: keys, filterByTraceID, filterBySpanID, mapTagNamesEnabled, mappedTags } = options;
|
||||
const tags = [...span.process.tags, ...span.tags].reduce((acc: string[], tag) => {
|
||||
if (mapTagNamesEnabled && mappedTags?.length) {
|
||||
const keysToCheck = mappedTags;
|
||||
const keyValue = keysToCheck.find((keyValue) => keyValue.key === tag.key);
|
||||
if (keyValue) {
|
||||
acc.push(`${keyValue.value ? keyValue.value : keyValue.key}:"${tag.value}"`);
|
||||
}
|
||||
} else {
|
||||
const keysToCheck = keys?.length ? keys : defaultKeys;
|
||||
if (keysToCheck.includes(tag.key)) {
|
||||
acc.push(`${tag.key}:"${tag.value}"`);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
options: TraceToLogsOptionsV2,
|
||||
tags: string,
|
||||
customQuery?: string
|
||||
): ElasticsearchOrOpensearchQuery {
|
||||
const { filterByTraceID, filterBySpanID } = options;
|
||||
if (customQuery) {
|
||||
return {
|
||||
query: customQuery,
|
||||
refId: '',
|
||||
metrics: [{ id: '1', type: 'logs' }],
|
||||
};
|
||||
}
|
||||
|
||||
let queryArr = [];
|
||||
if (filterBySpanID && span.spanID) {
|
||||
queryArr.push(`"${span.spanID}"`);
|
||||
queryArr.push('"${__span.spanId}"');
|
||||
}
|
||||
|
||||
if (filterByTraceID && span.traceID) {
|
||||
queryArr.push(`"${span.traceID}"`);
|
||||
queryArr.push('"${__span.traceId}"');
|
||||
}
|
||||
|
||||
if (tags.length > 0) {
|
||||
for (const tag of tags) {
|
||||
queryArr.push(tag);
|
||||
}
|
||||
if (tags) {
|
||||
queryArr.push('${__tags}');
|
||||
}
|
||||
|
||||
const dataLink: DataLink<ElasticsearchOrOpensearchQuery> = {
|
||||
title: dataSourceSettings.name,
|
||||
url: '',
|
||||
internal: {
|
||||
datasourceUid: dataSourceSettings.uid,
|
||||
datasourceName: dataSourceSettings.name,
|
||||
query: {
|
||||
query: queryArr.join(' AND '),
|
||||
refId: '',
|
||||
metrics: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'logs',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
return {
|
||||
query: queryArr.join(' AND '),
|
||||
refId: '',
|
||||
metrics: [{ id: '1', type: 'logs' }],
|
||||
};
|
||||
|
||||
return dataLink;
|
||||
}
|
||||
|
||||
function getLinkForSplunk(
|
||||
span: TraceSpan,
|
||||
options: TraceToLogsOptions,
|
||||
dataSourceSettings: DataSourceInstanceSettings
|
||||
) {
|
||||
const { tags: keys, filterByTraceID, filterBySpanID, mapTagNamesEnabled, mappedTags } = options;
|
||||
function getQueryForSplunk(span: TraceSpan, options: TraceToLogsOptionsV2, tags: string, customQuery?: string) {
|
||||
const { filterByTraceID, filterBySpanID } = options;
|
||||
|
||||
// In order, try to use mapped tags -> tags -> default tags
|
||||
const keysToCheck = mapTagNamesEnabled && mappedTags?.length ? mappedTags : keys?.length ? keys : defaultKeys;
|
||||
// Build tag portion of query
|
||||
const tags = [...span.process.tags, ...span.tags].reduce((acc, tag) => {
|
||||
if (mapTagNamesEnabled) {
|
||||
const keyValue = (keysToCheck as KeyValue[]).find((keyValue: KeyValue) => keyValue.key === tag.key);
|
||||
if (keyValue) {
|
||||
acc.push(`${keyValue.value ? keyValue.value : keyValue.key}="${tag.value}"`);
|
||||
}
|
||||
} else {
|
||||
if ((keysToCheck as string[]).includes(tag.key)) {
|
||||
acc.push(`${tag.key}="${tag.value}"`);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
if (customQuery) {
|
||||
return { query: customQuery, refId: '' };
|
||||
}
|
||||
|
||||
let query = '';
|
||||
if (tags.length > 0) {
|
||||
query += `${tags.join(' ')}`;
|
||||
if (tags) {
|
||||
query += '${__tags}';
|
||||
}
|
||||
if (filterByTraceID && span.traceID) {
|
||||
query += ` "${span.traceID}"`;
|
||||
query += ' "${__span.traceId}"';
|
||||
}
|
||||
if (filterBySpanID && span.spanID) {
|
||||
query += ` "${span.spanID}"`;
|
||||
query += ' "${__span.spanId}"';
|
||||
}
|
||||
|
||||
const dataLink: DataLink<DataQuery> = {
|
||||
title: dataSourceSettings.name,
|
||||
url: '',
|
||||
internal: {
|
||||
datasourceUid: dataSourceSettings.uid,
|
||||
datasourceName: dataSourceSettings.name,
|
||||
query: {
|
||||
query: query,
|
||||
refId: '',
|
||||
},
|
||||
},
|
||||
} as DataLink<DataQuery>;
|
||||
return {
|
||||
query: query,
|
||||
refId: '',
|
||||
};
|
||||
}
|
||||
|
||||
return dataLink;
|
||||
/**
|
||||
* Creates a string representing all the tags already formatted for use in the query. The tags are filtered so that
|
||||
* only intersection of tags that exist in a span and tags that you want are serialized into the string.
|
||||
*/
|
||||
function getFormattedTags(
|
||||
span: TraceSpan,
|
||||
tags: Array<{ key: string; value?: string }>,
|
||||
{ labelValueSign = '=', joinBy = ', ' }: { labelValueSign?: string; joinBy?: string } = {}
|
||||
) {
|
||||
// In order, try to use mapped tags -> tags -> default tags
|
||||
// Build tag portion of query
|
||||
return [
|
||||
...span.process.tags,
|
||||
...span.tags,
|
||||
{ key: 'spanId', value: span.spanID },
|
||||
{ key: 'traceId', value: span.traceID },
|
||||
{ key: 'name', value: span.operationName },
|
||||
{ key: 'duration', value: span.duration },
|
||||
]
|
||||
.map((tag) => {
|
||||
const keyValue = tags.find((keyValue) => keyValue.key === tag.key);
|
||||
if (keyValue) {
|
||||
return `${keyValue.value ? keyValue.value : keyValue.key}${labelValueSign}"${tag.value}"`;
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter((v) => Boolean(v))
|
||||
.join(joinBy);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -442,7 +470,11 @@ function getTimeRangeFromSpan(
|
||||
}
|
||||
|
||||
// Interpolates span attributes into trace to metric query, or returns default query
|
||||
function buildMetricsQuery(query: TraceToMetricQuery, tags: Array<KeyValue<string>> = [], span: TraceSpan): string {
|
||||
function buildMetricsQuery(
|
||||
query: TraceToMetricQuery,
|
||||
tags: Array<{ key: string; value?: string }> = [],
|
||||
span: TraceSpan
|
||||
): string {
|
||||
if (!query.query) {
|
||||
return `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`;
|
||||
}
|
||||
@ -464,3 +496,112 @@ function buildMetricsQuery(query: TraceToMetricQuery, tags: Array<KeyValue<strin
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variables from trace that can be used in the query
|
||||
* @param trace
|
||||
*/
|
||||
function scopedVarsFromTrace(trace: Trace): ScopedVars {
|
||||
return {
|
||||
__trace: {
|
||||
text: 'Trace',
|
||||
value: {
|
||||
duration: trace.duration,
|
||||
name: trace.traceName,
|
||||
traceId: trace.traceID,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Variables from span that can be used in the query
|
||||
* @param span
|
||||
*/
|
||||
function scopedVarsFromSpan(span: TraceSpan): ScopedVars {
|
||||
const tags: ScopedVars = {};
|
||||
|
||||
// We put all these tags together similar way we do for the __tags variable. This means there can be some overriding
|
||||
// of values if there is the same tag in both process tags and span tags.
|
||||
for (const tag of span.process.tags) {
|
||||
tags[tag.key] = tag.value;
|
||||
}
|
||||
for (const tag of span.tags) {
|
||||
tags[tag.key] = tag.value;
|
||||
}
|
||||
|
||||
return {
|
||||
__span: {
|
||||
text: 'Span',
|
||||
value: {
|
||||
spanId: span.spanID,
|
||||
traceId: span.traceID,
|
||||
duration: span.duration,
|
||||
name: span.operationName,
|
||||
tags: tags,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type VarValue = string | number | boolean | undefined;
|
||||
|
||||
/**
|
||||
* This function takes some code from template service replace() function to figure out if all variables are
|
||||
* interpolated. This is so we don't show links that do not work. This cuts a lots of corners though and that is why
|
||||
* it's a local function. We sort of don't care about the dashboard template variables for example. Also we only link
|
||||
* to loki/splunk/elastic, so it should be less probable that user needs part of a query that looks like a variable but
|
||||
* is actually part of the query language.
|
||||
* @param query
|
||||
* @param scopedVars
|
||||
*/
|
||||
function dataLinkHasAllVariablesDefined<T extends DataQuery>(query: T, scopedVars: ScopedVars): boolean {
|
||||
const vars = getVariablesMapInTemplate(getStringsFromObject(query), scopedVars);
|
||||
return Object.values(vars).every((val) => val !== undefined);
|
||||
}
|
||||
|
||||
function getStringsFromObject<T extends Object>(obj: T): string {
|
||||
let acc = '';
|
||||
for (const k of Object.keys(obj)) {
|
||||
// Honestly not sure how to type this to make TS happy.
|
||||
// @ts-ignore
|
||||
if (typeof obj[k] === 'string') {
|
||||
// @ts-ignore
|
||||
acc += ' ' + obj[k];
|
||||
// @ts-ignore
|
||||
} else if (typeof obj[k] === 'object' && obj[k] !== null) {
|
||||
// @ts-ignore
|
||||
acc += ' ' + getStringsFromObject(obj[k]);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
function getVariablesMapInTemplate(target: string, scopedVars: ScopedVars): Record<string, VarValue> {
|
||||
const regex = new RegExp(variableRegex);
|
||||
const values: Record<string, VarValue> = {};
|
||||
|
||||
target.replace(regex, (match, var1, var2, fmt2, var3, fieldPath) => {
|
||||
const variableName = var1 || var2 || var3;
|
||||
values[variableName] = getVariableValue(variableName, fieldPath, scopedVars);
|
||||
|
||||
// Don't care about the result anyway
|
||||
return '';
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function getVariableValue(variableName: string, fieldPath: string | undefined, scopedVars: ScopedVars): VarValue {
|
||||
const scopedVar = scopedVars[variableName];
|
||||
if (!scopedVar) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (fieldPath) {
|
||||
// @ts-ignore ScopedVars are typed in way that I don't think this is possible to type correctly.
|
||||
return property(fieldPath)(scopedVar.value);
|
||||
}
|
||||
|
||||
return scopedVar.value;
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ describe('getFieldLinksForExplore', () => {
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links[0].href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}],"panelsState":{}}'
|
||||
'{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -166,7 +166,7 @@ describe('getFieldLinksForExplore', () => {
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links[0].href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}],"panelsState":{}}'
|
||||
'{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -194,7 +194,7 @@ describe('getFieldLinksForExplore', () => {
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links[0].href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}],"panelsState":{}}'
|
||||
'{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo"}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
@ -236,7 +236,7 @@ describe('getFieldLinksForExplore', () => {
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links[0].href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo-foo2"}],"panelsState":{}}'
|
||||
'{"range":{"from":"now-1h","to":"now"},"datasource":"uid_1","queries":[{"query":"query_1-foo-foo2"}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
@ -24,6 +24,12 @@ const dataLinkHasRequiredPermissions = (link: DataLink) => {
|
||||
return !link.internal || contextSrv.hasAccessToExplore();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if every variable in the link has a value. If not this returns false. If there are no variables in the link
|
||||
* this will return true.
|
||||
* @param link
|
||||
* @param scopedVars
|
||||
*/
|
||||
const dataLinkHasAllVariablesDefined = (link: DataLink, scopedVars: ScopedVars) => {
|
||||
let hasAllRequiredVarDefined = true;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user