[One Discover] Display stacktrace in the logs overview tab (#204521)

## 📓 Summary
Adds a new section to the overview tab in the log details flyout in
Discover to display stacktrace information for logs and exceptions.

In a follow-up, the stacktrace could be moved to a new tab in the log
details flyout and actions can be added to the stacktrace (and quality)
icons in the document table to open the relevant sections in the flyout.

Closes https://github.com/elastic/kibana/issues/190460

### APM - Log stacktrace (library frames)
<img width="1470" alt="image"
src="https://github.com/user-attachments/assets/8991f882-d329-4bc5-aa37-424576bcee72"
/>

### APM - Exception (with cause)
<img width="1476" alt="image"
src="https://github.com/user-attachments/assets/cfbf24a7-6f82-48f1-b275-5aac977411ac"
/>

### APM - Exception (simple stacktrace)
<img width="1474" alt="image"
src="https://github.com/user-attachments/assets/fc0306c4-5fcd-4b74-bb0d-c1784a48d677"
/>

### Apache Tomcat Integration (Catalina) - Stacktrace
<img width="1472" alt="image"
src="https://github.com/user-attachments/assets/281f1822-faea-4e2d-9515-c11a9ee12f50"
/>

## 📝 Notes for reviewers
- The `@kbn/apm-types` package was marked as platform / shared as it's
being used by the
[unified_doc_viewer](https://github.com/elastic/kibana/blob/main/src/plugins/unified_doc_viewer/kibana.jsonc)
- The code used to render stacktraces in APM was moved into a new
`@kbn/event-stacktrace` package as it is reused in the
`unified_doc_viewer`
- The code used to render metadata table in APM was moved into a new
`@kbn/key-value-metadata-table` package

## 🧪 Testing instructions
The deployed environments have sample logs that can be used (time range:
Jan 1, 2025 - now). For a local setup, please follow the instructions
below:

1. Ingest sample logs with stacktraces
([gist](https://gist.github.com/gbamparop/0da21ca7f65b24c4a9c071ce9e9b97b0)).
Please note that these are test data and some fields that are not used
by stacktraces might not be consistent
2. View relevant logs in Discover (Query: `service.name: "synth-node-0"
OR apache_tomcat :*`, Time range: Jan 1, 2025 - now)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Giorgos Bamparopoulos 2025-01-22 18:06:14 +02:00 committed by GitHub
parent cf4d79c862
commit 368475e8e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
116 changed files with 772 additions and 111 deletions

4
.github/CODEOWNERS vendored
View file

@ -801,12 +801,15 @@ x-pack/platform/packages/shared/index-lifecycle-management/index_lifecycle_manag
x-pack/platform/packages/shared/index-management/index_management_shared_types @elastic/kibana-management
x-pack/platform/packages/shared/kbn-ai-assistant @elastic/search-kibana
x-pack/platform/packages/shared/kbn-alerting-comparators @elastic/response-ops
x-pack/platform/packages/shared/kbn-apm-types @elastic/obs-ux-infra_services-team
x-pack/platform/packages/shared/kbn-cloud-security-posture/common @elastic/kibana-cloud-security-posture
x-pack/platform/packages/shared/kbn-data-forge @elastic/obs-ux-management-team
x-pack/platform/packages/shared/kbn-elastic-assistant @elastic/security-generative-ai
x-pack/platform/packages/shared/kbn-elastic-assistant-common @elastic/security-generative-ai
x-pack/platform/packages/shared/kbn-entities-schema @elastic/obs-entities
x-pack/platform/packages/shared/kbn-event-stacktrace @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team
x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common @elastic/response-ops @elastic/appex-ai-infra @elastic/obs-ai-assistant @elastic/security-generative-ai
x-pack/platform/packages/shared/kbn-key-value-metadata-table @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team
x-pack/platform/packages/shared/kbn-langchain @elastic/security-generative-ai
x-pack/platform/packages/shared/kbn-slo-schema @elastic/obs-ux-management-team
x-pack/platform/packages/shared/ml/aiops_common @elastic/ml-ui
@ -907,7 +910,6 @@ x-pack/solutions/observability/packages/alert_details @elastic/obs-ux-management
x-pack/solutions/observability/packages/alerting_test_data @elastic/obs-ux-management-team
x-pack/solutions/observability/packages/get_padded_alert_time_range_util @elastic/obs-ux-management-team
x-pack/solutions/observability/packages/kbn-alerts-grouping @elastic/response-ops
x-pack/solutions/observability/packages/kbn-apm-types @elastic/obs-ux-infra_services-team
x-pack/solutions/observability/packages/kbn-custom-integrations @elastic/obs-ux-logs-team
x-pack/solutions/observability/packages/kbn-investigation-shared @elastic/obs-ux-management-team
x-pack/solutions/observability/packages/kbn-streams-schema @elastic/streams-program-team

View file

@ -188,7 +188,7 @@
"@kbn/apm-data-access-plugin": "link:x-pack/solutions/observability/plugins/apm_data_access",
"@kbn/apm-data-view": "link:src/platform/packages/shared/kbn-apm-data-view",
"@kbn/apm-plugin": "link:x-pack/solutions/observability/plugins/apm",
"@kbn/apm-types": "link:x-pack/solutions/observability/packages/kbn-apm-types",
"@kbn/apm-types": "link:x-pack/platform/packages/shared/kbn-apm-types",
"@kbn/apm-utils": "link:src/platform/packages/shared/kbn-apm-utils",
"@kbn/app-link-test-plugin": "link:test/plugin_functional/plugins/app_link_test",
"@kbn/application-usage-test-plugin": "link:x-pack/test/usage_collection/plugins/application_usage_test",
@ -499,6 +499,7 @@
"@kbn/event-annotation-plugin": "link:src/platform/plugins/private/event_annotation",
"@kbn/event-log-fixture-plugin": "link:x-pack/test/plugin_api_integration/plugins/event_log",
"@kbn/event-log-plugin": "link:x-pack/platform/plugins/shared/event_log",
"@kbn/event-stacktrace": "link:x-pack/platform/packages/shared/kbn-event-stacktrace",
"@kbn/expandable-flyout": "link:x-pack/solutions/security/packages/expandable-flyout",
"@kbn/exploratory-view-example-plugin": "link:x-pack/examples/exploratory_view_example",
"@kbn/exploratory-view-plugin": "link:x-pack/solutions/observability/plugins/exploratory_view",
@ -596,6 +597,7 @@
"@kbn/kbn-top-nav-plugin": "link:test/plugin_functional/plugins/kbn_top_nav",
"@kbn/kbn-tp-custom-visualizations-plugin": "link:test/plugin_functional/plugins/kbn_tp_custom_visualizations",
"@kbn/kbn-tp-run-pipeline-plugin": "link:test/interpreter_functional/plugins/kbn_tp_run_pipeline",
"@kbn/key-value-metadata-table": "link:x-pack/platform/packages/shared/kbn-key-value-metadata-table",
"@kbn/kibana-cors-test-plugin": "link:x-pack/test/functional_cors/plugins/kibana_cors_test",
"@kbn/kibana-overview-plugin": "link:src/platform/plugins/private/kibana_overview",
"@kbn/kibana-react-plugin": "link:src/platform/plugins/shared/kibana_react",

View file

@ -58,8 +58,8 @@ export type LogDocument = Fields &
'cloud.project.id'?: string;
'cloud.instance.id'?: string;
'error.stack_trace'?: string;
'error.exception.stacktrace'?: string;
'error.log.stacktrace'?: string;
'error.exception'?: unknown;
'error.log'?: unknown;
'log.custom': Record<string, unknown>;
'host.geo.location': number[];
'host.ip': string;

View file

@ -144,7 +144,7 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
.defaults({
...commonLongEntryFields,
'error.message': message,
'error.exception.stacktrace': 'Error message in error.exception.stacktrace',
'error.stack_trace': 'Stacktrace',
})
.timestamp(timestamp);
});
@ -174,7 +174,7 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
.defaults({
...commonLongEntryFields,
'event.original': message,
'error.log.stacktrace': 'Error message in error.log.stacktrace',
'error.stack_trace': 'Stacktrace',
'event.start': eventDate,
'event.end': moment(eventDate).add(1, 'm').toDate(),
})
@ -203,7 +203,7 @@ const scenario: Scenario<LogDocument> = async (runOptions) => {
.setHostIp(getIpAddress())
.defaults({
...commonLongEntryFields,
'error.stack_trace': 'Error message in error.stack_trace',
'error.stack_trace': 'Stacktrace',
})
.timestamp(timestamp);
});

View file

@ -15,6 +15,7 @@ import {
generateLongId,
generateShortId,
Instance,
LogDocument,
} from '@kbn/apm-synthtrace-client';
import { Scenario } from '../cli/scenario';
import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment';
@ -26,7 +27,7 @@ const ENVIRONMENT = getSynthtraceEnvironment(__filename);
const alwaysSpikeTransactionName = 'GET /always-spike';
const sometimesSpikeTransactionName = 'GET /sometimes-spike';
const scenario: Scenario<ApmFields> = async ({ logger, ...runOptions }) => {
const scenario: Scenario<LogDocument | ApmFields> = async ({ logger, ...runOptions }) => {
const { isLogsDb } = parseLogsScenarioOpts(runOptions.scenarioOpts);
return {

View file

@ -77,4 +77,5 @@ export const storybookAliases = {
ui_actions_enhanced: 'src/platform/plugins/shared/ui_actions_enhanced/.storybook',
unified_search: 'src/platform/plugins/shared/unified_search/.storybook',
profiling: 'x-pack/solutions/observability/plugins/profiling/.storybook',
event_stacktrace: 'x-pack/platform/packages/shared/kbn-event-stacktrace/.storybook',
};

View file

@ -37,8 +37,8 @@ export interface LogDocument extends DataTableRecord {
'data_stream.dataset': string;
'error.stack_trace'?: string;
'error.exception.stacktrace'?: string;
'error.log.stacktrace'?: string;
'error.exception.stacktrace.abs_path'?: string;
'error.log.stacktrace.abs_path'?: string;
};
}
@ -83,8 +83,8 @@ export interface ResourceFields {
export interface StackTraceFields {
'error.stack_trace'?: string;
'error.exception.stacktrace'?: string;
'error.log.stacktrace'?: string;
'error.exception.stacktrace.abs_path'?: string;
'error.log.stacktrace.abs_path'?: string;
}
export interface SmartFieldGridColumnOptions {

View file

@ -19,6 +19,7 @@ export const TRACE_ID_FIELD = 'trace.id';
export const LOG_FILE_PATH_FIELD = 'log.file.path';
export const DATASTREAM_NAMESPACE_FIELD = 'data_stream.namespace';
export const DATASTREAM_DATASET_FIELD = 'data_stream.dataset';
export const DATASTREAM_TYPE_FIELD = 'data_stream.type';
// Resource Fields
export const AGENT_NAME_FIELD = 'agent.name';
@ -41,5 +42,5 @@ export const DEGRADED_DOCS_FIELDS = [IGNORED_FIELD, IGNORED_FIELD_VALUES_FIELD]
// Error Stacktrace
export const ERROR_STACK_TRACE = 'error.stack_trace';
export const ERROR_EXCEPTION_STACKTRACE = 'error.exception.stacktrace';
export const ERROR_LOG_STACKTRACE = 'error.log.stacktrace';
export const ERROR_EXCEPTION_STACKTRACE_ABS_PATH = 'error.exception.stacktrace.abs_path';
export const ERROR_LOG_STACKTRACE_ABS_PATH = 'error.log.stacktrace.abs_path';

View file

@ -9,19 +9,19 @@
import { getFieldValue, LogDocument, StackTraceFields } from '..';
import {
ERROR_EXCEPTION_STACKTRACE,
ERROR_LOG_STACKTRACE,
ERROR_EXCEPTION_STACKTRACE_ABS_PATH,
ERROR_LOG_STACKTRACE_ABS_PATH,
ERROR_STACK_TRACE,
} from '../field_constants';
export const getStacktraceFields = (doc: LogDocument): StackTraceFields => {
const errorStackTrace = getFieldValue(doc, ERROR_STACK_TRACE);
const errorExceptionStackTrace = getFieldValue(doc, ERROR_EXCEPTION_STACKTRACE);
const errorLogStackTrace = getFieldValue(doc, ERROR_LOG_STACKTRACE);
const errorExceptionStackTrace = getFieldValue(doc, ERROR_EXCEPTION_STACKTRACE_ABS_PATH);
const errorLogStackTrace = getFieldValue(doc, ERROR_LOG_STACKTRACE_ABS_PATH);
return {
[ERROR_STACK_TRACE]: errorStackTrace,
[ERROR_EXCEPTION_STACKTRACE]: errorExceptionStackTrace,
[ERROR_LOG_STACKTRACE]: errorLogStackTrace,
[ERROR_EXCEPTION_STACKTRACE_ABS_PATH]: errorExceptionStackTrace,
[ERROR_LOG_STACKTRACE_ABS_PATH]: errorLogStackTrace,
};
};

View file

@ -23,4 +23,4 @@
"kibanaUtils"
]
}
}
}

View file

@ -12,11 +12,13 @@ import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
import { getLogDocumentOverview } from '@kbn/discover-utils';
import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import { ObservabilityLogsAIAssistantFeatureRenderDeps } from '@kbn/discover-shared-plugin/public';
import { getStacktraceFields, LogDocument } from '@kbn/discover-utils/src';
import { LogsOverviewHeader } from './logs_overview_header';
import { LogsOverviewHighlights } from './logs_overview_highlights';
import { FieldActionsProvider } from '../../hooks/use_field_actions';
import { getUnifiedDocViewerServices } from '../../plugin';
import { LogsOverviewDegradedFields } from './logs_overview_degraded_fields';
import { LogsOverviewStacktraceSection } from './logs_overview_stacktrace_section';
export type LogsOverviewProps = DocViewRenderProps & {
renderAIAssistant?: (deps: ObservabilityLogsAIAssistantFeatureRenderDeps) => JSX.Element;
@ -34,6 +36,8 @@ export function LogsOverview({
const { fieldFormats } = getUnifiedDocViewerServices();
const parsedDoc = getLogDocumentOverview(hit, { dataView, fieldFormats });
const LogsOverviewAIAssistant = renderAIAssistant;
const stacktraceFields = getStacktraceFields(hit as LogDocument);
const isStacktraceAvailable = Object.values(stacktraceFields).some(Boolean);
return (
<FieldActionsProvider
@ -47,6 +51,7 @@ export function LogsOverview({
<EuiHorizontalRule margin="xs" />
<LogsOverviewHighlights formattedDoc={parsedDoc} flattenedDoc={hit.flattened} />
<LogsOverviewDegradedFields rawDoc={hit.raw} />
{isStacktraceAvailable && <LogsOverviewStacktraceSection hit={hit} dataView={dataView} />}
{LogsOverviewAIAssistant && <LogsOverviewAIAssistant doc={hit} />}
</FieldActionsProvider>
);

View file

@ -0,0 +1,52 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EuiAccordion, EuiHorizontalRule, EuiTitle, useGeneratedHtmlId } from '@elastic/eui';
import { DataTableRecord } from '@kbn/discover-utils';
import { i18n } from '@kbn/i18n';
import React from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { StacktraceContent } from './sub_components/stacktrace/stacktrace_content';
const stacktraceAccordionTitle = i18n.translate(
'unifiedDocViewer.docView.logsOverview.accordion.title.stacktrace',
{
defaultMessage: 'Stacktrace',
}
);
export function LogsOverviewStacktraceSection({
hit,
dataView,
}: {
hit: DataTableRecord;
dataView: DataView;
}) {
const accordionId = useGeneratedHtmlId({
prefix: stacktraceAccordionTitle,
});
return (
<>
<EuiAccordion
id={accordionId}
buttonContent={
<EuiTitle size="xs">
<p>{stacktraceAccordionTitle}</p>
</EuiTitle>
}
paddingSize="m"
initialIsOpen={false}
data-test-subj="unifiedDocViewLogsOverviewStacktraceAccordion"
>
<StacktraceContent hit={hit} dataView={dataView} />
</EuiAccordion>
<EuiHorizontalRule margin="xs" />
</>
);
}

View file

@ -0,0 +1,233 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import '@kbn/code-editor-mock/jest_helper';
import * as hooks from '../../../../hooks/use_es_doc_search';
import { EuiLoadingSpinner } from '@elastic/eui';
import { buildDataTableRecord } from '@kbn/discover-utils';
import { ApmStacktrace } from './apm_stacktrace';
import { EuiThemeProvider } from '@elastic/eui';
import { ExceptionStacktrace, PlaintextStacktrace, Stacktrace } from '@kbn/event-stacktrace';
const mockDataView = {
getComputedFields: () => [],
} as never;
describe('APM Stacktrace component', () => {
test('renders loading state', () => {
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, () => {}]);
const comp = mountWithIntl(
<ApmStacktrace
hit={{
raw: { _id: '1', _index: 'index1' },
flattened: {},
id: '',
}}
dataView={mockDataView}
/>
);
const loadingSpinner = comp.find(EuiLoadingSpinner);
expect(loadingSpinner).toHaveLength(1);
});
test('renders error state', () => {
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, () => {}]);
const comp = mountWithIntl(
<ApmStacktrace
hit={{
raw: { _id: '1', _index: 'index1' },
flattened: {},
id: '',
}}
dataView={mockDataView}
/>
);
const errorComponent = comp.find('[data-test-subj="unifiedDocViewerApmStacktraceErrorMsg"]');
expect(errorComponent).toHaveLength(1);
});
test('renders log stacktrace', () => {
const mockHit = getMockHit({
id: '1',
grouping_key: '1',
log: {
message: 'Log message',
stacktrace: [
{
exclude_from_grouping: false,
abs_path: 'test.js',
filename: 'test.js',
line: {
number: 1,
context: 'console.log(err)',
},
function: '<anonymous>',
context: {
pre: ['console.log(err)'],
post: ['console.log(err)'],
},
vars: {},
},
{
exclude_from_grouping: false,
library_frame: true,
abs_path: 'test.js',
filename: 'test.js',
line: {
number: 1,
},
function: 'test',
vars: {},
},
],
},
});
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [2, mockHit, () => {}]);
const comp = mountWithIntl(
<EuiThemeProvider>
<ApmStacktrace
hit={{
raw: { _id: '1', _index: 'index1' },
flattened: {},
id: '',
}}
dataView={mockDataView}
/>
</EuiThemeProvider>
);
const stacktraceComponent = comp.find(Stacktrace);
expect(stacktraceComponent).toHaveLength(1);
});
test('renders exception stacktrace', () => {
const mockHit = getMockHit({
id: '1',
grouping_key: '1',
exception: [
{
message: 'Exception stacktrace',
stacktrace: [
{
exclude_from_grouping: false,
abs_path: 'test.js',
filename: 'test.js',
line: {
number: 1,
context: 'console.log(err)',
},
function: '<anonymous>',
context: {
pre: ['console.log(err)'],
post: ['console.log(err);'],
},
vars: {},
},
{
exclude_from_grouping: false,
library_frame: true,
abs_path: 'test.js',
filename: 'test.js',
line: {
number: 1,
},
function: 'test',
vars: {},
},
],
},
{
handled: true,
module: 'module',
attributes: {
test: 'test',
},
message: 'message',
type: 'type',
},
],
});
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [2, mockHit, () => {}]);
const comp = mountWithIntl(
<EuiThemeProvider>
<ApmStacktrace
hit={{
raw: { _id: '1', _index: 'index1' },
flattened: {},
id: '',
}}
dataView={mockDataView}
/>
</EuiThemeProvider>
);
const stacktraceComponent = comp.find(ExceptionStacktrace);
expect(stacktraceComponent).toHaveLength(1);
});
test('renders plain text stacktrace', () => {
const mockHit = getMockHit({
id: '1',
grouping_key: '1',
exception: [
{
handled: true,
message: 'message',
type: 'type',
},
],
stack_trace: 'test',
});
jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [2, mockHit, () => {}]);
const comp = mountWithIntl(
<EuiThemeProvider>
<ApmStacktrace
hit={{
raw: { _id: '1', _index: 'index1' },
flattened: {},
id: '',
}}
dataView={mockDataView}
/>
</EuiThemeProvider>
);
const stacktraceComponent = comp.find(PlaintextStacktrace);
expect(stacktraceComponent).toHaveLength(1);
});
});
function getMockHit(error: Record<string, unknown>) {
return buildDataTableRecord({
_index: '.ds-logs-apm.error-default-2024.12.31-000001',
_id: 'id123',
_score: 1,
_source: {
data_stream: {
type: 'logs',
dataset: 'apm.error',
namespace: 'default',
},
'@timestamp': '2024-12-31T00:00:00.000Z',
message: 'Log stacktrace',
error,
},
});
}

