Skip to content

Commit 2486a71

Browse files
authored
[APM] Language-specific stacktrace formatting (#75924)
* [APM] Language-specific stacktrace formatting * Add todos * more * add at prefix for java * [APM] Fix overlapping transaction names ...in the table and the header. Did this by adding `word-break: break-all` to them. Also: * Rename List to TransactionList * Add stories for TransactionList and ApmHeader * Add missing type information to transactions based on sample transaction * Fixes #73960.
1 parent 4f23f0a commit 2486a71

22 files changed

+1515
-169
lines changed

x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.stories.tsx

Lines changed: 807 additions & 0 deletions
Large diffs are not rendered by default.

x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
1010
import { EuiAccordion, EuiTitle } from '@elastic/eui';
1111
import { px, unit } from '../../../style/variables';
1212
import { Stacktrace } from '.';
13-
import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
13+
import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
1414

1515
// @ts-ignore Styled Components has trouble inferring the types of the default props here.
1616
const Accordion = styled(EuiAccordion)`
@@ -55,7 +55,7 @@ interface CauseStacktraceProps {
5555
codeLanguage?: string;
5656
id: string;
5757
message?: string;
58-
stackframes?: IStackframe[];
58+
stackframes?: Stackframe[];
5959
}
6060

6161
export function CauseStacktrace({

x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { registerLanguage } from 'react-syntax-highlighter/dist/light';
2121
// @ts-ignore
2222
import { xcode } from 'react-syntax-highlighter/dist/styles';
2323
import styled from 'styled-components';
24-
import { IStackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe';
24+
import { StackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe';
2525
import { borderRadius, px, unit, units } from '../../../style/variables';
2626

2727
registerLanguage('javascript', javascript);
@@ -102,20 +102,20 @@ const Code = styled.code`
102102
z-index: 2;
103103
`;
104104

105-
function getStackframeLines(stackframe: IStackframeWithLineContext) {
105+
function getStackframeLines(stackframe: StackframeWithLineContext) {
106106
const line = stackframe.line.context;
107107
const preLines = stackframe.context?.pre || [];
108108
const postLines = stackframe.context?.post || [];
109109
return [...preLines, line, ...postLines];
110110
}
111111

112-
function getStartLineNumber(stackframe: IStackframeWithLineContext) {
112+
function getStartLineNumber(stackframe: StackframeWithLineContext) {
113113
const preLines = size(stackframe.context?.pre || []);
114114
return stackframe.line.number - preLines;
115115
}
116116

117117
interface Props {
118-
stackframe: IStackframeWithLineContext;
118+
stackframe: StackframeWithLineContext;
119119
codeLanguage?: string;
120120
isLibraryFrame: boolean;
121121
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
9+
import { renderWithTheme } from '../../../utils/testHelpers';
10+
import { FrameHeading } from './FrameHeading';
11+
12+
function getRenderedStackframeText(
13+
stackframe: Stackframe,
14+
codeLanguage: string
15+
) {
16+
const result = renderWithTheme(
17+
<FrameHeading
18+
codeLanguage={codeLanguage}
19+
isLibraryFrame={false}
20+
stackframe={stackframe}
21+
/>
22+
);
23+
24+
return result.getByTestId('FrameHeading').textContent;
25+
}
26+
27+
describe('FrameHeading', () => {
28+
describe('with a Go stackframe', () => {
29+
it('renders', () => {
30+
expect(
31+
getRenderedStackframeText(
32+
{
33+
exclude_from_grouping: false,
34+
filename: 'main.go',
35+
abs_path: '/src/opbeans-go/main.go',
36+
line: { number: 196 },
37+
function: 'Main.func2',
38+
module: 'main',
39+
},
40+
'go'
41+
)
42+
).toEqual('main.go in Main.func2 at line 196');
43+
});
44+
});
45+
46+
describe('with a Java stackframe', () => {
47+
it('renders', () => {
48+
expect(
49+
getRenderedStackframeText(
50+
{
51+
library_frame: true,
52+
exclude_from_grouping: false,
53+
filename: 'OutputBuffer.java',
54+
classname: 'org.apache.catalina.connector.OutputBuffer',
55+
line: { number: 825 },
56+
module: 'org.apache.catalina.connector',
57+
function: 'flushByteBuffer',
58+
},
59+
'Java'
60+
)
61+
).toEqual(
62+
'at org.apache.catalina.connector.OutputBuffer.flushByteBuffer(OutputBuffer.java:825)'
63+
);
64+
});
65+
});
66+
67+
describe('with a .NET stackframe', () => {
68+
describe('with a classname', () => {
69+
it('renders', () => {
70+
expect(
71+
getRenderedStackframeText(
72+
{
73+
classname: 'OpbeansDotnet.Controllers.CustomersController',
74+
exclude_from_grouping: false,
75+
filename:
76+
'/src/opbeans-dotnet/Controllers/CustomersController.cs',
77+
abs_path:
78+
'/src/opbeans-dotnet/Controllers/CustomersController.cs',
79+
line: { number: 23 },
80+
module:
81+
'opbeans-dotnet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null',
82+
function: 'Get',
83+
},
84+
'C#'
85+
)
86+
).toEqual(
87+
'OpbeansDotnet.Controllers.CustomersController in Get in /src/opbeans-dotnet/Controllers/CustomersController.cs at line 23'
88+
);
89+
});
90+
});
91+
92+
describe('with no classname', () => {
93+
it('renders', () => {
94+
expect(
95+
getRenderedStackframeText(
96+
{
97+
exclude_from_grouping: false,
98+
filename:
99+
'Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider+ResultEnumerable`1',
100+
line: { number: 0 },
101+
function: 'GetEnumerator',
102+
module:
103+
'Microsoft.EntityFrameworkCore, Version=2.2.6.0, Culture=neutral, PublicKeyToken=adb9793829ddae60',
104+
},
105+
'C#'
106+
)
107+
).toEqual(
108+
'Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider+ResultEnumerable`1 in GetEnumerator'
109+
);
110+
});
111+
});
112+
});
113+
114+
describe('with a Node stackframe', () => {
115+
it('renders', () => {
116+
expect(
117+
getRenderedStackframeText(
118+
{
119+
library_frame: true,
120+
exclude_from_grouping: false,
121+
filename: 'internal/async_hooks.js',
122+
abs_path: 'internal/async_hooks.js',
123+
line: { number: 120 },
124+
function: 'callbackTrampoline',
125+
},
126+
'javascript'
127+
)
128+
).toEqual('at callbackTrampoline (internal/async_hooks.js:120)');
129+
});
130+
131+
describe('with a classname', () => {
132+
it('renders', () => {
133+
expect(
134+
getRenderedStackframeText(
135+
{
136+
classname: 'TCPConnectWrap',
137+
exclude_from_grouping: false,
138+
library_frame: true,
139+
filename: 'internal/stream_base_commons.js',
140+
abs_path: 'internal/stream_base_commons.js',
141+
line: { number: 205 },
142+
function: 'onStreamRead',
143+
},
144+
'javascript'
145+
)
146+
).toEqual(
147+
'at TCPConnectWrap.onStreamRead (internal/stream_base_commons.js:205)'
148+
);
149+
});
150+
});
151+
152+
describe('with no classname and no function', () => {
153+
it('renders', () => {
154+
expect(
155+
getRenderedStackframeText(
156+
{
157+
exclude_from_grouping: false,
158+
library_frame: true,
159+
filename: 'internal/stream_base_commons.js',
160+
abs_path: 'internal/stream_base_commons.js',
161+
line: { number: 205 },
162+
},
163+
'javascript'
164+
)
165+
).toEqual('at (internal/stream_base_commons.js:205)');
166+
});
167+
});
168+
});
169+
170+
describe('with a Python stackframe', () => {
171+
it('renders', () => {
172+
expect(
173+
getRenderedStackframeText(
174+
{
175+
exclude_from_grouping: false,
176+
library_frame: false,
177+
filename: 'opbeans/views.py',
178+
abs_path: '/app/opbeans/views.py',
179+
line: {
180+
number: 190,
181+
context: ' return post_order(request)',
182+
},
183+
module: 'opbeans.views',
184+
function: 'orders',
185+
context: {
186+
pre: [
187+
' # set transaction name to post_order',
188+
" elasticapm.set_transaction_name('POST opbeans.views.post_order')",
189+
],
190+
post: [
191+
' order_list = list(m.Order.objects.values(',
192+
" 'id', 'customer_id', 'customer__full_name', 'created_at'",
193+
],
194+
},
195+
vars: { request: "<WSGIRequest: POST '/api/orders'>" },
196+
},
197+
'python'
198+
)
199+
).toEqual('opbeans/views.py in orders at line 190');
200+
});
201+
});
202+
203+
describe('with a Ruby stackframe', () => {
204+
it('renders', () => {
205+
expect(
206+
getRenderedStackframeText(
207+
{
208+
library_frame: false,
209+
exclude_from_grouping: false,
210+
abs_path: '/app/app/controllers/api/customers_controller.rb',
211+
filename: 'api/customers_controller.rb',
212+
line: {
213+
number: 15,
214+
context: ' render json: Customer.find(params[:id])\n',
215+
},
216+
function: 'show',
217+
context: {
218+
pre: ['\n', ' def show\n'],
219+
post: [' end\n', ' end\n'],
220+
},
221+
},
222+
'ruby'
223+
)
224+
).toEqual("api/customers_controller.rb:15 in `show'");
225+
});
226+
});
227+
228+
describe('with a RUM stackframe', () => {
229+
it('renders', () => {
230+
expect(
231+
getRenderedStackframeText(
232+
{
233+
library_frame: false,
234+
exclude_from_grouping: false,
235+
filename: 'static/js/main.616809fb.js',
236+
abs_path: 'http://opbeans-frontend:3000/static/js/main.616809fb.js',
237+
sourcemap: {
238+
error:
239+
'No Sourcemap available for ServiceName opbeans-rum, ServiceVersion 2020-08-25 02:09:37, Path http://opbeans-frontend:3000/static/js/main.616809fb.js.',
240+
updated: false,
241+
},
242+
line: { number: 319, column: 3842 },
243+
function: 'unstable_runWithPriority',
244+
},
245+
'javascript'
246+
)
247+
).toEqual(
248+
'at unstable_runWithPriority (static/js/main.616809fb.js:319:3842)'
249+
);
250+
});
251+
});
252+
});

