Trace View: Add Session for this span button (#89656)

* Add ability to show Session for this span button in trace view

* Add session link when fe o11y ids are available

* Add tests for creating session links

* Also check for session id written following otel semantic convention
This commit is contained in:
Javier Ruiz 2024-06-25 13:48:06 +02:00 committed by GitHub
parent 0759c98504
commit df4b280134
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 94 additions and 2 deletions

View File

@ -114,6 +114,11 @@ const getStyles = (theme: GrafanaTheme2) => {
LinkIcon: css`
font-size: 1.5em;
`,
linkList: css({
display: 'flex',
flexWrap: 'wrap',
gap: '10px',
}),
};
};
@ -303,6 +308,7 @@ export default function SpanDetail(props: SpanDetailProps) {
let logLinkButton: JSX.Element | null = null;
let profileLinkButton: JSX.Element | null = null;
let sessionLinkButton: JSX.Element | null = null;
if (createSpanLink) {
const links = createSpanLink(span);
const logsLink = links?.filter((link) => link.type === SpanLinkType.Logs);
@ -315,6 +321,15 @@ export default function SpanDetail(props: SpanDetailProps) {
if (links && profilesLink && profilesLink.length > 0) {
profileLinkButton = createLinkButton(profilesLink[0], SpanLinkType.Profiles, 'Profiles for this span', 'link');
}
const sessionLink = links?.filter((link) => link.type === SpanLinkType.Session);
if (links && sessionLink && sessionLink.length > 0) {
sessionLinkButton = createLinkButton(
sessionLink[0],
SpanLinkType.Session,
'Session for this span',
'frontend-observability'
);
}
}
const focusSpanLink = createFocusSpanLink(traceID, spanID);
@ -326,8 +341,11 @@ export default function SpanDetail(props: SpanDetailProps) {
<LabeledList className={styles.list} divider={true} items={overviewItems} />
</div>
</div>
<span style={{ marginRight: '10px' }}>{logLinkButton}</span>
{profileLinkButton}
<div className={styles.linkList}>
{logLinkButton}
{profileLinkButton}
{sessionLinkButton}
</div>
<Divider spacing={1} />
<div>
<div>

View File

@ -9,6 +9,7 @@ export enum SpanLinkType {
Traces = 'trace',
Metrics = 'metric',
Profiles = 'profile',
Session = 'session',
Unknown = 'unknown',
}

View File

@ -1266,6 +1266,57 @@ describe('createSpanLinkFactory', () => {
});
});
describe('should return session link', () => {
beforeAll(() => {
setLinkSrv(new LinkSrv());
setTemplateSrv(new TemplateSrv());
});
it('does not add link when no ids are present', () => {
const createLink = setupSpanLinkFactory();
expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan());
const sessionLink = links?.find((link) => link.type === SpanLinkType.Session);
expect(sessionLink).toBe(undefined);
});
it('adds link when fe o11y ids are present', () => {
const createLink = setupSpanLinkFactory();
expect(createLink).toBeDefined();
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'feo11y-service',
tags: [{ key: 'gf.feo11y.app.id', value: 'appId' }],
},
tags: [{ key: 'session_id', value: 'the-session-id' }],
})
);
expect(links).toHaveLength(1);
const sessionLink = links?.find((link) => link.type === SpanLinkType.Session);
expect(sessionLink).toBeDefined();
expect(sessionLink!.href).toBe('/a/grafana-kowalski-app/apps/appId/sessions/the-session-id');
});
it('adds link when session id is defined following otel semantic convention', () => {
const createLink = setupSpanLinkFactory();
expect(createLink).toBeDefined();
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'feo11y-service',
tags: [{ key: 'gf.feo11y.app.id', value: 'appId' }],
},
tags: [{ key: 'session.id', value: 'the-otel-session-id' }],
})
);
expect(links).toHaveLength(1);
const sessionLink = links?.find((link) => link.type === SpanLinkType.Session);
expect(sessionLink).toBeDefined();
expect(sessionLink!.href).toBe('/a/grafana-kowalski-app/apps/appId/sessions/the-otel-session-id');
});
});
describe('should return pyroscope link', () => {
beforeAll(() => {
setDataSourceSrv({

View File

@ -137,6 +137,7 @@ const formatDefaultKeys = (keys: string[]) => {
const defaultKeys = formatDefaultKeys(['cluster', 'hostname', 'namespace', 'pod', 'service.name', 'service.namespace']);
export const defaultProfilingKeys = formatDefaultKeys(['service.name', 'service.namespace']);
export const pyroscopeProfileIdTagKey = 'pyroscope.profile.id';
export const feO11yTagKey = 'gf.feo11y.app.id';
function legacyCreateSpanLinkFactory(
splitOpenFn: SplitOpen,
@ -348,6 +349,18 @@ function legacyCreateSpanLinkFactory(
}
}
// Get session links
const feO11yLink = getLinkForFeO11y(span);
if (feO11yLink) {
links.push({
title: 'Session for this span',
href: feO11yLink,
content: <Icon name="frontend-observability" title="Session for this span" />,
field,
type: SpanLinkType.Session,
});
}
return links;
};
}
@ -382,6 +395,15 @@ function getQueryForLoki(
};
}
function getLinkForFeO11y(span: TraceSpan): string | undefined {
const feO11yAppId = span.process.tags.find((tag) => tag.key === feO11yTagKey)?.value;
const feO11ySessionId = span.tags.find((tag) => tag.key === 'session_id' || tag.key === 'session.id')?.value;
return feO11yAppId && feO11ySessionId
? `/a/grafana-kowalski-app/apps/${feO11yAppId}/sessions/${feO11ySessionId}`
: undefined;
}
// we do not have access to the dataquery type for opensearch,
// so here is a minimal interface that handles both elasticsearch and opensearch.
interface ElasticsearchOrOpensearchQuery extends DataQuery {