View file

@ -0,0 +1,78 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EuiLoadingSpinner } from '@elastic/eui';
import { DataTableRecord } from '@kbn/discover-utils';
import { i18n } from '@kbn/i18n';
import React, { useEffect, useState } from 'react';
import { ExceptionStacktrace, PlaintextStacktrace, Stacktrace } from '@kbn/event-stacktrace';
import type { APMError } from '@kbn/apm-types';
import type { DataView } from '@kbn/data-views-plugin/public';
import { ElasticRequestState } from '@kbn/unified-doc-viewer';
import { useEsDocSearch } from '../../../../hooks';
export const APM_ERROR_DATASTREAM_FIELDS = {
dataStreamType: 'logs',
dataStreamDataset: 'apm.error',
};
export function ApmStacktrace({ hit, dataView }: { hit: DataTableRecord; dataView: DataView }) {
const [apmErrorDoc, setApmErrorDoc] = useState<APMError>();
const [requestState, esHit] = useEsDocSearch({
id: hit.raw._id || '',
index: hit.raw._index,
dataView,
});
useEffect(() => {
if (requestState === ElasticRequestState.Found && esHit) {
setApmErrorDoc(esHit?.raw._source as unknown as APMError);
}
}, [requestState, esHit]);
if (requestState === ElasticRequestState.Loading) {
return <EuiLoadingSpinner size="m" />;
}
if (requestState === ElasticRequestState.Error || requestState === ElasticRequestState.NotFound) {
return (
<p data-test-subj="unifiedDocViewerApmStacktraceErrorMsg">
{i18n.translate('unifiedDocViewer.apmStacktrace.errorMessage', {
defaultMessage: 'Failed to load stacktrace',
})}
</p>
);
}
const codeLanguage = apmErrorDoc?.service?.language?.name;
const exceptions = apmErrorDoc?.error?.exception || [];
const logStackframes = apmErrorDoc?.error?.log?.stacktrace;
const isPlaintextException =
!!apmErrorDoc?.error?.stack_trace && exceptions.length === 1 && !exceptions[0].stacktrace;
if (apmErrorDoc?.error?.log?.message) {
return <Stacktrace stackframes={logStackframes} codeLanguage={codeLanguage} />;
}
if (apmErrorDoc?.error?.exception?.length) {
return isPlaintextException ? (
<PlaintextStacktrace
message={exceptions[0].message}
type={exceptions[0]?.type}
stacktrace={apmErrorDoc?.error.stack_trace}
codeLanguage={codeLanguage}
/>
) : (
<ExceptionStacktrace codeLanguage={codeLanguage} exceptions={exceptions} />
);
}
return null;
}