x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,23 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import React, { Fragment } from 'react';
7+
import React, { ComponentType } from 'react';
88
import styled from 'styled-components';
9-
import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
9+
import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
1010
import { fontFamilyCode, fontSize, px, units } from '../../../style/variables';
11+
import {
12+
CSharpFrameHeadingRenderer,
13+
DefaultFrameHeadingRenderer,
14+
FrameHeadingRendererProps,
15+
JavaFrameHeadingRenderer,
16+
JavaScriptFrameHeadingRenderer,
17+
RubyFrameHeadingRenderer,
18+
} from './frame_heading_renderers';
1119

1220
const FileDetails = styled.div`
1321
color: ${({ theme }) => theme.eui.euiColorDarkShade};
14-
padding: ${px(units.half)} 0;
22+
line-height: 1.5; /* matches the line-hight of the accordion container button */
23+
padding: ${px(units.eighth)} 0;
1524
font-family: ${fontFamilyCode};
1625
font-size: ${fontSize};
1726
`;
@@ -25,29 +34,37 @@ const AppFrameFileDetail = styled.span`
2534
`;
2635

2736
interface Props {
28-
stackframe: IStackframe;
37+
codeLanguage?: string;
38+
stackframe: Stackframe;
2939
isLibraryFrame: boolean;
3040
}
3141

