diff --git a/packages/kbn-apm-synthtrace-client/src/lib/apm/apm_fields.ts b/packages/kbn-apm-synthtrace-client/src/lib/apm/apm_fields.ts index 7c3ab2c2b1e3a6..3ee43dc63f04f0 100644 --- a/packages/kbn-apm-synthtrace-client/src/lib/apm/apm_fields.ts +++ b/packages/kbn-apm-synthtrace-client/src/lib/apm/apm_fields.ts @@ -50,6 +50,35 @@ export interface GeoLocation { type: string; } +export interface APMStacktrace { + abs_path?: string; + classname?: string; + context?: { + post?: string[]; + pre?: string[]; + }; + exclude_from_grouping?: boolean; + filename?: string; + function?: string; + module?: string; + library_frame?: boolean; + line?: + | { + column?: number; + number: number; + } + | { + context?: string; + }; + sourcemap?: { + error?: string; + updated?: boolean; + }; + vars?: { + [key: string]: unknown; + }; +} + type ExperimentalFields = Partial<{ 'metricset.interval': string; 'transaction.duration.summary': string; @@ -80,6 +109,8 @@ export type ApmFields = Fields<{ 'cloud.provider': string; 'cloud.region': string; 'cloud.service.name': string; + // otel + 'code.stacktrace': string; 'container.id': string; 'destination.address': string; 'destination.port': number; @@ -169,6 +200,7 @@ export type ApmFields = Fields<{ 'span.duration.us': number; 'span.id': string; 'span.name': string; + 'span.stacktrace': APMStacktrace[]; 'span.self_time.count': number; 'span.self_time.sum.us': number; 'span.subtype': string; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/generate_span_stacktrace_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/generate_span_stacktrace_data.ts new file mode 100644 index 00000000000000..85eeb2cd456efb --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/generate_span_stacktrace_data.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import { synthtrace } from '../../../synthtrace'; + +function getAPMGeneratedStacktrace() { + const apmGeneratedStacktrace = apm + .service({ + name: 'apm-generated', + environment: 'production', + agentName: 'java', + }) + .instance('instance a'); + + return Array.from( + timerange( + new Date('2022-01-01T00:00:00.000Z'), + new Date('2022-01-01T00:01:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return apmGeneratedStacktrace + .transaction({ transactionName: `Transaction A` }) + .defaults({ + 'service.language.name': 'java', + }) + .timestamp(timestamp) + .duration(1000) + .success() + .children( + apmGeneratedStacktrace + .span({ + spanName: `Span A`, + spanType: 'internal', + 'span.stacktrace': [ + { + library_frame: false, + exclude_from_grouping: false, + filename: 'OutputBuffer.java', + classname: 'org.apache.catalina.connector.OutputBuffer', + line: { number: 825 }, + module: 'org.apache.catalina.connector', + function: 'flushByteBuffer', + }, + ], + }) + .timestamp(timestamp + 50) + .duration(100) + .failure() + ); + }) + ); +} + +function getOtelGeneratedStacktrace() { + const apmGeneratedStacktrace = apm + .service({ + name: 'otel-generated', + environment: 'production', + agentName: 'java', + }) + .instance('instance a'); + + return Array.from( + timerange( + new Date('2022-01-01T00:00:00.000Z'), + new Date('2022-01-01T00:01:00.000Z') + ) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return apmGeneratedStacktrace + .transaction({ transactionName: `Transaction A` }) + .timestamp(timestamp) + .duration(1000) + .defaults({ + 'service.language.name': 'java', + }) + .success() + .children( + apmGeneratedStacktrace + .span({ + spanName: `Span A`, + spanType: 'internal', + 'code.stacktrace': + 'java.lang.Throwable\n\tat co.elastic.otel.ElasticSpanProcessor.captureStackTrace(ElasticSpanProcessor.java:81)', + }) + .timestamp(timestamp + 50) + .duration(100) + .failure() + ); + }) + ); +} + +export function generateSpanStacktraceData() { + const apmGeneratedStacktrace = getAPMGeneratedStacktrace(); + const otelGeneratedStacktrace = getOtelGeneratedStacktrace(); + + synthtrace.index([...apmGeneratedStacktrace, ...otelGeneratedStacktrace]); +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/span_stacktrace.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/span_stacktrace.cy.ts new file mode 100644 index 00000000000000..dbc4a1072ffa01 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/transaction_details/span_stacktrace.cy.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import { synthtrace } from '../../../synthtrace'; +import { generateSpanStacktraceData } from './generate_span_stacktrace_data'; + +const start = '2022-01-01T00:00:00.000Z'; +const end = '2022-01-01T00:15:00.000Z'; + +function getServiceInventoryUrl({ serviceName }: { serviceName: string }) { + return url.format({ + pathname: `/app/apm/services/${serviceName}`, + query: { + rangeFrom: start, + rangeTo: end, + environment: 'ENVIRONMENT_ALL', + kuery: '', + serviceGroup: '', + transactionType: 'request', + comparisonEnabled: true, + offset: '1d', + }, + }); +} + +describe('Span stacktrace', () => { + beforeEach(() => { + cy.loginAsViewerUser(); + }); + describe('span flyout', () => { + before(() => { + generateSpanStacktraceData(); + }); + + after(() => { + synthtrace.clean(); + }); + it('Shows APM agent generated stacktrace', () => { + cy.visitKibana(getServiceInventoryUrl({ serviceName: 'apm-generated' })); + cy.contains('Transaction A').click(); + cy.contains('Span A').click(); + cy.getByTestSubj('spanStacktraceTab').click(); + cy.contains( + 'at org.apache.catalina.connector.OutputBuffer.flushByteBuffer(OutputBuffer.java:825)' + ); + }); + + it('Shows Otel generated stacktrace', () => { + cy.visitKibana(getServiceInventoryUrl({ serviceName: 'otel-generated' })); + cy.contains('Transaction A').click(); + cy.contains('Span A').click(); + cy.getByTestSubj('spanStacktraceTab').click(); + cy.contains( + `java.lang.Throwable at co.elastic.otel.ElasticSpanProcessor.captureStackTrace(ElasticSpanProcessor.java:81)` + ); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx index 59b06577b2757d..f32bd7be896d21 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx @@ -25,6 +25,7 @@ import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { isEmpty } from 'lodash'; import React, { Fragment } from 'react'; +import { PlaintextStacktrace } from '../../../../../error_group_details/error_sampler/plaintext_stacktrace'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { useFetcher, isPending } from '../../../../../../../hooks/use_fetcher'; @@ -204,6 +205,7 @@ function SpanFlyoutBody({ flyoutDetailTab?: string; }) { const stackframes = span.span.stacktrace; + const plaintextStacktrace = span.code?.stacktrace; const codeLanguage = parentTransaction?.service.language?.name; const spanDb = span.span.db; const spanTypes = getSpanTypes(span); @@ -232,10 +234,11 @@ function SpanFlyoutBody({ ), }, - ...(!isEmpty(stackframes) + ...(!isEmpty(stackframes) || !isEmpty(plaintextStacktrace) ? [ { id: 'stack-trace', + 'data-test-subj': 'spanStacktraceTab', name: i18n.translate( 'xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel', { @@ -245,10 +248,17 @@ function SpanFlyoutBody({ content: ( - + {stackframes ? ( + + ) : ( + + )} ), }, diff --git a/x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx b/x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx index 4a3646dbb78c1c..379dd5700406ee 100644 --- a/x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/stacktrace/index.tsx @@ -17,6 +17,7 @@ import { Stackframe as StackframeComponent } from './stackframe'; interface Props { stackframes?: Stackframe[]; codeLanguage?: string; + stackTrace?: string; } export function Stacktrace({ stackframes = [], codeLanguage }: Props) { diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index 1cac68f74b8b7b..301a4c96dfa358 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -71,6 +71,9 @@ export interface SpanRaw extends APMBaseDoc { id: string; }; child?: { id: string[] }; + code?: { + stacktrace?: string; + }; http?: Http; url?: Url; }