View file

@ -0,0 +1,43 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { EuiCodeBlock } from '@elastic/eui';
import { DataTableRecord, fieldConstants, getFieldValue } from '@kbn/discover-utils';
import { i18n } from '@kbn/i18n';
import React from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { APM_ERROR_DATASTREAM_FIELDS, ApmStacktrace } from './apm_stacktrace';
export function StacktraceContent({ hit, dataView }: { hit: DataTableRecord; dataView: DataView }) {
const errorStackTrace = getFieldValue(hit, fieldConstants.ERROR_STACK_TRACE) as string;
const dataStreamTypeField = getFieldValue(hit, fieldConstants.DATASTREAM_TYPE_FIELD) as string;
const dataStreamDatasetField = getFieldValue(
hit,
fieldConstants.DATASTREAM_DATASET_FIELD
) as string;
if (
dataStreamTypeField === APM_ERROR_DATASTREAM_FIELDS.dataStreamType &&
dataStreamDatasetField === APM_ERROR_DATASTREAM_FIELDS.dataStreamDataset
) {
return <ApmStacktrace hit={hit} dataView={dataView} />;
}
if (errorStackTrace) {
return <EuiCodeBlock isCopyable={true}>{errorStackTrace}</EuiCodeBlock>;
}
return (
<p>
{i18n.translate('unifiedDocViewer.stacktraceSection.errorMessage', {
defaultMessage: 'Failed to load stacktrace',
})}
</p>
);
}

