Skip to content

Commit

Permalink
[APM] Language-specific stacktrace formatting (#75924)
Browse files Browse the repository at this point in the history
* [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.
  • Loading branch information
smith authored Aug 31, 2020
1 parent 4f23f0a commit 2486a71
Show file tree
Hide file tree
Showing 22 changed files with 1,515 additions and 169 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import { EuiAccordion, EuiTitle } from '@elastic/eui';
import { px, unit } from '../../../style/variables';
import { Stacktrace } from '.';
import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';

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

export function CauseStacktrace({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { registerLanguage } from 'react-syntax-highlighter/dist/light';
// @ts-ignore
import { xcode } from 'react-syntax-highlighter/dist/styles';
import styled from 'styled-components';
import { IStackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { StackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { borderRadius, px, unit, units } from '../../../style/variables';

registerLanguage('javascript', javascript);
Expand Down Expand Up @@ -102,20 +102,20 @@ const Code = styled.code`
z-index: 2;
`;

function getStackframeLines(stackframe: IStackframeWithLineContext) {
function getStackframeLines(stackframe: StackframeWithLineContext) {
const line = stackframe.line.context;
const preLines = stackframe.context?.pre || [];
const postLines = stackframe.context?.post || [];
return [...preLines, line, ...postLines];
}

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

interface Props {
stackframe: IStackframeWithLineContext;
stackframe: StackframeWithLineContext;
codeLanguage?: string;
isLibraryFrame: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { renderWithTheme } from '../../../utils/testHelpers';
import { FrameHeading } from './FrameHeading';

function getRenderedStackframeText(
stackframe: Stackframe,
codeLanguage: string
) {
const result = renderWithTheme(
<FrameHeading
codeLanguage={codeLanguage}
isLibraryFrame={false}
stackframe={stackframe}
/>
);

return result.getByTestId('FrameHeading').textContent;
}

describe('FrameHeading', () => {
describe('with a Go stackframe', () => {
it('renders', () => {
expect(
getRenderedStackframeText(
{
exclude_from_grouping: false,
filename: 'main.go',
abs_path: '/src/opbeans-go/main.go',
line: { number: 196 },
function: 'Main.func2',
module: 'main',
},
'go'
)
).toEqual('main.go in Main.func2 at line 196');
});
});

describe('with a Java stackframe', () => {
it('renders', () => {
expect(
getRenderedStackframeText(
{
library_frame: true,
exclude_from_grouping: false,
filename: 'OutputBuffer.java',
classname: 'org.apache.catalina.connector.OutputBuffer',
line: { number: 825 },
module: 'org.apache.catalina.connector',
function: 'flushByteBuffer',
},
'Java'
)
).toEqual(
'at org.apache.catalina.connector.OutputBuffer.flushByteBuffer(OutputBuffer.java:825)'
);
});
});

describe('with a .NET stackframe', () => {
describe('with a classname', () => {
it('renders', () => {
expect(
getRenderedStackframeText(
{
classname: 'OpbeansDotnet.Controllers.CustomersController',
exclude_from_grouping: false,
filename:
'/src/opbeans-dotnet/Controllers/CustomersController.cs',
abs_path:
'/src/opbeans-dotnet/Controllers/CustomersController.cs',
line: { number: 23 },
module:
'opbeans-dotnet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null',
function: 'Get',
},
'C#'
)
).toEqual(
'OpbeansDotnet.Controllers.CustomersController in Get in /src/opbeans-dotnet/Controllers/CustomersController.cs at line 23'
);
});
});

describe('with no classname', () => {
it('renders', () => {
expect(
getRenderedStackframeText(
{
exclude_from_grouping: false,
filename:
'Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider+ResultEnumerable`1',
line: { number: 0 },
function: 'GetEnumerator',
module:
'Microsoft.EntityFrameworkCore, Version=2.2.6.0, Culture=neutral, PublicKeyToken=adb9793829ddae60',
},
'C#'
)
).toEqual(
'Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider+ResultEnumerable`1 in GetEnumerator'
);
});
});
});

describe('with a Node stackframe', () => {
it('renders', () => {
expect(
getRenderedStackframeText(
{
library_frame: true,
exclude_from_grouping: false,
filename: 'internal/async_hooks.js',
abs_path: 'internal/async_hooks.js',
line: { number: 120 },
function: 'callbackTrampoline',
},
'javascript'
)
).toEqual('at callbackTrampoline (internal/async_hooks.js:120)');
});

describe('with a classname', () => {
it('renders', () => {
expect(
getRenderedStackframeText(
{
classname: 'TCPConnectWrap',
exclude_from_grouping: false,
library_frame: true,
filename: 'internal/stream_base_commons.js',
abs_path: 'internal/stream_base_commons.js',
line: { number: 205 },
function: 'onStreamRead',
},
'javascript'
)
).toEqual(
'at TCPConnectWrap.onStreamRead (internal/stream_base_commons.js:205)'
);
});
});

describe('with no classname and no function', () => {
it('renders', () => {
expect(
getRenderedStackframeText(
{
exclude_from_grouping: false,
library_frame: true,
filename: 'internal/stream_base_commons.js',
abs_path: 'internal/stream_base_commons.js',
line: { number: 205 },
},
'javascript'
)
).toEqual('at (internal/stream_base_commons.js:205)');
});
});
});

describe('with a Python stackframe', () => {
it('renders', () => {
expect(
getRenderedStackframeText(
{
exclude_from_grouping: false,
library_frame: false,
filename: 'opbeans/views.py',
abs_path: '/app/opbeans/views.py',
line: {
number: 190,
context: ' return post_order(request)',
},
module: 'opbeans.views',
function: 'orders',
context: {
pre: [
' # set transaction name to post_order',
" elasticapm.set_transaction_name('POST opbeans.views.post_order')",
],
post: [
' order_list = list(m.Order.objects.values(',
" 'id', 'customer_id', 'customer__full_name', 'created_at'",
],
},
vars: { request: "<WSGIRequest: POST '/api/orders'>" },
},
'python'
)
).toEqual('opbeans/views.py in orders at line 190');
});
});

describe('with a Ruby stackframe', () => {
it('renders', () => {
expect(
getRenderedStackframeText(
{
library_frame: false,
exclude_from_grouping: false,
abs_path: '/app/app/controllers/api/customers_controller.rb',
filename: 'api/customers_controller.rb',
line: {
number: 15,
context: ' render json: Customer.find(params[:id])\n',
},
function: 'show',
context: {
pre: ['\n', ' def show\n'],
post: [' end\n', ' end\n'],
},
},
'ruby'
)
).toEqual("api/customers_controller.rb:15 in `show'");
});
});

describe('with a RUM stackframe', () => {
it('renders', () => {
expect(
getRenderedStackframeText(
{
library_frame: false,
exclude_from_grouping: false,
filename: 'static/js/main.616809fb.js',
abs_path: 'http://opbeans-frontend:3000/static/js/main.616809fb.js',
sourcemap: {
error:
'No Sourcemap available for ServiceName opbeans-rum, ServiceVersion 2020-08-25 02:09:37, Path http://opbeans-frontend:3000/static/js/main.616809fb.js.',
updated: false,
},
line: { number: 319, column: 3842 },
function: 'unstable_runWithPriority',
},
'javascript'
)
).toEqual(
'at unstable_runWithPriority (static/js/main.616809fb.js:319:3842)'
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { Fragment } from 'react';
import React, { ComponentType } from 'react';
import styled from 'styled-components';
import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { fontFamilyCode, fontSize, px, units } from '../../../style/variables';
import {
CSharpFrameHeadingRenderer,
DefaultFrameHeadingRenderer,
FrameHeadingRendererProps,
JavaFrameHeadingRenderer,
JavaScriptFrameHeadingRenderer,
RubyFrameHeadingRenderer,
} from './frame_heading_renderers';

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

interface Props {
stackframe: IStackframe;
codeLanguage?: string;
stackframe: Stackframe;
isLibraryFrame: boolean;
}

function FrameHeading({ stackframe, isLibraryFrame }: Props) {
const FileDetail = isLibraryFrame
function FrameHeading({ codeLanguage, stackframe, isLibraryFrame }: Props) {
const FileDetail: ComponentType = isLibraryFrame
? LibraryFrameFileDetail
: AppFrameFileDetail;
const lineNumber = stackframe.line?.number ?? 0;

const name =
'filename' in stackframe ? stackframe.filename : stackframe.classname;
let Renderer: ComponentType<FrameHeadingRendererProps>;
switch (codeLanguage?.toString().toLowerCase()) {
case 'c#':
Renderer = CSharpFrameHeadingRenderer;
break;
case 'java':
Renderer = JavaFrameHeadingRenderer;
break;
case 'javascript':
Renderer = JavaScriptFrameHeadingRenderer;
break;
case 'ruby':
Renderer = RubyFrameHeadingRenderer;
break;
default:
Renderer = DefaultFrameHeadingRenderer;
break;
}

return (
<FileDetails>
<FileDetail>{name}</FileDetail> in{' '}
<FileDetail>{stackframe.function}</FileDetail>
{lineNumber > 0 && (
<Fragment>
{' at '}
<FileDetail>line {lineNumber}</FileDetail>
</Fragment>
)}
<FileDetails data-test-subj="FrameHeading">
<Renderer fileDetailComponent={FileDetail} stackframe={stackframe} />
</FileDetails>
);
}
Expand Down
Loading

0 comments on commit 2486a71

Please sign in to comment.