From a83dee603147f329edc79f0267e39e6a1eddd373 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 3 Nov 2022 09:52:40 +0100 Subject: [PATCH] Elasticsearch: Add trace to logs functionality (#58063) * Elasticsearch: Implement trace to logs * Fix tests --- .betterer.results | 14 +- .../TraceToLogs/TraceToLogsSettings.tsx | 6 +- .../explore/TraceView/createSpanLink.test.ts | 194 ++++++++++++++++-- .../explore/TraceView/createSpanLink.tsx | 60 +++++- 4 files changed, 241 insertions(+), 33 deletions(-) diff --git a/.betterer.results b/.betterer.results index 089366bfc15..bffae94656a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx index 5085be0b43a..0b7933461f6 100644 --- a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx +++ b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx @@ -34,6 +34,7 @@ interface Props extends DataSourcePluginOptionsEditorProps {} export function TraceToLogsSettings({ options, onOptionsChange }: Props) { const styles = useStyles2(getStyles); + const supportedDataSourceTypes = ['loki', 'grafana-splunk-datasource', 'elasticsearch']; return (
@@ -47,10 +48,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) { { - // 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} diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 925cd932fb4..f65a1241f1c 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -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 = {}, datasourceUid = 'lokiUid') { @@ -602,12 +766,12 @@ function setupSpanLinkFactory(options: Partial = {}, datasou createFocusSpanLink: (traceId, spanId) => { return { href: `${traceId}-${spanId}`, - } as any; + } as unknown as LinkModel; }, }); } -function createTraceSpan(overrides: Partial = {}): TraceSpan { +function createTraceSpan(overrides: Partial = {}) { return { spanID: '6605c7b08e715d6c', traceID: '7946b05c2e2e4e5a', @@ -643,5 +807,5 @@ function createTraceSpan(overrides: Partial = {}): TraceSpan { ], }, ...overrides, - } as any; + } as TraceSpan; } diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 25a32994adb..6d06f8c8ec6 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -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 | undefined = {} as DataLink | 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 = { + 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,