View file

@ -36,7 +36,10 @@
"@kbn/router-utils",
"@kbn/unified-field-list",
"@kbn/core-lifecycle-browser",
"@kbn/management-settings-ids"
"@kbn/management-settings-ids",
"@kbn/apm-types",
"@kbn/event-stacktrace"
],
"exclude": [
"target/**/*",

View file

@ -90,8 +90,8 @@
"@kbn/apm-synthtrace/*": ["packages/kbn-apm-synthtrace/*"],
"@kbn/apm-synthtrace-client": ["packages/kbn-apm-synthtrace-client"],
"@kbn/apm-synthtrace-client/*": ["packages/kbn-apm-synthtrace-client/*"],
"@kbn/apm-types": ["x-pack/solutions/observability/packages/kbn-apm-types"],
"@kbn/apm-types/*": ["x-pack/solutions/observability/packages/kbn-apm-types/*"],
"@kbn/apm-types": ["x-pack/platform/packages/shared/kbn-apm-types"],
"@kbn/apm-types/*": ["x-pack/platform/packages/shared/kbn-apm-types/*"],
"@kbn/apm-utils": ["src/platform/packages/shared/kbn-apm-utils"],
"@kbn/apm-utils/*": ["src/platform/packages/shared/kbn-apm-utils/*"],
"@kbn/app-link-test-plugin": ["test/plugin_functional/plugins/app_link_test"],
@ -890,6 +890,8 @@
"@kbn/event-log-fixture-plugin/*": ["x-pack/test/plugin_api_integration/plugins/event_log/*"],
"@kbn/event-log-plugin": ["x-pack/platform/plugins/shared/event_log"],
"@kbn/event-log-plugin/*": ["x-pack/platform/plugins/shared/event_log/*"],
"@kbn/event-stacktrace": ["x-pack/platform/packages/shared/kbn-event-stacktrace"],
"@kbn/event-stacktrace/*": ["x-pack/platform/packages/shared/kbn-event-stacktrace/*"],
"@kbn/expandable-flyout": ["x-pack/solutions/security/packages/expandable-flyout"],
"@kbn/expandable-flyout/*": ["x-pack/solutions/security/packages/expandable-flyout/*"],
"@kbn/expect": ["packages/kbn-expect"],
@ -1116,6 +1118,8 @@
"@kbn/kbn-tp-custom-visualizations-plugin/*": ["test/plugin_functional/plugins/kbn_tp_custom_visualizations/*"],
"@kbn/kbn-tp-run-pipeline-plugin": ["test/interpreter_functional/plugins/kbn_tp_run_pipeline"],
"@kbn/kbn-tp-run-pipeline-plugin/*": ["test/interpreter_functional/plugins/kbn_tp_run_pipeline/*"],
"@kbn/key-value-metadata-table": ["x-pack/platform/packages/shared/kbn-key-value-metadata-table"],
"@kbn/key-value-metadata-table/*": ["x-pack/platform/packages/shared/kbn-key-value-metadata-table/*"],
"@kbn/kibana-cors-test-plugin": ["x-pack/test/functional_cors/plugins/kibana_cors_test"],
"@kbn/kibana-cors-test-plugin/*": ["x-pack/test/functional_cors/plugins/kibana_cors_test/*"],
"@kbn/kibana-manifest-schema": ["packages/kbn-kibana-manifest-schema"],

View file

@ -177,7 +177,8 @@
"solutions/observability/plugins/ux"
],
"xpack.urlDrilldown": "platform/plugins/private/drilldowns/url_drilldown",
"xpack.watcher": "platform/plugins/private/watcher"
"xpack.watcher": "platform/plugins/private/watcher",
"xpack.eventStacktrace": "platform/packages/shared/kbn-event-stacktrace"
},
"exclude": [
"examples"
@ -187,4 +188,4 @@
"@kbn/translations-plugin/translations/ja-JP.json",
"@kbn/translations-plugin/translations/fr-FR.json"
]
}
}

