Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add links to logs from trace #93

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e780c60
update deps and fix errors
aocenas Sep 30, 2021
267a1ea
Add trace to logs section
aocenas Sep 30, 2021
8f1eadc
Merge branch 'master' into aocenas/trace-to-logs
aocenas Oct 6, 2021
89ae5b2
Remove unused code
aocenas Oct 12, 2021
332b772
Add links
aocenas Oct 15, 2021
4ea69dd
Merge branch 'master' into aocenas/trace-to-logs
aocenas Oct 15, 2021
8e42193
Convert to milliseconds
aocenas Oct 15, 2021
c32aae6
Merge branch 'aocenas/fix-times-2' into aocenas/trace-to-logs
aocenas Oct 15, 2021
1594cf7
Merge branch 'master' into aocenas/trace-to-logs
aocenas Oct 26, 2021
95a8377
Add filter by request id to log links
aocenas Dec 6, 2021
60317a3
Merge remote-tracking branch 'origin/aocenas/trace-to-logs' into aoce…
aocenas Dec 6, 2021
c221707
Fix transform test
aocenas Dec 14, 2021
24ad6af
Fix data source test
aocenas Dec 14, 2021
cdca104
Fix merge conflict
sarahzinger Dec 20, 2022
7b3ed0c
Only handle lambdas for now
sarahzinger Dec 20, 2022
2974ac6
Do not fetch account id if not on service map page
sarahzinger Jan 13, 2023
e305004
Merge branch 'main' into aocenas/trace-to-logs
sarahzinger Jan 23, 2023
261d810
Merge branch 'refactor-account-dropdown' into aocenas/trace-to-logs
sarahzinger Jan 23, 2023
24918a0
Lint fix
sarahzinger Jan 23, 2023
9032c45
Merge branch 'refactor-account-dropdown' into aocenas/trace-to-logs
sarahzinger Jan 23, 2023
1749a3c
Lint
sarahzinger Jan 23, 2023
ff5c348
Merge branch 'main' into aocenas/trace-to-logs
sarahzinger Jan 23, 2023
c76dd3d
Merge branch 'main' into aocenas/trace-to-logs
sarahzinger Feb 1, 2023
6f993a2
Add support for API gateway
sarahzinger Mar 13, 2023
2ee7bdf
Merge conflict
sarahzinger Mar 13, 2023
bb4b4fc
Test fix
sarahzinger Mar 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions src/DataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@grafana/data';
import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { mergeMap } from 'rxjs/operators';

