Skip to content

Commit 3eba310

Browse files
kpattichakibanamachine
authored andcommitted
[Discover][Traces] Show errors in the context of a trace (#234178)
## Summary - Introduces `GET /internal/apm/unified_traces/{traceId}/errors` fetching the unified errors (apm, unprocessed otel) - Shows errors related to a trace (apm and uprocessed otel errors) - Fix the error count for the unified waterfall - Collapse Logs section by default ### APM errors https://github.com/user-attachments/assets/808732f5-86c0-4b9e-b5d1-d00a24ead44e ### Otel processed errors (sending to apm server) https://github.com/user-attachments/assets/529d3971-684c-45f0-a975-b2b7ed149f8c ### Otel unprocessed errors https://github.com/user-attachments/assets/a769394d-ee90-464a-a154-41c1b9c519d4 ### How to test it - Produce any errors (apm, otel, unprocessed errors) - Go to discover ### Todo - [ ] tests ### Notes - apm doesn't support unprocessed errors --------- Co-authored-by: kibanamachine <[email protected]>
1 parent 0adf7f2 commit 3eba310

File tree

27 files changed

+1139
-56
lines changed

27 files changed

+1139
-56
lines changed

src/platform/packages/shared/kbn-apm-synthtrace-client/src/lib/otel_logs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type OtelLogDocument = Fields &
1616
Partial<{
1717
_index?: string;
1818
trace_id?: string;
19+
span_id?: string;
1920
attributes?: Record<string, unknown>;
2021
severity_text?: string;
2122
resource?: {

src/platform/packages/shared/kbn-apm-synthtrace/src/scenarios/many_errors.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,25 @@ const scenario: Scenario<ApmFields> = async (runOptions) => {
4141
.interval('1m')
4242
.rate(2000)
4343
.generator((timestamp, index) => {
44-
const severity = severities[index % severities.length];
45-
const errorMessage = `${severity}: ${getRandomNameForIndex(index)} ${index}`;
44+
const errors = Array.from({ length: 10 }, (_, errorIndex) => {
45+
const severity = severities[errorIndex % severities.length];
46+
const errorMessage = `${severity}: ${getRandomNameForIndex(index)}`;
47+
48+
return instance
49+
.error({
50+
message: errorMessage + ` ${errorIndex}`,
51+
type: getExceptionTypeForIndex(index + errorIndex),
52+
culprit: 'request (node_modules/@elastic/transport/src/Transport.ts)',
53+
})
54+
.timestamp(timestamp + 50 * (errorIndex + 1)); // Stagger error timestamps
55+
});
56+
4657
return instance
4758
.transaction({ transactionName })
4859
.timestamp(timestamp)
4960
.duration(1000)
5061
.failure()
51-
.errors(
52-
instance
53-
.error({
54-
message: errorMessage,
55-
type: getExceptionTypeForIndex(index),
56-
culprit: 'request (node_modules/@elastic/transport/src/Transport.ts)',
57-
})
58-
.timestamp(timestamp + 50)
59-
);
62+
.errors(...errors);
6063
});
6164

6265
return withClient(

src/platform/plugins/shared/discover_shared/public/services/discover_features/types.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import type { DataTableRecord } from '@kbn/discover-utils';
1111
import type { FunctionComponent, PropsWithChildren } from 'react';
1212
import type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
1313
import type { Query, TimeRange } from '@kbn/es-query';
14-
import type { SpanLinks } from '@kbn/apm-types';
14+
import type { SpanLinks, ErrorsByTraceId } from '@kbn/apm-types';
1515
import type { ProcessorEvent } from '@kbn/apm-types-shared';
16+
1617
import type { FeaturesRegistry } from '../../../common';
1718

1819
/**
@@ -59,6 +60,19 @@ export interface ObservabilityTracesSpanLinksFeature {
5960
) => Promise<SpanLinks>;
6061
}
6162

63+
export interface ObservabilityTracesFetchErrorsFeature {
64+
id: 'observability-traces-fetch-errors';
65+
fetchErrorsByTraceId: (
66+
params: {
67+
traceId: string;
68+
docId?: string;
69+
start: string;
70+
end: string;
71+
},
72+
signal: AbortSignal
73+
) => Promise<ErrorsByTraceId>;
74+
}
75+
6276
export interface ObservabilityCreateSLOFeature {
6377
id: 'observability-create-slo';
6478
createSLOFlyout: (props: {
@@ -108,6 +122,7 @@ export type DiscoverFeature =
108122
| ObservabilityCreateSLOFeature
109123
| ObservabilityLogEventsFeature
110124
| ObservabilityTracesSpanLinksFeature
125+
| ObservabilityTracesFetchErrorsFeature
111126
| SecuritySolutionFeature;
112127

113128
/**

src/platform/plugins/shared/unified_doc_viewer/public/components/content_framework/section/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface ContentFrameworkSectionProps {
4040
actions?: Action[];
4141
children: React.ReactNode;
4242
'data-test-subj'?: string;
43+
initialIsOpen?: boolean;
4344
}
4445

4546
export function ContentFrameworkSection({
@@ -49,8 +50,9 @@ export function ContentFrameworkSection({
4950
actions,
5051
children,
5152
'data-test-subj': accordionDataTestSubj,
53+
initialIsOpen = true,
5254
}: ContentFrameworkSectionProps) {
53-
const [isAccordionExpanded, setIsAccordionExpanded] = useState(true);
55+
const [isAccordionExpanded, setIsAccordionExpanded] = useState(initialIsOpen);
5456
const renderActions = () => (
5557
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd" alignItems="center">
5658
{actions?.map((action, idx) => {
@@ -89,7 +91,7 @@ export function ContentFrameworkSection({
8991
<EuiAccordion
9092
data-test-subj={accordionDataTestSubj}
9193
id={`sectionAccordion-${id}`}
92-
initialIsOpen
94+
initialIsOpen={isAccordionExpanded}
9395
onToggle={setIsAccordionExpanded}
9496
buttonContent={
9597
<EuiFlexGroup alignItems="center" gutterSize="s">
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { i18n } from '@kbn/i18n';
11+
12+
export const OPEN_IN_DISCOVER_LABEL = i18n.translate(
13+
'unifiedDocViewer.observability.traces.openInDiscoverLinkLabel',
14+
{
15+
defaultMessage: 'Open in Discover',
16+
}
17+
);
18+
19+
export const OPEN_IN_DISCOVER_LABEL_ARIAL_LABEL = i18n.translate(
20+
'unifiedDocViewer.observability.traces.openInDiscoverArialLabel',
21+
{ defaultMessage: 'Open in discover link' }
22+
);
23+
24+
export const NOT_AVAILABLE_LABEL = i18n.translate('unifiedDocViewer.observability.traces.na', {
25+
defaultMessage: 'N/A',
26+
});
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,41 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import { getEsqlQuery } from './get_esql_query';
10+
import { createTraceContextWhereClause } from './create_trace_context_where_clause';
1111
import { from } from '@kbn/esql-composer';
1212

1313
const source = from('foo-*');
1414

15-
describe('getEsqlQuery', () => {
15+
describe('createTraceContextWhereClause', () => {
1616
it('returns a where AST node with only traceId', () => {
17-
const pipeline = source.pipe(getEsqlQuery({ traceId: 'abc123' }));
17+
const pipeline = source.pipe(createTraceContextWhereClause({ traceId: 'abc123' }));
1818
expect(pipeline.toString()).toEqual('FROM foo-*\n | WHERE trace.id == "abc123"');
1919
});
2020

2121
it('returns a pipeline with traceId and spanId', () => {
22-
const pipeline = source.pipe(getEsqlQuery({ traceId: 'abc123', spanId: 'span456' }));
22+
const pipeline = source.pipe(
23+
createTraceContextWhereClause({ traceId: 'abc123', spanId: 'span456' })
24+
);
2325
expect(pipeline.toString()).toEqual(
2426
'FROM foo-*\n | WHERE trace.id == "abc123" AND span.id == "span456"'
2527
);
2628
});
2729
it('returns a pipeline with traceId and transactionId', () => {
28-
const pipeline = source.pipe(getEsqlQuery({ traceId: 'abc123', transactionId: 'txn789' }));
30+
const pipeline = source.pipe(
31+
createTraceContextWhereClause({ traceId: 'abc123', transactionId: 'txn789' })
32+
);
2933
expect(pipeline.toString()).toEqual(
3034
'FROM foo-*\n | WHERE trace.id == "abc123" AND transaction.id == "txn789"'
3135
);
3236
});
3337

3438
it('returns a pipeline with all fields', () => {
3539
const pipeline = source.pipe(
36-
getEsqlQuery({ traceId: 'abc123', spanId: 'span456', transactionId: 'txn789' })
40+
createTraceContextWhereClause({
41+
traceId: 'abc123',
42+
spanId: 'span456',
43+
transactionId: 'txn789',
44+
})
3745
);
3846
expect(pipeline.toString()).toEqual(
3947
'FROM foo-*\n | WHERE trace.id == "abc123" AND transaction.id == "txn789" AND span.id == "span456"'
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import { SPAN_ID_FIELD, TRACE_ID_FIELD, TRANSACTION_ID_FIELD } from '@kbn/discover-utils';
1010
import { where } from '@kbn/esql-composer';
1111

12-
export const getEsqlQuery = ({
12+
export const createTraceContextWhereClause = ({
1313
traceId,
1414
spanId,
1515
transactionId,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
/*
11+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
12+
* or more contributor license agreements. Licensed under the "Elastic License
13+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
14+
* Public License v 1"; you may not use this file except in compliance with, at
15+
* your election, the "Elastic License 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
16+
* Public License v 1".
17+
*/
18+
19+
import type { EuiInMemoryTableProps } from '@elastic/eui';
20+
import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiSpacer, EuiText } from '@elastic/eui';
21+
import { i18n } from '@kbn/i18n';
22+
import React, { useMemo } from 'react';
23+
import { ContentFrameworkSection } from '../../../../content_framework/section';
24+
import { getColumns } from './get_columns';
25+
import { useFetchErrorsByTraceId } from './use_fetch_errors_by_trace_id';
26+
import { useDataSourcesContext } from '../../hooks/use_data_sources';
27+
import { useGetGenerateDiscoverLink } from '../../hooks/use_get_generate_discover_link';
28+
import { OPEN_IN_DISCOVER_LABEL, OPEN_IN_DISCOVER_LABEL_ARIAL_LABEL } from '../../common/constants';
29+
import { createTraceContextWhereClause } from '../../common/create_trace_context_where_clause';
30+
31+
export interface Props {
32+
traceId: string;
33+
docId?: string;
34+
}
35+
36+
const sorting: EuiInMemoryTableProps['sorting'] = {
37+
sort: { field: 'lastSeen', direction: 'desc' as const },
38+
};
39+
40+
export function ErrorsTable({ traceId, docId }: Props) {
41+
const { indexes } = useDataSourcesContext();
42+
const { generateDiscoverLink } = useGetGenerateDiscoverLink({ indexPattern: indexes.apm.errors });
43+
44+
const { loading, error, response } = useFetchErrorsByTraceId({
45+
traceId,
46+
docId,
47+
});
48+
49+
const { columns, openInDiscoverLink } = useMemo(() => {
50+
const cols = getColumns({ traceId, docId, generateDiscoverLink, source: response.source });
51+
52+
const link = generateDiscoverLink(createTraceContextWhereClause({ traceId, spanId: docId }));
53+
54+
return { columns: cols, openInDiscoverLink: link };
55+
}, [traceId, docId, generateDiscoverLink, response.source]);
56+
57+
if (loading || (!error && response.traceErrors.length === 0)) {
58+
return null;
59+
}
60+
61+
return (
62+
<ContentFrameworkSection
63+
data-test-subj="unifiedDocViewerErrorsAccordion"
64+
id="errorsSection"
65+
title={i18n.translate('unifiedDocViewer.observability.traces.docViewerSpanOverview.errors', {
66+
defaultMessage: 'Errors',
67+
})}
68+
description={i18n.translate(
69+
'unifiedDocViewer.observability.traces.docViewerSpanOverview.errors.description',
70+
{ defaultMessage: 'Errors that occurred during this span and their causes' }
71+
)}
72+
actions={
73+
openInDiscoverLink
74+
? [
75+
{
76+
icon: 'discoverApp',
77+
label: OPEN_IN_DISCOVER_LABEL,
78+
ariaLabel: OPEN_IN_DISCOVER_LABEL_ARIAL_LABEL,
79+
href: openInDiscoverLink,
80+
dataTestSubj: 'unifiedDocViewerSpanLinksRefreshButton',
81+
},
82+
]
83+
: undefined
84+
}
85+
>
86+
<EuiSpacer size="s" />
87+
{error ? (
88+
<EuiText color="subdued">
89+
{i18n.translate('unifiedDocViewer.observability.traces.docViewerSpanOverview.error', {
90+
defaultMessage: 'An error happened when trying to fetch data. Please try again',
91+
})}
92+
</EuiText>
93+
) : (
94+
<EuiFlexGroup direction="column" gutterSize="s">
95+
<EuiFlexItem>
96+
<EuiInMemoryTable
97+
responsiveBreakpoint={false}
98+
items={response.traceErrors}
99+
columns={columns}
100+
pagination={{ showPerPageOptions: false, pageSize: 5 }}
101+
sorting={sorting}
102+
compressed
103+
/>
104+
</EuiFlexItem>
105+
</EuiFlexGroup>
106+
)}
107+
</ContentFrameworkSection>
108+
);
109+
}

0 commit comments

Comments
 (0)