mirror of https://github.com/grafana/grafana
CloudWatch Logs: Add link to Xray data source for trace IDs in logs (#39135)
* Refactor log query handling * Add link to config page * Change message about missing xray to alert * Add xrayTraceLinks * Fix typo in field name * Fix tests and lint * Move test * Add test for trace id link * lintpull/39198/head
parent
3c433dc36d
commit
fb1c31e1b6
@ -0,0 +1,57 @@ |
||||
import React from 'react'; |
||||
import { css } from '@emotion/css'; |
||||
import { Alert, InlineField, useStyles2 } from '@grafana/ui'; |
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; |
||||
import { DataSourcePicker } from '@grafana/runtime'; |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
infoText: css` |
||||
padding-bottom: ${theme.spacing(2)}; |
||||
color: ${theme.colors.text.secondary}; |
||||
`,
|
||||
}); |
||||
|
||||
interface Props { |
||||
datasourceUid?: string; |
||||
onChange: (uid: string) => void; |
||||
} |
||||
|
||||
const xRayDsId = 'grafana-x-ray-datasource'; |
||||
|
||||
export function XrayLinkConfig({ datasourceUid, onChange }: Props) { |
||||
const hasXrayDatasource = Boolean(getDatasourceSrv().getList({ pluginId: xRayDsId }).length); |
||||
|
||||
const styles = useStyles2(getStyles); |
||||
|
||||
return ( |
||||
<> |
||||
<h3 className="page-heading">X-ray trace link</h3> |
||||
|
||||
<div className={styles.infoText}> |
||||
Grafana will automatically create a link to a trace in X-ray data source if logs contain @xrayTraceId field |
||||
</div> |
||||
|
||||
{!hasXrayDatasource && ( |
||||
<Alert |
||||
title={ |
||||
'There is no X-ray datasource to link to. First add an X-ray data source and then link it to Cloud Watch. ' |
||||
} |
||||
severity="info" |
||||
/> |
||||
)} |
||||
|
||||
<div className="gf-form-group"> |
||||
<InlineField label="Data source" labelWidth={28} tooltip="X-ray data source containing traces"> |
||||
<DataSourcePicker |
||||
pluginId={xRayDsId} |
||||
onChange={(ds) => onChange(ds.uid)} |
||||
current={datasourceUid} |
||||
noDefault={true} |
||||
/> |
||||
</InlineField> |
||||
</div> |
||||
</> |
||||
); |
||||
} |
@ -0,0 +1,95 @@ |
||||
import { DataQueryResponse, dateMath } from '@grafana/data'; |
||||
import { addDataLinksToLogsResponse } from './datalinks'; |
||||
import { setDataSourceSrv } from '@grafana/runtime'; |
||||
|
||||
describe('addDataLinksToLogsResponse', () => { |
||||
it('should add data links to response', async () => { |
||||
const mockResponse: DataQueryResponse = { |
||||
data: [ |
||||
{ |
||||
fields: [ |
||||
{ |
||||
name: '@message', |
||||
config: {}, |
||||
}, |
||||
{ |
||||
name: '@xrayTraceId', |
||||
config: {}, |
||||
}, |
||||
], |
||||
refId: 'A', |
||||
}, |
||||
], |
||||
}; |
||||
|
||||
const mockOptions: any = { |
||||
targets: [ |
||||
{ |
||||
refId: 'A', |
||||
expression: 'stats count(@message) by bin(1h)', |
||||
logGroupNames: ['fake-log-group-one', 'fake-log-group-two'], |
||||
region: 'us-east-1', |
||||
}, |
||||
], |
||||
}; |
||||
|
||||
const time = { |
||||
from: dateMath.parse('2016-12-31 15:00:00Z', false)!, |
||||
to: dateMath.parse('2016-12-31 16:00:00Z', false)!, |
||||
}; |
||||
|
||||
setDataSourceSrv({ |
||||
async get() { |
||||
return { |
||||
name: 'Xray', |
||||
}; |
||||
}, |
||||
} as any); |
||||
|
||||
await addDataLinksToLogsResponse( |
||||
mockResponse, |
||||
mockOptions, |
||||
{ ...time, raw: time }, |
||||
(s) => s ?? '', |
||||
(r) => r, |
||||
'xrayUid' |
||||
); |
||||
expect(mockResponse).toMatchObject({ |
||||
data: [ |
||||
{ |
||||
fields: [ |
||||
{ |
||||
name: '@message', |
||||
config: { |
||||
links: [ |
||||
{ |
||||
url: |
||||
"https://us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#logs-insights:queryDetail=~(end~'2016-12-31T16*3a00*3a00.000Z~start~'2016-12-31T15*3a00*3a00.000Z~timeType~'ABSOLUTE~tz~'UTC~editorString~'stats*20count*28*40message*29*20by*20bin*281h*29~isLiveTail~false~source~(~'fake-log-group-one~'fake-log-group-two))", |
||||
title: 'View in CloudWatch console', |
||||
}, |
||||
], |
||||
}, |
||||
}, |
||||
{ |
||||
name: '@xrayTraceId', |
||||
config: { |
||||
links: [ |
||||
{ |
||||
url: '', |
||||
title: 'Xray', |
||||
internal: { |
||||
query: { query: '${__value.raw}', region: 'us-east-1', queryType: 'getTrace' }, |
||||
datasourceUid: 'xrayUid', |
||||
datasourceName: 'Xray', |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
}, |
||||
], |
||||
refId: 'A', |
||||
}, |
||||
], |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,88 @@ |
||||
import { DataFrame, DataLink, DataQueryRequest, DataQueryResponse, ScopedVars, TimeRange } from '@grafana/data'; |
||||
import { CloudWatchLogsQuery, CloudWatchQuery } from '../types'; |
||||
import { AwsUrl, encodeUrl } from '../aws_url'; |
||||
import { getDataSourceSrv } from '@grafana/runtime'; |
||||
|
||||
type ReplaceFn = ( |
||||
target?: string, |
||||
scopedVars?: ScopedVars, |
||||
displayErrorIfIsMultiTemplateVariable?: boolean, |
||||
fieldName?: string |
||||
) => string; |
||||
|
||||
export async function addDataLinksToLogsResponse( |
||||
response: DataQueryResponse, |
||||
request: DataQueryRequest<CloudWatchQuery>, |
||||
range: TimeRange, |
||||
replaceFn: ReplaceFn, |
||||
getRegion: (region: string) => string, |
||||
tracingDatasourceUid?: string |
||||
): Promise<void> { |
||||
const replace = (target: string, fieldName?: string) => replaceFn(target, request.scopedVars, true, fieldName); |
||||
|
||||
for (const dataFrame of response.data as DataFrame[]) { |
||||
const curTarget = request.targets.find((target) => target.refId === dataFrame.refId) as CloudWatchLogsQuery; |
||||
const interpolatedRegion = getRegion(replace(curTarget.region, 'region')); |
||||
|
||||
for (const field of dataFrame.fields) { |
||||
if (field.name === '@xrayTraceId' && tracingDatasourceUid) { |
||||
getRegion(replace(curTarget.region, 'region')); |
||||
const xrayLink = await createInternalXrayLink(tracingDatasourceUid, interpolatedRegion); |
||||
if (xrayLink) { |
||||
field.config.links = [xrayLink]; |
||||
} |
||||
} else { |
||||
// Right now we add generic link to open the query in xray console to every field so it shows in the logs row
|
||||
// details. Unfortunately this also creates link for all values inside table which look weird.
|
||||
field.config.links = [createAwsConsoleLink(curTarget, range, interpolatedRegion, replace)]; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
async function createInternalXrayLink(datasourceUid: string, region: string) { |
||||
let ds; |
||||
try { |
||||
ds = await getDataSourceSrv().get(datasourceUid); |
||||
} catch (e) { |
||||
console.error('Could not load linked xray data source, it was probably deleted after it was linked', e); |
||||
return undefined; |
||||
} |
||||
|
||||
return { |
||||
title: ds.name, |
||||
url: '', |
||||
internal: { |
||||
query: { query: '${__value.raw}', queryType: 'getTrace', region: region }, |
||||
datasourceUid: datasourceUid, |
||||
datasourceName: ds.name, |
||||
}, |
||||
} as DataLink; |
||||
} |
||||
|
||||
function createAwsConsoleLink( |
||||
target: CloudWatchLogsQuery, |
||||
range: TimeRange, |
||||
region: string, |
||||
replace: (target: string, fieldName?: string) => string |
||||
) { |
||||
const interpolatedExpression = target.expression ? replace(target.expression) : ''; |
||||
const interpolatedGroups = target.logGroupNames?.map((logGroup: string) => replace(logGroup, 'log groups')) ?? []; |
||||
|
||||
const urlProps: AwsUrl = { |
||||
end: range.to.toISOString(), |
||||
start: range.from.toISOString(), |
||||
timeType: 'ABSOLUTE', |
||||
tz: 'UTC', |
||||
editorString: interpolatedExpression, |
||||
isLiveTail: false, |
||||
source: interpolatedGroups, |
||||
}; |
||||
|
||||
const encodedUrl = encodeUrl(urlProps, region); |
||||
return { |
||||
url: encodedUrl, |
||||
title: 'View in CloudWatch console', |
||||
targetBlank: true, |
||||
}; |
||||
} |
Loading…
Reference in new issue