mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tempo: Traces to Logs - From viewing Tempo traces to displaying Splunk logs (#46855)
* Add functionality to see splunk logs via tempo trace view
This commit is contained in:
@@ -13,11 +13,11 @@ describe('createSpanLinkFactory', () => {
|
||||
expect(createLink).not.toBeDefined();
|
||||
});
|
||||
|
||||
describe('should return link', () => {
|
||||
describe('should return loki link', () => {
|
||||
beforeAll(() => {
|
||||
setDataSourceSrv({
|
||||
getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined {
|
||||
return { uid: 'loki1', name: 'loki1' } as any;
|
||||
return { uid: 'loki1', name: 'loki1', type: 'loki' } as any;
|
||||
},
|
||||
} as any);
|
||||
|
||||
@@ -215,14 +215,140 @@ describe('createSpanLinkFactory', () => {
|
||||
expect(linkDef).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return splunk link', () => {
|
||||
const splunkUID = 'splunkUID';
|
||||
|
||||
beforeAll(() => {
|
||||
setDataSourceSrv({
|
||||
getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined {
|
||||
return { uid: splunkUID, name: 'Splunk 8', type: 'grafana-splunk-datasource' } as any;
|
||||
},
|
||||
} as any);
|
||||
|
||||
setLinkSrv(new LinkSrv());
|
||||
setTemplateSrv(new TemplateSrv());
|
||||
});
|
||||
|
||||
it('the `query` keyword is used in the link rather than `expr` that loki uses', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
datasourceUid: splunkUID,
|
||||
});
|
||||
const linkDef = createLink!(createTraceSpan());
|
||||
expect(linkDef!.href).toContain(`${encodeURIComponent('datasource":"Splunk 8","queries":[{"query"')}`);
|
||||
expect(linkDef!.href).not.toContain(`${encodeURIComponent('datasource":"Splunk 8","queries":[{"expr"')}`);
|
||||
});
|
||||
|
||||
it('automatically timeshifts the timerange by one second in a splunk query', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
datasourceUid: splunkUID,
|
||||
});
|
||||
const linkDef = createLink!(createTraceSpan());
|
||||
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: splunkUID,
|
||||
filterByTraceID: true,
|
||||
filterBySpanID: true,
|
||||
});
|
||||
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!(createTraceSpan());
|
||||
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}],"panelsState":{}}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should format one tag correctly', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: ['ip'],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!(
|
||||
createTraceSpan({
|
||||
process: {
|
||||
serviceName: 'service',
|
||||
tags: [{ key: 'ip', value: '192.168.0.1' }],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}],"panelsState":{}}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should format multiple tags correctly', () => {
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: ['ip', 'hostname'],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!(
|
||||
createTraceSpan({
|
||||
process: {
|
||||
serviceName: 'service',
|
||||
tags: [
|
||||
{ key: 'hostname', value: 'hostname1' },
|
||||
{ key: 'ip', value: '192.168.0.1' },
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"hostname=\\"hostname1\\" ip=\\"192.168.0.1\\"","refId":""}],"panelsState":{}}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
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":"Splunk 8","queries":[{"query":"service=\\"serviceName\\" pod=\\"podName\\"","refId":""}],"panelsState":{}}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupSpanLinkFactory(options: Partial<TraceToLogsOptions> = {}) {
|
||||
function setupSpanLinkFactory(options: Partial<TraceToLogsOptions> = {}, datasourceUid = 'lokiUid') {
|
||||
const splitOpenFn = jest.fn();
|
||||
return createSpanLinkFactory({
|
||||
splitOpenFn,
|
||||
traceToLogsOptions: {
|
||||
datasourceUid: 'lokiUid',
|
||||
datasourceUid,
|
||||
...options,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import {
|
||||
DataFrame,
|
||||
DataLink,
|
||||
DataQuery,
|
||||
DataSourceInstanceSettings,
|
||||
dateTime,
|
||||
Field,
|
||||
KeyValue,
|
||||
LinkModel,
|
||||
mapInternalLinkToExplore,
|
||||
rangeUtil,
|
||||
SplitOpen,
|
||||
@@ -70,6 +73,7 @@ function legacyCreateSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions?
|
||||
}
|
||||
|
||||
const dataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToLogsOptions.datasourceUid);
|
||||
const isSplunkDS = dataSourceSettings?.type === 'grafana-splunk-datasource';
|
||||
|
||||
if (!dataSourceSettings) {
|
||||
return undefined;
|
||||
@@ -80,35 +84,37 @@ function legacyCreateSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions?
|
||||
// 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 link: LinkModel<Field>;
|
||||
|
||||
const expr = getLokiQueryFromSpan(span, traceToLogsOptions);
|
||||
if (!expr) {
|
||||
return undefined;
|
||||
switch (dataSourceSettings?.type) {
|
||||
case 'loki':
|
||||
dataLink = getLinkForLoki(span, traceToLogsOptions, dataSourceSettings);
|
||||
if (!dataLink) {
|
||||
return undefined;
|
||||
}
|
||||
break;
|
||||
case 'grafana-splunk-datasource':
|
||||
dataLink = getLinkForSplunk(span, traceToLogsOptions, dataSourceSettings);
|
||||
break;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dataLink: DataLink<LokiQuery> = {
|
||||
title: dataSourceSettings.name,
|
||||
url: '',
|
||||
internal: {
|
||||
datasourceUid: dataSourceSettings.uid,
|
||||
datasourceName: dataSourceSettings.name,
|
||||
query: {
|
||||
expr,
|
||||
refId: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const link = mapInternalLinkToExplore({
|
||||
link = mapInternalLinkToExplore({
|
||||
link: dataLink,
|
||||
internalLink: dataLink.internal!,
|
||||
internalLink: dataLink?.internal!,
|
||||
scopedVars: {},
|
||||
range: getTimeRangeFromSpan(span, {
|
||||
startMs: traceToLogsOptions.spanStartTimeShift
|
||||
? rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift)
|
||||
: 0,
|
||||
endMs: traceToLogsOptions.spanEndTimeShift ? rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift) : 0,
|
||||
}),
|
||||
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()),
|
||||
@@ -126,13 +132,11 @@ function legacyCreateSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions?
|
||||
* Default keys to use when there are no configured tags.
|
||||
*/
|
||||
const defaultKeys = ['cluster', 'hostname', 'namespace', 'pod'];
|
||||
|
||||
function getLokiQueryFromSpan(span: TraceSpan, options: TraceToLogsOptions): string | undefined {
|
||||
function getLinkForLoki(span: TraceSpan, options: TraceToLogsOptions, dataSourceSettings: DataSourceInstanceSettings) {
|
||||
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 (mapTagNamesEnabled) {
|
||||
@@ -152,17 +156,79 @@ function getLokiQueryFromSpan(span: TraceSpan, options: TraceToLogsOptions): str
|
||||
if (!tags.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let query = `{${tags.join(', ')}}`;
|
||||
|
||||
let expr = `{${tags.join(', ')}}`;
|
||||
if (filterByTraceID && span.traceID) {
|
||||
query += ` |="${span.traceID}"`;
|
||||
expr += ` |="${span.traceID}"`;
|
||||
}
|
||||
if (filterBySpanID && span.spanID) {
|
||||
query += ` |="${span.spanID}"`;
|
||||
expr += ` |="${span.spanID}"`;
|
||||
}
|
||||
|
||||
return query;
|
||||
const dataLink: DataLink<LokiQuery> = {
|
||||
title: dataSourceSettings.name,
|
||||
url: '',
|
||||
internal: {
|
||||
datasourceUid: dataSourceSettings.uid,
|
||||
datasourceName: dataSourceSettings.name,
|
||||
query: {
|
||||
expr: expr,
|
||||
refId: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return dataLink;
|
||||
}
|
||||
|
||||
function getLinkForSplunk(
|
||||
span: TraceSpan,
|
||||
options: TraceToLogsOptions,
|
||||
dataSourceSettings: DataSourceInstanceSettings
|
||||
) {
|
||||
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 (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[]);
|
||||
|
||||
let query = '';
|
||||
if (tags.length > 0) {
|
||||
query += `${tags.join(' ')}`;
|
||||
}
|
||||
if (filterByTraceID && span.traceID) {
|
||||
query += ` "${span.traceID}"`;
|
||||
}
|
||||
if (filterBySpanID && 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 dataLink;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,18 +236,23 @@ function getLokiQueryFromSpan(span: TraceSpan, options: TraceToLogsOptions): str
|
||||
*/
|
||||
function getTimeRangeFromSpan(
|
||||
span: TraceSpan,
|
||||
timeShift: { startMs: number; endMs: number } = { startMs: 0, endMs: 0 }
|
||||
timeShift: { startMs: number; endMs: number } = { startMs: 0, endMs: 0 },
|
||||
isSplunkDS = false
|
||||
): TimeRange {
|
||||
const adjustedStartTime = Math.floor(span.startTime / 1000 + timeShift.startMs);
|
||||
const from = dateTime(adjustedStartTime);
|
||||
const spanEndMs = (span.startTime + span.duration) / 1000;
|
||||
let adjustedEndTime = Math.floor(spanEndMs + timeShift.endMs);
|
||||
|
||||
// Because we can only pass milliseconds in the url we need to check if they equal.
|
||||
// We need end time to be later than start time
|
||||
if (adjustedStartTime === adjustedEndTime) {
|
||||
// Splunk requires a time interval of >= 1s, rather than >=1ms like Loki timerange in below elseif block
|
||||
if (isSplunkDS && adjustedEndTime - adjustedStartTime < 1000) {
|
||||
adjustedEndTime = adjustedStartTime + 1000;
|
||||
} else if (adjustedStartTime === adjustedEndTime) {
|
||||
// Because we can only pass milliseconds in the url we need to check if they equal.
|
||||
// We need end time to be later than start time
|
||||
adjustedEndTime++;
|
||||
}
|
||||
|
||||
const to = dateTime(adjustedEndTime);
|
||||
|
||||
// Beware that public/app/features/explore/state/main.ts SplitOpen fn uses the range from here. No matter what is in the url.
|
||||
|
||||
Reference in New Issue
Block a user