import {
Group,
Expand All @@ -26,7 +26,7 @@ import {
} from './types';
import { parseGraphResponse, transformTraceResponse } from 'utils/transform';
import { XRayLanguageProvider } from 'language_provider';
import { makeLinks } from './utils/links';
import { addTraceToLogsLinks, makeServiceMapLinks } from './utils/links';

export class XrayDataSource extends DataSourceWithBackend<XrayQuery, XrayJsonData> {
private instanceSettings: DataSourceInstanceSettings<XrayJsonData>;
Expand All @@ -43,13 +43,17 @@ export class XrayDataSource extends DataSourceWithBackend<XrayQuery, XrayJsonDat
const processedRequest = processRequest(request, getTemplateSrv());
let response = super.query(processedRequest);
return response.pipe(
map((dataQueryResponse) => {
return {
...dataQueryResponse,
data: dataQueryResponse.data.flatMap((frame) => {
mergeMap(async (dataQueryResponse) => {
const parsedData = await Promise.all(
dataQueryResponse.data.map((frame) => {
const target = request.targets.find((t) => t.refId === frame.refId);
return this.parseResponse(frame, target);
}),
})
);

return {
...dataQueryResponse,
data: parsedData.flat(),
};
})
);
Expand Down Expand Up @@ -142,11 +146,11 @@ export class XrayDataSource extends DataSourceWithBackend<XrayQuery, XrayJsonDat
return `https://${region}.console.aws.amazon.com/xray/home?region=${region}`;
}

private parseResponse(response: DataFrame, query?: XrayQuery): DataFrame[] {
private async parseResponse(response: DataFrame, query?: XrayQuery): Promise<DataFrame[]> {
// TODO this would better be based on type but backend Go def does not have dataFrame.type
switch (response.name) {
case 'Traces':
return parseTraceResponse(response, query);
return parseTraceResponse(response, query, this.instanceSettings.jsonData.tracesToLogs?.datasourceUid);
case 'TraceSummaries':
return parseTracesListResponse(response, this.instanceSettings, query);
case 'InsightSummaries':
Expand Down Expand Up @@ -203,7 +207,11 @@ function getDurationText(duration: DateTimeDuration) {
The x-ray trace has a bit strange format where it comes as json and then some parts are string which also contains
json, so some parts are escaped and we have to double parse that.
*/
function parseTraceResponse(response: DataFrame, query?: XrayQuery): DataFrame[] {
async function parseTraceResponse(
response: DataFrame,
query: XrayQuery | undefined,
logsDatasourceUid: string | undefined
): Promise<DataFrame[]> {
// Again assuming this will ge single field with single value which will be the trace data blob
const traceData = response.fields[0].values.get(0);
const traceParsed: XrayTraceDataRaw = JSON.parse(traceData);
Expand All @@ -220,6 +228,7 @@ function parseTraceResponse(response: DataFrame, query?: XrayQuery): DataFrame[]
};

const frame = transformTraceResponse(traceParsedForReal);
await addTraceToLogsLinks(frame, query?.region, logsDatasourceUid);
frame.refId = query?.refId;

return [frame];
Expand Down Expand Up @@ -262,12 +271,12 @@ function parseServiceMapResponse(
const [servicesFrame, edgesFrame] = parseGraphResponse(response, query);
const serviceQuery = 'service(id(name: "${__data.fields.title}", type: "${__data.fields.subTitle}"))';
servicesFrame.fields[0].config = {
links: makeLinks(serviceQuery, instanceSettings, query),
links: makeServiceMapLinks(serviceQuery, instanceSettings, query),
};

const edgeQuery = 'edge("${__data.fields.sourceName}", "${__data.fields.targetName}")';
edgesFrame.fields[0].config = {
links: makeLinks(edgeQuery, instanceSettings, query),
links: makeServiceMapLinks(edgeQuery, instanceSettings, query),
};
return [servicesFrame, edgesFrame];
}
Expand Down
18 changes: 14 additions & 4 deletions src/components/ConfigEditor/ConfigEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { AwsAuthDataSourceJsonData, AwsAuthDataSourceSecureJsonData, ConnectionConfig } from '@grafana/aws-sdk';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { AwsAuthDataSourceSecureJsonData, ConnectionConfig } from '@grafana/aws-sdk';
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
import React, { PureComponent } from 'react';
import { TraceToLogs } from './TraceToLogs';
import { XrayJsonData } from '../../types';

export type Props = DataSourcePluginOptionsEditorProps<AwsAuthDataSourceJsonData, AwsAuthDataSourceSecureJsonData>;
export type Props = DataSourcePluginOptionsEditorProps<XrayJsonData, AwsAuthDataSourceSecureJsonData>;

export class ConfigEditor extends PureComponent<Props> {
render() {
const { onOptionsChange, options } = this.props;
return (
<div>
<ConnectionConfig {...this.props} />
{/* can add x-ray specific things here */}
<TraceToLogs
datasourceUid={options.jsonData.tracesToLogs?.datasourceUid}
onChange={(uid) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToLogs', {
datasourceUid: uid,
})
}
/>
</div>
);
}
Expand Down
43 changes: 43 additions & 0 deletions src/components/ConfigEditor/TraceToLogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { css } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime';
import { InlineField, InlineFieldRow, useStyles } from '@grafana/ui';
import React from 'react';

interface Props {
datasourceUid?: string;
onChange: (uid: string) => void;
}

export function TraceToLogs({ datasourceUid, onChange }: Props) {
const styles = useStyles(getStyles);

return (
<div className={css({ width: '100%' })}>
<h3 className="page-heading">Trace to logs</h3>

<div className={styles.infoText}>
Trace to logs let&apos;s you navigate from a trace span to the selected data source&apos;s log.
</div>

<InlineFieldRow>
<InlineField tooltip="The data source the trace is going to navigate to" label="Data source" labelWidth={26}>
<DataSourcePicker
pluginId="cloudwatch"
current={datasourceUid}
noDefault={true}
width={40}
onChange={(ds) => onChange(ds.uid)}
/>
</InlineField>
</InlineFieldRow>
</div>
);
}

const getStyles = (theme: GrafanaTheme) => ({
infoText: css`
padding-bottom: ${theme.spacing.md};
color: ${theme.colors.textSemiWeak};
`,
});
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ export enum XrayQueryType {
}

export interface XrayJsonData extends AwsAuthDataSourceJsonData {
// Can add X-Ray specific values here
tracesToLogs?: {
datasourceUid: string;
};
}

export interface TSDBResponse<T = any> {
Expand Down
62 changes: 59 additions & 3 deletions src/utils/links.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { XrayQuery, XrayQueryType } from '../types';
import { DataSourceInstanceSettings } from '@grafana/data';
import { DataFrame, DataLink, DataSourceInstanceSettings } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';

export function makeLinks(itemQuery: string, instanceSettings: DataSourceInstanceSettings, dataQuery?: XrayQuery) {
/**
* Create links that will be shown when clicked on a service in service map. They target the same data source but
* queries for traces related to that particular service
* @param itemQuery String query part that should filter a particular service
* @param instanceSettings
* @param dataQuery Existing query to pass on any additional query data so they stay the same, like region.
*/
export function makeServiceMapLinks(
itemQuery: string,
instanceSettings: DataSourceInstanceSettings,
dataQuery?: XrayQuery
) {
const makeLink = linkFactory(itemQuery, instanceSettings, dataQuery);
return [
makeLink('Traces/All', XrayQueryType.getTraceSummaries),
Expand All @@ -15,8 +27,52 @@ export function makeLinks(itemQuery: string, instanceSettings: DataSourceInstanc
];
}

export async function addTraceToLogsLinks(frame: DataFrame, region?: string, datasourceUid?: string) {
if (!datasourceUid) {
return;
}
try {
const spanField = frame.fields.find((f) => f.name === 'spanID')!;
const requestIdField = frame.fields.find((f) => f.name === '__request_id')!;
const link = await makeTraceToLogsLink(datasourceUid, region, {
hasRequestId: Boolean(requestIdField.values.get(0)),
});
spanField.config.links = [link];
} catch (error) {
// There are some things that can go wrong like datasourceUID not existing anymore etc. Does not seem useful to
// error the whole query in that case so we will just skip the links.
console.error(error);
}
}

async function makeTraceToLogsLink(
datasourceUid: string,
region = 'default',
options: { hasRequestId: boolean }
): Promise<DataLink> {
const logsDS = await getDataSourceSrv().get(datasourceUid);
const filter = options.hasRequestId
? 'filter @requestId = "${__data.fields.__request_id}" or @message like "${__data.fields.traceID}"'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be a filter that also x-ray uses when looking for logs from a trace in their UI.

: 'filter @message like "${__data.fields.traceID}"';
return {
title: 'CloudWatch Logs',
url: '',
internal: {
query: {
region,
queryMode: 'Logs',
// Just use the data from the data frame. Needs to be filled in during transform.
logGroupNames: ['${__data.fields.__log_group}'],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We assume the __log_group field is already present.

expression: `fields @message | ${filter}`,
},
datasourceUid,
datasourceName: logsDS.name,
},
};
}

function linkFactory(itemQuery: string, instanceSettings: DataSourceInstanceSettings, dataQuery?: XrayQuery) {
return (title: string, queryType: XrayQueryType, queryFilter?: string) => {
return (title: string, queryType: XrayQueryType, queryFilter?: string): DataLink => {
return {
title,
url: '',
Expand Down
Loading