mirror of https://github.com/grafana/grafana
Explore: Add link to logs from trace span (#28229)
* Add trace to logs link * Do a bit of refactor and allow for custom time range in split * Add margin and noopener to the link * Fix tests * Fix testspull/28276/head
parent
26e2faa779
commit
c8658f3ee8
@ -0,0 +1,88 @@ |
||||
import { createSpanLinkFactory } from './createSpanLink'; |
||||
import { config, setDataSourceSrv, setTemplateSrv } from '@grafana/runtime'; |
||||
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; |
||||
|
||||
describe('createSpanLinkFactory', () => { |
||||
beforeAll(() => { |
||||
config.featureToggles.traceToLogs = true; |
||||
}); |
||||
|
||||
afterAll(() => { |
||||
config.featureToggles.traceToLogs = false; |
||||
}); |
||||
|
||||
it('returns undefined if there is no loki data source', () => { |
||||
setDataSourceSrv({ |
||||
getExternal() { |
||||
return [ |
||||
{ |
||||
meta: { |
||||
id: 'not loki', |
||||
}, |
||||
} as DataSourceInstanceSettings, |
||||
]; |
||||
}, |
||||
} as any); |
||||
const splitOpenFn = jest.fn(); |
||||
const createLink = createSpanLinkFactory(splitOpenFn); |
||||
expect(createLink).not.toBeDefined(); |
||||
}); |
||||
|
||||
it('creates correct link', () => { |
||||
setDataSourceSrv({ |
||||
getExternal() { |
||||
return [ |
||||
{ |
||||
name: 'loki1', |
||||
uid: 'lokiUid', |
||||
meta: { |
||||
id: 'loki', |
||||
}, |
||||
} as DataSourceInstanceSettings, |
||||
]; |
||||
}, |
||||
getDataSourceSettingsByUid(uid: string): DataSourceInstanceSettings | undefined { |
||||
if (uid === 'lokiUid') { |
||||
return { |
||||
name: 'Loki1', |
||||
} as any; |
||||
} |
||||
return undefined; |
||||
}, |
||||
} as any); |
||||
|
||||
setTemplateSrv({ |
||||
replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string { |
||||
return target!; |
||||
}, |
||||
} as any); |
||||
|
||||
const splitOpenFn = jest.fn(); |
||||
const createLink = createSpanLinkFactory(splitOpenFn); |
||||
expect(createLink).toBeDefined(); |
||||
const linkDef = createLink!({ |
||||
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000, |
||||
duration: 1000 * 1000, |
||||
process: { |
||||
tags: [ |
||||
{ |
||||
key: 'cluster', |
||||
value: 'cluster1', |
||||
}, |
||||
{ |
||||
key: 'hostname', |
||||
value: 'hostname1', |
||||
}, |
||||
{ |
||||
key: 'label2', |
||||
value: 'val2', |
||||
}, |
||||
], |
||||
} as any, |
||||
} as any); |
||||
|
||||
expect(linkDef.href).toBe( |
||||
`/explore?left={"range":{"from":"20201014T005955","to":"20201014T020001"},"datasource":"Loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}]}` |
||||
); |
||||
}); |
||||
}); |
@ -0,0 +1,92 @@ |
||||
import React from 'react'; |
||||
import { config, getDataSourceSrv, getTemplateSrv } from '@grafana/runtime'; |
||||
import { DataLink, dateTime, Field, mapInternalLinkToExplore, TimeRange, TraceSpan } from '@grafana/data'; |
||||
import { LokiQuery } from '../../../plugins/datasource/loki/types'; |
||||
import { Icon } from '@grafana/ui'; |
||||
|
||||
/** |
||||
* This is a factory for the link creator. It returns the function mainly so it can return undefined in which case |
||||
* the trace view won't create any links and to capture the datasource and split function making it easier to memoize |
||||
* with useMemo. |
||||
*/ |
||||
export function createSpanLinkFactory(splitOpenFn: (options: { datasourceUid: string; query: any }) => void) { |
||||
if (!config.featureToggles.traceToLogs) { |
||||
return undefined; |
||||
} |
||||
|
||||
// Right now just hardcoded for first loki DS we can find
|
||||
const lokiDs = getDataSourceSrv() |
||||
.getExternal() |
||||
.find(ds => ds.meta.id === 'loki'); |
||||
|
||||
if (!lokiDs) { |
||||
return undefined; |
||||
} |
||||
|
||||
return function(span: TraceSpan): { href: string; onClick?: (event: any) => void; content: React.ReactNode } { |
||||
// This is reusing existing code from derived fields which may not be ideal match so some data is a bit faked at
|
||||
// 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.
|
||||
|
||||
const dataLink: DataLink<LokiQuery> = { |
||||
title: lokiDs.name, |
||||
url: '', |
||||
internal: { |
||||
datasourceUid: lokiDs.uid, |
||||
query: { |
||||
expr: getLokiQueryFromSpan(span), |
||||
refId: '', |
||||
}, |
||||
}, |
||||
}; |
||||
const link = mapInternalLinkToExplore(dataLink, {}, getTimeRangeFromSpan(span), {} as Field, { |
||||
onClickFn: splitOpenFn, |
||||
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), |
||||
getDataSourceSettingsByUid: getDataSourceSrv().getDataSourceSettingsByUid.bind(getDataSourceSrv()), |
||||
}); |
||||
return { |
||||
href: link.href, |
||||
onClick: link.onClick, |
||||
content: <Icon name="file-alt" title="Show logs" />, |
||||
}; |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Right now this is just hardcoded and later will probably be part of some user configuration. |
||||
*/ |
||||
const allowedKeys = ['cluster', 'hostname', 'namespace', 'pod']; |
||||
|
||||
function getLokiQueryFromSpan(span: TraceSpan): string { |
||||
const tags = span.process.tags.reduce((acc, tag) => { |
||||
if (allowedKeys.includes(tag.key)) { |
||||
acc.push(`${tag.key}="${tag.value}"`); |
||||
} |
||||
return acc; |
||||
}, [] as string[]); |
||||
return `{${tags.join(', ')}}`; |
||||
} |
||||
|
||||
/** |
||||
* Gets a time range from the span. Naively this could be just start and end time of the span but we also want some |
||||
* buffer around that just so we do not miss some logs which may not have timestamps aligned with the span. Right |
||||
* now the buffers are hardcoded which may be a bit weird for very short spans but at the same time, fractional buffers |
||||
* with very short spans could mean microseconds and that could miss some logs relevant to that spans. In the future |
||||
* something more intelligent should probably be implemented |
||||
*/ |
||||
function getTimeRangeFromSpan(span: TraceSpan): TimeRange { |
||||
const from = dateTime(span.startTime / 1000 - 5 * 1000); |
||||
const spanEndMs = (span.startTime + span.duration) / 1000; |
||||
const to = dateTime(spanEndMs + 1000 * 60 * 60); |
||||
return { |
||||
from, |
||||
to, |
||||
// Weirdly Explore does not handle ISO string which would have been the default stringification if passed as object
|
||||
// and we have to use this custom format :( .
|
||||
raw: { |
||||
from: from.format('YYYYMMDDTHHmmss'), |
||||
to: to.format('YYYYMMDDTHHmmss'), |
||||
}, |
||||
}; |
||||
} |
Loading…
Reference in new issue