View file

@ -4,6 +4,6 @@
"owner": [
"@elastic/obs-ux-infra_services-team"
],
"group": "observability",
"visibility": "private"
}
"group": "platform",
"visibility": "shared"
}

View file

@ -0,0 +1,11 @@
/*
* 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 { setGlobalConfig } from '@storybook/testing-react';
import * as globalStorybookConfig from './preview';
setGlobalConfig(globalStorybookConfig);

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
module.exports = require('@kbn/storybook').defaultConfig;

View file

@ -0,0 +1,9 @@
/*
* 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 { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common';
export const decorators = [EuiThemeProviderDecorator];

View file

@ -0,0 +1,19 @@
# @kbn/event-stacktrace
This package contains components that render event (error, log, span) stack traces.
## Unit Tests (Jest)
```
node scripts/jest --config x-pack/platform/packages/shared/kbn-event-stacktrace/README.md [--watch]
```
## Storybook
### Start
```
yarn storybook event_stacktrace
```
All files with a .stories.tsx extension will be loaded. You can access the development environment at http://localhost:9001.

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
export * from './src/components/stacktrace/cause_stacktrace';
export * from './src/components/stacktrace/frame_heading';
export * from './src/components/stacktrace';
export * from './src/components/stacktrace/library_stacktrace';
export * from './src/components/stacktrace/stackframe';
export * from './src/components/stacktrace/variables';
export * from './src/components/stacktrace/plain/plaintext_stacktrace';
export * from './src/components/stacktrace/exception/exception_stacktrace';

View file

@ -0,0 +1,15 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../..',
roots: ['<rootDir>/x-pack/platform/packages/shared/kbn-event-stacktrace'],
setupFiles: [
'<rootDir>/x-pack/platform/packages/shared/kbn-event-stacktrace/.storybook/jest_setup.js',
],
};

View file

@ -0,0 +1,10 @@
{
"type": "shared-browser",
"id": "@kbn/event-stacktrace",
"owner": [
"@elastic/obs-ux-infra_services-team",
"@elastic/obs-ux-logs-team"
],
"group": "platform",
"visibility": "shared"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/event-stacktrace",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0"
}

View file

@ -7,7 +7,7 @@
import { shallow } from 'enzyme';
import React from 'react';
import { mountWithTheme } from '../../../utils/test_helpers';
import { mountWithTheme } from '../../utils/test_helpers';
import { CauseStacktrace } from './cause_stacktrace';
describe('CauseStacktrace', () => {

View file

@ -9,8 +9,8 @@ import { EuiAccordion, EuiTitle, useEuiFontSize } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import styled from '@emotion/styled';
import { Stackframe } from '@kbn/apm-types';
import { Stacktrace } from '.';
import type { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
const Accordion = styled(EuiAccordion)`
border-top: ${({ theme }) => theme.euiTheme.border.thin};
@ -37,7 +37,7 @@ function CausedBy({ message }: { message: string }) {
return (
<CausedByContainer>
<CausedByHeading>
{i18n.translate('xpack.apm.stacktraceTab.causedByFramesToogleButtonLabel', {
{i18n.translate('xpack.eventStacktrace.stacktraceTab.causedByFramesToogleButtonLabel', {
defaultMessage: 'Caused By',
})}
</CausedByHeading>

View file

@ -7,7 +7,7 @@
import React from 'react';
import { EuiCodeBlock } from '@elastic/eui';
import type { StackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe';
import type { StackframeWithLineContext } from '@kbn/apm-types';
function getStackframeLines(stackframe: StackframeWithLineContext) {
const line = stackframe.line.context;

View file

@ -6,10 +6,10 @@
*/
import React from 'react';
import type { Exception } from '../../../../../typings/es_schemas/raw/error_raw';
import { Stacktrace } from '../../../shared/stacktrace';
import { CauseStacktrace } from '../../../shared/stacktrace/cause_stacktrace';
import type { Exception } from '@kbn/apm-types/es_schemas_raw';
import { ExceptionStacktraceTitle } from './exception_stacktrace_title';
import { CauseStacktrace } from '../cause_stacktrace';
import { Stacktrace } from '..';
interface ExceptionStacktraceProps {
codeLanguage?: string;

View file

@ -6,8 +6,8 @@
*/
import React from 'react';
import type { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { renderWithTheme } from '../../../utils/test_helpers';
import { Stackframe } from '@kbn/apm-types';
import { renderWithTheme } from '../../utils/test_helpers';
import { FrameHeading } from './frame_heading';
function getRenderedStackframeText(stackframe: Stackframe, codeLanguage: string, idx: string) {

View file

@ -9,8 +9,7 @@ import type { ComponentType } from 'react';
import React from 'react';
import styled from '@emotion/styled';
import { useEuiFontSize } from '@elastic/eui';
import type { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import type { FrameHeadingRendererProps } from './frame_heading_renderers';
import { Stackframe } from '@kbn/apm-types';
import {
CSharpFrameHeadingRenderer,
DefaultFrameHeadingRenderer,
@ -18,6 +17,7 @@ import {
JavaScriptFrameHeadingRenderer,
RubyFrameHeadingRenderer,
PhpFrameHeadingRenderer,
FrameHeadingRendererProps,
} from './frame_heading_renderers';
const FileDetails = styled.div`

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import type { ComponentType } from 'react';
import type { Stackframe } from '../../../../../typings/es_schemas/raw/fields/stackframe';
import { ComponentType } from 'react';
import type { Stackframe } from '@kbn/apm-types';
export interface FrameHeadingRendererProps {
fileDetailComponent: ComponentType<React.PropsWithChildren<{}>>;

View file

@ -8,9 +8,8 @@
import { i18n } from '@kbn/i18n';
import { isEmpty, last } from 'lodash';
import React, { Fragment } from 'react';
import { EuiCodeBlock } from '@elastic/eui';
import type { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { EmptyMessage } from '../empty_message';
import { EuiCodeBlock, EuiEmptyPrompt } from '@elastic/eui';
import type { Stackframe } from '@kbn/apm-types';
import { LibraryStacktrace } from './library_stacktrace';
import { Stackframe as StackframeComponent } from './stackframe';
@ -23,11 +22,15 @@ interface Props {
export function Stacktrace({ stackframes = [], codeLanguage }: Props) {
if (isEmpty(stackframes)) {
return (
<EmptyMessage
heading={i18n.translate('xpack.apm.stacktraceTab.noStacktraceAvailableLabel', {
defaultMessage: 'No stack trace available.',
})}
hideSubheading
<EuiEmptyPrompt
titleSize="s"
title={
<div>
{i18n.translate('xpack.eventStacktrace.stacktraceTab.noStacktraceAvailableLabel', {
defaultMessage: 'No stack trace available.',
})}
</div>
}
/>
);
}

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { renderWithTheme } from '../../../utils/test_helpers';
import { renderWithTheme } from '../../utils/test_helpers';
import { LibraryStacktrace } from './library_stacktrace';
describe('LibraryStacktrace', () => {

View file

@ -9,7 +9,7 @@ import { EuiAccordion } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import styled from '@emotion/styled';
import type { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { Stackframe } from '@kbn/apm-types';
import { Stackframe as StackframeComponent } from './stackframe';
const LibraryStacktraceAccordion = styled(EuiAccordion)`
@ -29,10 +29,13 @@ export function LibraryStacktrace({ codeLanguage, id, stackframes }: Props) {
return (
<LibraryStacktraceAccordion
buttonContent={i18n.translate('xpack.apm.stacktraceTab.libraryFramesToogleButtonLabel', {
defaultMessage: '{count, plural, one {# library frame} other {# library frames}}',
values: { count: stackframes.length },
})}
buttonContent={i18n.translate(
'xpack.eventStacktrace.stacktraceTab.libraryFramesToogleButtonLabel',
{
defaultMessage: '{count, plural, one {# library frame} other {# library frames}}',
values: { count: stackframes.length },
}
)}
data-test-subj="LibraryStacktraceAccordion"
id={id}
>

View file

@ -7,7 +7,7 @@
import React from 'react';
import { EuiCodeBlock } from '@elastic/eui';
import { ExceptionStacktraceTitle } from './exception_stacktrace_title';
import { ExceptionStacktraceTitle } from '../exception/exception_stacktrace_title';
interface PlaintextStacktraceProps {
codeLanguage?: string;

View file

@ -6,10 +6,9 @@
*/
import React from 'react';
import type { ReactWrapper } from 'enzyme';
import { shallow } from 'enzyme';
import type { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { mountWithTheme } from '../../../utils/test_helpers';
import { ReactWrapper, shallow } from 'enzyme';
import type { Stackframe } from '@kbn/apm-types';
import { mountWithTheme } from '../../utils/test_helpers';
import { Stackframe as StackframeComponent } from './stackframe';
import stacktracesMock from './__fixtures__/stacktraces.json';

View file

@ -8,10 +8,7 @@
import { EuiAccordion, useEuiFontSize } from '@elastic/eui';
import React from 'react';
import styled from '@emotion/styled';
import type {
Stackframe as StackframeType,
StackframeWithLineContext,
} from '../../../../typings/es_schemas/raw/fields/stackframe';
import type { Stackframe as StackframeType, StackframeWithLineContext } from '@kbn/apm-types';
import { Context } from './context';
import { FrameHeading } from './frame_heading';
import { Variables } from './variables';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { Stackframe } from '@kbn/apm-types';
import { getGroupedStackframes } from '.';
import stacktracesMock from './__fixtures__/stacktraces.json';

View file

@ -8,10 +8,9 @@
import { EuiAccordion } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { KeyValueTable, getFlattenedKeyValuePairs } from '@kbn/key-value-metadata-table';
import { Stackframe } from '@kbn/apm-types';
import styled from '@emotion/styled';
import type { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe';
import { KeyValueTable } from '../key_value_table';
import { flattenObject } from '../../../../common/utils/flatten_object';
const VariablesContainer = styled.div`
background: ${({ theme }) => theme.euiTheme.colors.emptyShade};
@ -28,16 +27,19 @@ export function Variables({ vars }: Props) {
if (!vars) {
return null;
}
const flattenedVariables = flattenObject(vars);
const flattenedVariables = getFlattenedKeyValuePairs(vars);
return (
<React.Fragment>
<VariablesContainer>
<EuiAccordion
id="local-variables"
className="euiAccordion"
buttonContent={i18n.translate('xpack.apm.stacktraceTab.localVariablesToogleButtonLabel', {
defaultMessage: 'Local variables',
})}
buttonContent={i18n.translate(
'xpack.eventStacktrace.stacktraceTab.localVariablesToogleButtonLabel',
{
defaultMessage: 'Local variables',
}
)}
>
<React.Fragment>
<KeyValueTable keyValuePairs={flattenedVariables} />

View file

@ -0,0 +1,27 @@
/*
* 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 React from 'react';
import { EuiThemeProvider } from '@elastic/eui';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render } from '@testing-library/react';
// eslint-disable-next-line import/no-extraneous-dependencies
import { mount, MountRendererProps } from 'enzyme';
export function renderWithTheme(component: React.ReactNode, params?: any) {
return render(<EuiThemeProvider>{component}</EuiThemeProvider>, params);
}
export function mountWithTheme(tree: React.ReactElement<any>) {
function WrappingThemeProvider(props: any) {
return <EuiThemeProvider>{props.children}</EuiThemeProvider>;
}
return mount(tree, {
wrappingComponent: WrappingThemeProvider,
} as MountRendererProps);
}

View file

@ -0,0 +1,26 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react",
"@testing-library/jest-dom"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
"src/**/*.json",
"../../../../../typings/emotion.d.ts"
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/i18n",
"@kbn/apm-types",
"@kbn/key-value-metadata-table"
]
}

View file

@ -0,0 +1,3 @@
# @kbn/key-value-metadata-table
Key-value metadata table

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export { KeyValueTable } from './src';
export { getFlattenedKeyValuePairs } from './src/utils/get_flattened_key_value_pairs';
export type { KeyValuePair } from './src/utils/get_flattened_key_value_pairs';

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../..',
roots: ['<rootDir>/x-pack/platform/packages/shared/kbn-key-value-metadata-table'],
};

View file

@ -0,0 +1,12 @@
{
"type": "shared-common",
"id": "@kbn/key-value-metadata-table",
"owner": [
"@elastic/obs-ux-infra_services-team",
"@elastic/obs-ux-logs-team"
],
"group": "platform",
"visibility": "shared"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/key-value-metadata-table",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0"
}

View file

@ -8,7 +8,7 @@
import { isBoolean, isNumber, isObject } from 'lodash';
import React from 'react';
import styled from '@emotion/styled';
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
import { i18n } from '@kbn/i18n';
const EmptyValue = styled.span`
color: ${({ theme }) => theme.euiTheme.colors.mediumShade};
@ -29,7 +29,13 @@ export function FormattedValue({ value }: { value: any }): JSX.Element {
} else if (isBoolean(value) || isNumber(value)) {
return <React.Fragment>{String(value)}</React.Fragment>;
} else if (!value) {
return <EmptyValue>{NOT_AVAILABLE_LABEL}</EmptyValue>;
return (
<EmptyValue>
{i18n.translate('keyValueMetadataTable.notAvailableLabel', {
defaultMessage: 'N/A',
})}
</EmptyValue>
);
}
return <React.Fragment>{value}</React.Fragment>;

Some files were not shown because too many files have changed in this diff Show more