32-
function FrameHeading({ stackframe, isLibraryFrame }: Props) {
33-
const FileDetail = isLibraryFrame
42+
function FrameHeading({ codeLanguage, stackframe, isLibraryFrame }: Props) {
43+
const FileDetail: ComponentType = isLibraryFrame
3444
? LibraryFrameFileDetail
3545
: AppFrameFileDetail;
36-
const lineNumber = stackframe.line?.number ?? 0;
37-
38-
const name =
39-
'filename' in stackframe ? stackframe.filename : stackframe.classname;
46+
let Renderer: ComponentType<FrameHeadingRendererProps>;
47+
switch (codeLanguage?.toString().toLowerCase()) {
48+
case 'c#':
49+
Renderer = CSharpFrameHeadingRenderer;
50+
break;
51+
case 'java':
52+
Renderer = JavaFrameHeadingRenderer;
53+
break;
54+
case 'javascript':
55+
Renderer = JavaScriptFrameHeadingRenderer;
56+
break;
57+
case 'ruby':
58+
Renderer = RubyFrameHeadingRenderer;
59+
break;
60+
default:
61+
Renderer = DefaultFrameHeadingRenderer;
62+
break;
63+
}
4064

4165
return (
42-
<FileDetails>
43-
<FileDetail>{name}</FileDetail> in{' '}
44-
<FileDetail>{stackframe.function}</FileDetail>
45-
{lineNumber > 0 && (
46-
<Fragment>
47-
{' at '}
48-
<FileDetail>line {lineNumber}</FileDetail>
49-
</Fragment>
50-
)}
66+
<FileDetails data-test-subj="FrameHeading">
67+
<Renderer fileDetailComponent={FileDetail} stackframe={stackframe} />
5168
</FileDetails>
5269
);
5370
}

0 commit comments

Comments
 (0)