diff --git a/docs/sources/datasources/jaeger.md b/docs/sources/datasources/jaeger.md index 0f4509aa46c..f0b686cd6d8 100644 --- a/docs/sources/datasources/jaeger.md +++ b/docs/sources/datasources/jaeger.md @@ -32,6 +32,7 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t - **Data source -** Target data source. - **Tags -** The tags that will be used in the Loki query. Default is `'cluster', 'hostname', 'namespace', 'pod'`. +- **Map tag names -** When enabled, allows configuring how Jaeger tag names map to Loki label names. For example, map `service.name` to `service`. - **Span start time shift -** Shift in the start time for the Loki query based on the span start time. In order to extend to the past, you need to use a negative value. Use time interval units like 5s, 1m, 3h. The default is 0. - **Span end time shift -** Shift in the end time for the Loki query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0. - **Filter by Trace ID -** Toggle to append the trace ID to the Loki query. @@ -147,12 +148,15 @@ datasources: tracesToLogs: # Field with internal link pointing to a Loki data source in Grafana. # datasourceUid value must match the `datasourceUid` value of the Loki data source. - datasourceUid: loki - tags: - - cluster - - hostname - - namespace - - pod + datasourceUid: 'loki' + tags: ['job', 'instance', 'pod', 'namespace'] + mappedTags: [{ key: 'service.name', value: 'service' }] + mapTagNamesEnabled: false + spanStartTimeShift: '1h' + spanEndTimeShift: '1h' + filterByTraceID: false + filterBySpanID: false + lokiSearch: true secureJsonData: basicAuthPassword: my_password ``` diff --git a/docs/sources/datasources/tempo.md b/docs/sources/datasources/tempo.md index eeb5ad5d77e..0a688d7f2d5 100644 --- a/docs/sources/datasources/tempo.md +++ b/docs/sources/datasources/tempo.md @@ -31,6 +31,7 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t - **Data source -** Target data source. - **Tags -** The tags that will be used in the Loki query. Default is `'cluster', 'hostname', 'namespace', 'pod'`. +- **Map tag names -** When enabled, allows configuring how Tempo tag names map to Loki label names. For example, map `service.name` to `service`. - **Span start time shift -** A shift in the start time for the Loki query based on the start time for the span. To extend the time to the past, use a negative value. You can use time units, for example, 5s, 1m, 3h. The default is 0. - **Span end time shift -** Shift in the end time for the Loki query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0. - **Filter by Trace ID -** Toggle to append the trace ID to the Loki query. @@ -167,6 +168,8 @@ datasources: tracesToLogs: datasourceUid: 'loki' tags: ['job', 'instance', 'pod', 'namespace'] + mappedTags: [{ key: 'service.name', value: 'service' }] + mapTagNamesEnabled: false spanStartTimeShift: '1h' spanEndTimeShift: '1h' filterByTraceID: false diff --git a/docs/sources/datasources/zipkin.md b/docs/sources/datasources/zipkin.md index 1b1f4b6df86..ed530afa4ec 100644 --- a/docs/sources/datasources/zipkin.md +++ b/docs/sources/datasources/zipkin.md @@ -32,6 +32,7 @@ This is a configuration for the [trace to logs feature]({{< relref "../explore/t - **Data source -** Target data source. - **Tags -** The tags that will be used in the Loki query. Default is `'cluster', 'hostname', 'namespace', 'pod'`. +- **Map tag names -** When enabled, allows configuring how Zipkin tag names map to Loki label names. For example, map `service.name` to `service`. - **Span start time shift -** Shift in the start time for the Loki query based on the span start time. In order to extend to the past, you need to use a negative value. Use time interval units like 5s, 1m, 3h. The default is 0. - **Span end time shift -** Shift in the end time for the Loki query based on the span end time. Time units can be used here, for example, 5s, 1m, 3h. The default is 0. - **Filter by Trace ID -** Toggle to append the trace ID to the Loki query. diff --git a/public/app/core/components/TraceToLogs/KeyValueInput.tsx b/public/app/core/components/TraceToLogs/KeyValueInput.tsx new file mode 100644 index 00000000000..40258a93874 --- /dev/null +++ b/public/app/core/components/TraceToLogs/KeyValueInput.tsx @@ -0,0 +1,110 @@ +import { css } from '@emotion/css'; +import { GrafanaTheme, KeyValue } from '@grafana/data'; +import { SegmentInput, useStyles, InlineLabel, Icon } from '@grafana/ui'; +import React from 'react'; + +const EQ_WIDTH = 3; // = 24px in inline label + +interface Props { + values: Array>; + onChange: (values: Array>) => void; + id?: string; + keyPlaceholder?: string; + valuePlaceholder?: string; +} + +const KeyValueInput = ({ + values, + onChange, + id, + keyPlaceholder = 'Key', + valuePlaceholder = 'Value (optional)', +}: Props) => { + const styles = useStyles(getStyles); + + return ( +
+ {values.length ? ( + values.map((value, idx) => ( +
+ { + onChange( + values.map((v, i) => { + if (i === idx) { + v.key = String(e); + } + return v; + }) + ); + }} + /> + + = + + { + onChange( + values.map((v, i) => { + if (i === idx) { + v.value = String(e); + } + return v; + }) + ); + }} + /> + + {idx === values.length - 1 ? ( + + ) : null} +
+ )) + ) : ( + + )} +
+ ); +}; + +export default KeyValueInput; + +const getStyles = (theme: GrafanaTheme) => ({ + wrapper: css` + display: flex; + flex-direction: column; + gap: ${theme.spacing.xs} 0; + `, + pair: css` + display: flex; + justify-content: start; + align-items: center; + `, + operator: css` + color: ${theme.palette.orange}; + `, +}); diff --git a/public/app/core/components/TraceToLogsSettings.tsx b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx similarity index 73% rename from public/app/core/components/TraceToLogsSettings.tsx rename to public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx index f2ebc087211..e8a47e9bcff 100644 --- a/public/app/core/components/TraceToLogsSettings.tsx +++ b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx @@ -3,15 +3,19 @@ import { DataSourceJsonData, DataSourcePluginOptionsEditorProps, GrafanaTheme, + KeyValue, updateDatasourcePluginJsonDataOption, } from '@grafana/data'; import { DataSourcePicker } from '@grafana/runtime'; import { InlineField, InlineFieldRow, Input, TagsInput, useStyles, InlineSwitch } from '@grafana/ui'; import React from 'react'; +import KeyValueInput from './KeyValueInput'; export interface TraceToLogsOptions { datasourceUid?: string; tags?: string[]; + mappedTags?: Array>; + mapTagNamesEnabled?: boolean; spanStartTimeShift?: string; spanEndTimeShift?: string; filterByTraceID?: boolean; @@ -54,19 +58,64 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) { + {options.jsonData.tracesToLogs?.mapTagNamesEnabled ? ( + + + ({ key: tag })) ?? + [] + } + onChange={(v) => + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', { + ...options.jsonData.tracesToLogs, + mappedTags: v, + }) + } + /> + + + ) : ( + + + + updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', { + ...options.jsonData.tracesToLogs, + tags: tags, + }) + } + /> + + + )} + - + ) => updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', { - datasourceUid: options.jsonData.tracesToLogs?.datasourceUid, - tags: tags, + ...options.jsonData.tracesToLogs, + mapTagNamesEnabled: event.currentTarget.checked, }) } /> @@ -156,6 +205,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) { /> + { expect(linkDef!.href).toBe('testSpanId'); }); + + it('handles renamed tags', () => { + const createLink = setupSpanLinkFactory({ + mapTagNamesEnabled: true, + mappedTags: [ + { key: 'service.name', value: 'service' }, + { key: 'k8s.pod.name', value: 'pod' }, + ], + }); + expect(createLink).toBeDefined(); + const linkDef = createLink!( + createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'service.name', value: 'serviceName' }, + { key: 'k8s.pod.name', value: 'podName' }, + ], + }, + }) + ); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{service=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}' + )}` + ); + }); + + it('handles incomplete renamed tags', () => { + const createLink = setupSpanLinkFactory({ + mapTagNamesEnabled: true, + mappedTags: [ + { key: 'service.name', value: '' }, + { key: 'k8s.pod.name', value: 'pod' }, + ], + }); + expect(createLink).toBeDefined(); + const linkDef = createLink!( + createTraceSpan({ + process: { + serviceName: 'service', + tags: [ + { key: 'service.name', value: 'serviceName' }, + { key: 'k8s.pod.name', value: 'podName' }, + ], + }, + }) + ); + expect(linkDef!.href).toBe( + `/explore?left=${encodeURIComponent( + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{service.name=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}' + )}` + ); + }); }); }); diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 3133272d1d3..5170eb41236 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -3,6 +3,7 @@ import { DataLink, dateTime, Field, + KeyValue, mapInternalLinkToExplore, rangeUtil, SplitOpen, @@ -11,7 +12,7 @@ import { import { getTemplateSrv } from '@grafana/runtime'; import { Icon } from '@grafana/ui'; import { SpanLinkDef, SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components'; -import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings'; +import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import React from 'react'; import { LokiQuery } from '../../../plugins/datasource/loki/types'; @@ -122,11 +123,22 @@ function legacyCreateSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions? const defaultKeys = ['cluster', 'hostname', 'namespace', 'pod']; function getLokiQueryFromSpan(span: TraceSpan, options: TraceToLogsOptions): string { - const { tags: keys, filterByTraceID, filterBySpanID } = options; - const keysToCheck = keys?.length ? keys : defaultKeys; + const { tags: keys, filterByTraceID, filterBySpanID, mapTagNamesEnabled, mappedTags } = 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 (keysToCheck.includes(tag.key)) { - acc.push(`${tag.key}="${tag.value}"`); + 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[]); diff --git a/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx b/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx index 6ce9e60fd92..7ad0fe8ead0 100644 --- a/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx +++ b/public/app/plugins/datasource/jaeger/components/ConfigEditor.tsx @@ -1,7 +1,7 @@ import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { DataSourceHttpSettings } from '@grafana/ui'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; -import { TraceToLogsSettings } from 'app/core/components/TraceToLogsSettings'; +import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import React from 'react'; export type Props = DataSourcePluginOptionsEditorProps; diff --git a/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx b/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx index e55dd42fb75..b5bc57e135d 100644 --- a/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx +++ b/public/app/plugins/datasource/tempo/QueryEditor/QueryField.tsx @@ -12,7 +12,7 @@ import { Themeable2, withTheme2, } from '@grafana/ui'; -import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings'; +import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import React from 'react'; import { LokiQueryField } from '../../loki/components/LokiQueryField'; import { LokiQuery } from '../../loki/types'; diff --git a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx index d8481a92e2a..d00d8955ae0 100644 --- a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx @@ -1,6 +1,6 @@ import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { DataSourceHttpSettings } from '@grafana/ui'; -import { TraceToLogsSettings } from 'app/core/components/TraceToLogsSettings'; +import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import React from 'react'; import { ServiceGraphSettings } from './ServiceGraphSettings'; import { config } from '@grafana/runtime'; diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index e6b4470cd2c..4b77522a3eb 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -10,7 +10,7 @@ import { isValidGoDuration, LoadingState, } from '@grafana/data'; -import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings'; +import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { config, BackendSrvRequest, DataSourceWithBackend, getBackendSrv } from '@grafana/runtime'; import { serializeParams } from 'app/core/utils/fetch'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; diff --git a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx index 88b15b191d4..59a00826b8e 100644 --- a/public/app/plugins/datasource/zipkin/ConfigEditor.tsx +++ b/public/app/plugins/datasource/zipkin/ConfigEditor.tsx @@ -1,7 +1,7 @@ import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { DataSourceHttpSettings } from '@grafana/ui'; import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings'; -import { TraceToLogsSettings } from 'app/core/components/TraceToLogsSettings'; +import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import React from 'react'; export type Props = DataSourcePluginOptionsEditorProps;