Tracing: Add option to map tag names to log label names in trace to logs settings (#45178)

* Add mapped tags input to allow renaming tags in trace to logs settings

* Use mappedTags in createSpanLink

* Update traceToLogs docs

* Show 'add kv' button if no tags

* Update docs

* Default mappedTags to tag values
This commit is contained in:
Connor Lindsey 2022-02-22 15:17:45 -07:00 committed by GitHub
parent 6165377eb2
commit af2d19b02e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 260 additions and 26 deletions

View File

@ -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
```

View File

@ -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

View File

@ -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.

View File

@ -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<KeyValue<string>>;
onChange: (values: Array<KeyValue<string>>) => void;
id?: string;
keyPlaceholder?: string;
valuePlaceholder?: string;
}
const KeyValueInput = ({
values,
onChange,
id,
keyPlaceholder = 'Key',
valuePlaceholder = 'Value (optional)',
}: Props) => {
const styles = useStyles(getStyles);
return (
<div className={styles.wrapper}>
{values.length ? (
values.map((value, idx) => (
<div className={styles.pair} key={idx}>
<SegmentInput
id={`${id}-key-${idx}`}
placeholder={keyPlaceholder}
value={value.key}
onChange={(e) => {
onChange(
values.map((v, i) => {
if (i === idx) {
v.key = String(e);
}
return v;
})
);
}}
/>
<InlineLabel aria-label="equals" className={styles.operator} width={EQ_WIDTH}>
=
</InlineLabel>
<SegmentInput
id={`${id}-value-${idx}`}
placeholder={valuePlaceholder}
value={value.value}
onChange={(e) => {
onChange(
values.map((v, i) => {
if (i === idx) {
v.value = String(e);
}
return v;
})
);
}}
/>
<button
onClick={() => onChange([...values.slice(0, idx), ...values.slice(idx + 1)])}
className="gf-form-label query-part"
aria-label="Remove tag"
>
<Icon name="times" />
</button>
{idx === values.length - 1 ? (
<button
onClick={() => onChange([...values, { key: '', value: '' }])}
className="gf-form-label query-part"
aria-label="Add tag"
>
<Icon name="plus" />
</button>
) : null}
</div>
))
) : (
<button
onClick={() => onChange([...values, { key: '', value: '' }])}
className="gf-form-label query-part"
aria-label="Add tag"
>
<Icon name="plus" />
</button>
)}
</div>
);
};
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};
`,
});

View File

@ -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<KeyValue<string>>;
mapTagNamesEnabled?: boolean;
spanStartTimeShift?: string;
spanEndTimeShift?: string;
filterByTraceID?: boolean;
@ -54,19 +58,64 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
</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>
)}
<InlineFieldRow>
<InlineField
tooltip="Tags that will be used in the Loki query. Default tags: 'cluster', 'hostname', 'namespace', 'pod'"
label="Tags"
label="Map tag names"
labelWidth={26}
grow
tooltip="Map trace tag names to log label names. Ex: k8s.pod.name -> pod"
>
<TagsInput
tags={options.jsonData.tracesToLogs?.tags}
width={40}
onChange={(tags) =>
<InlineSwitch
id="mapTagNames"
value={options.jsonData.tracesToLogs?.mapTagNamesEnabled ?? false}
onChange={(event: React.SyntheticEvent<HTMLInputElement>) =>
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) {
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Loki Search" labelWidth={26} grow tooltip="Use this logs data source to search for traces.">
<InlineSwitch

View File

@ -19,7 +19,7 @@ import {
transformTraceData,
TTraceTimeline,
} from '@jaegertracing/jaeger-ui-components';
import { TraceToLogsData } from 'app/core/components/TraceToLogsSettings';
import { TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { StoreState } from 'app/types';

View File

@ -2,7 +2,7 @@ import { DataSourceInstanceSettings, MutableDataFrame } from '@grafana/data';
import { setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
import { createSpanLinkFactory } from './createSpanLink';
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
import { TraceToLogsOptions } from '../../../core/components/TraceToLogsSettings';
import { TraceToLogsOptions } from '../../../core/components/TraceToLogs/TraceToLogsSettings';
import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv';
import { TemplateSrv } from '../../templating/template_srv';
@ -141,6 +141,60 @@ describe('createSpanLinkFactory', () => {
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":{}}'
)}`
);
});
});
});

View File

@ -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[]);

View File

@ -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;

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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;