Elasticsearch: Add trace to logs functionality (#58063)

* Elasticsearch: Implement trace to logs

* Fix tests
This commit is contained in:
Ivana Huckova 2022-11-03 09:52:40 +01:00 committed by GitHub
parent eb84358aa7
commit a83dee6031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 241 additions and 33 deletions

View File

@ -3968,17 +3968,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"]
],
"public/app/features/explore/TraceView/createSpanLink.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
],
"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"],
@ -3989,8 +3978,7 @@ exports[`better eslint`] = {
[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.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"]
[0, 0, 0, "Do not use any type assertions.", "9"]
],
"public/app/features/explore/Wrapper.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -34,6 +34,7 @@ interface Props extends DataSourcePluginOptionsEditorProps<TraceToLogsData> {}
export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
const styles = useStyles2(getStyles);
const supportedDataSourceTypes = ['loki', 'grafana-splunk-datasource', 'elasticsearch'];
return (
<div className={css({ width: '100%' })}>
@ -47,10 +48,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
<InlineField tooltip="The data source the trace is going to navigate to" label="Data source" labelWidth={26}>
<DataSourcePicker
inputId="trace-to-logs-data-source-picker"
filter={(ds) => {
// Trace to logs only supports loki and splunk at the moment
return ds.type === 'loki' || ds.type === 'grafana-splunk-datasource';
}}
filter={(ds) => supportedDataSourceTypes.includes(ds.type)}
current={options.jsonData.tracesToLogs?.datasourceUid}
noDefault={true}
width={40}

View File

@ -1,5 +1,5 @@
import { DataSourceInstanceSettings, MutableDataFrame } from '@grafana/data';
import { setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
import { DataSourceInstanceSettings, LinkModel, MutableDataFrame } from '@grafana/data';
import { DataSourceSrv, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
@ -23,10 +23,10 @@ describe('createSpanLinkFactory', () => {
describe('should return loki link', () => {
beforeAll(() => {
setDataSourceSrv({
getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined {
return { uid: 'loki1_uid', name: 'loki1', type: 'loki' } as any;
getInstanceSettings() {
return { uid: 'loki1_uid', name: 'loki1', type: 'loki' } as unknown as DataSourceInstanceSettings;
},
} as any);
} as unknown as DataSourceSrv);
setLinkSrv(new LinkSrv());
setTemplateSrv(new TemplateSrv());
@ -246,10 +246,14 @@ describe('createSpanLinkFactory', () => {
beforeAll(() => {
setDataSourceSrv({
getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined {
return { uid: splunkUID, name: 'Splunk 8', type: 'grafana-splunk-datasource' } as any;
getInstanceSettings() {
return {
uid: splunkUID,
name: 'Splunk 8',
type: 'grafana-splunk-datasource',
} as unknown as DataSourceInstanceSettings;
},
} as any);
} as unknown as DataSourceSrv);
setLinkSrv(new LinkSrv());
setTemplateSrv(new TemplateSrv());
@ -385,10 +389,10 @@ describe('createSpanLinkFactory', () => {
describe('should return metric link', () => {
beforeAll(() => {
setDataSourceSrv({
getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined {
return { uid: 'prom1Uid', name: 'prom1', type: 'prometheus' } as any;
getInstanceSettings() {
return { uid: 'prom1Uid', name: 'prom1', type: 'prometheus' } as unknown as DataSourceInstanceSettings;
},
} as any);
} as unknown as DatasourceSrv);
setLinkSrv(new LinkSrv());
setTemplateSrv(new TemplateSrv());
@ -565,7 +569,7 @@ describe('createSpanLinkFactory', () => {
refType: 'FOLLOWS_FROM',
spanID: 'span1',
traceID: 'traceID',
span: { operationName: 'SpanName' } as any,
span: { operationName: 'SpanName' } as TraceSpan,
},
],
subsidiarilyReferencedBy: [{ refType: 'FOLLOWS_FROM', spanID: 'span3', traceID: 'traceID2' }],
@ -589,6 +593,166 @@ describe('createSpanLinkFactory', () => {
);
});
});
describe('elasticsearch link', () => {
const elasticsearchUID = 'elasticsearchUID';
beforeAll(() => {
setDataSourceSrv({
getInstanceSettings() {
return {
uid: elasticsearchUID,
name: 'Elasticsearch',
type: 'elasticsearch',
} as unknown as DataSourceInstanceSettings;
},
} as unknown as DataSourceSrv);
setLinkSrv(new LinkSrv());
setTemplateSrv(new TemplateSrv());
});
it('creates link with correct simple query', () => {
const createLink = setupSpanLinkFactory({
datasourceUid: elasticsearchUID,
});
const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
expect(linkDef!.href).toContain(
encodeURIComponent(
`datasource":"${elasticsearchUID}","queries":[{"query":"cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]`
)
);
});
it('automatically timeshifts the time range by one second in a query', () => {
const createLink = setupSpanLinkFactory({
datasourceUid: elasticsearchUID,
});
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"}')}`
);
expect(linkDef!.href).not.toContain(
`${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:00.000Z"}')}`
);
});
it('formats query correctly if filterByTraceID and or filterBySpanID is true', () => {
const createLink = setupSpanLinkFactory(
{
datasourceUid: elasticsearchUID,
filterByTraceID: true,
filterBySpanID: true,
},
elasticsearchUID
);
expect(createLink).toBeDefined();
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":"${elasticsearchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}],"panelsState":{}}`
)}`
);
});
it('should format one tag correctly', () => {
const createLink = setupSpanLinkFactory(
{
tags: ['ip'],
},
elasticsearchUID
);
expect(createLink).toBeDefined();
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
tags: [{ key: 'ip', value: '192.168.0.1' }],
},
})
);
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":"${elasticsearchUID}","queries":[{"query":"ip:\\"192.168.0.1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}],"panelsState":{}}`
)}`
);
});
it('should format multiple tags correctly', () => {
const createLink = setupSpanLinkFactory(
{
tags: ['ip', 'hostname'],
},
elasticsearchUID
);
expect(createLink).toBeDefined();
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
tags: [
{ key: 'hostname', value: 'hostname1' },
{ key: 'ip', value: '192.168.0.1' },
],
},
})
);
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":"${elasticsearchUID}","queries":[{"query":"hostname:\\"hostname1\\" AND ip:\\"192.168.0.1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}],"panelsState":{}}`
)}`
);
});
it('handles renamed tags', () => {
const createLink = setupSpanLinkFactory(
{
mapTagNamesEnabled: true,
mappedTags: [
{ key: 'service.name', value: 'service' },
{ key: 'k8s.pod.name', value: 'pod' },
],
},
elasticsearchUID
);
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(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
`{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${elasticsearchUID}","queries":[{"query":"service:\\"serviceName\\" AND pod:\\"podName\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}],"panelsState":{}}`
)}`
);
});
});
});
function setupSpanLinkFactory(options: Partial<TraceToLogsOptions> = {}, datasourceUid = 'lokiUid') {
@ -602,12 +766,12 @@ function setupSpanLinkFactory(options: Partial<TraceToLogsOptions> = {}, datasou
createFocusSpanLink: (traceId, spanId) => {
return {
href: `${traceId}-${spanId}`,
} as any;
} as unknown as LinkModel;
},
});
}
function createTraceSpan(overrides: Partial<TraceSpan> = {}): TraceSpan {
function createTraceSpan(overrides: Partial<TraceSpan> = {}) {
return {
spanID: '6605c7b08e715d6c',
traceID: '7946b05c2e2e4e5a',
@ -643,5 +807,5 @@ function createTraceSpan(overrides: Partial<TraceSpan> = {}): TraceSpan {
],
},
...overrides,
} as any;
} as TraceSpan;
}

View File

@ -22,6 +22,7 @@ import { SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components';
import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
import { TraceToMetricQuery, TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { ElasticsearchQuery } from 'app/plugins/datasource/elasticsearch/types';
import { PromQuery } from 'app/plugins/datasource/prometheus/types';
import { LokiQuery } from '../../../plugins/datasource/loki/types';
@ -103,7 +104,7 @@ function legacyCreateSpanLinkFactory(
// 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<LokiQuery | DataQuery> | undefined = {} as DataLink<LokiQuery | DataQuery> | undefined;
let dataLink: DataLink | undefined;
// Get logs link
if (logsDataSourceSettings && traceToLogsOptions) {
@ -114,6 +115,8 @@ function legacyCreateSpanLinkFactory(
case 'grafana-splunk-datasource':
dataLink = getLinkForSplunk(span, traceToLogsOptions, logsDataSourceSettings);
break;
case 'elasticsearch':
dataLink = getLinkForElasticsearch(span, traceToLogsOptions, logsDataSourceSettings);
}
if (dataLink) {
@ -280,6 +283,61 @@ function getLinkForLoki(span: TraceSpan, options: TraceToLogsOptions, dataSource
return dataLink;
}
function getLinkForElasticsearch(
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;
}, []);
let query = '';
if (tags.length > 0) {
query += `${tags.join(' AND ')}`;
}
if (filterByTraceID && span.traceID) {
query = `"${span.traceID}" AND ` + query;
}
if (filterBySpanID && span.spanID) {
query = `"${span.spanID}" AND ` + query;
}
const dataLink: DataLink<ElasticsearchQuery> = {
title: dataSourceSettings.name,
url: '',
internal: {
datasourceUid: dataSourceSettings.uid,
datasourceName: dataSourceSettings.name,
query: {
query: query,
refId: '',
metrics: [
{
id: '1',
type: 'logs',
},
],
},
},
};
return dataLink;
}
function getLinkForSplunk(
span: TraceSpan,
options: TraceToLogsOptions,