mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
cf4d79c862
commit
368475e8e5
116 changed files with 772 additions and 111 deletions
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -23,4 +23,4 @@
|
|||
"kibanaUtils"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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" />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
"owner": [
|
||||
"@elastic/obs-ux-infra_services-team"
|
||||
],
|
||||
"group": "observability",
|
||||
"visibility": "private"
|
||||
}
|
||||
"group": "platform",
|
||||
"visibility": "shared"
|
||||
}
|
|
@ -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);
|
|
@ -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;
|
|
@ -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];
|
|
@ -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.
|
|
@ -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';
|
|
@ -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',
|
||||
],
|
||||
};
|
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/event-stacktrace",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0"
|
||||
}
|
|
@ -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', () => {
|
|
@ -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>
|
|
@ -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;
|
|
@ -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;
|
|
@ -40,7 +40,7 @@ export function ExceptionStacktraceTitle({
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiTitle size="xs">
|
||||
<EuiTitle size="xxs">
|
||||
<h4>{title}</h4>
|
||||
</EuiTitle>
|
||||
);
|
|
@ -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) {
|
|
@ -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`
|
|
@ -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<{}>>;
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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', () => {
|
|
@ -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}
|
||||
>
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
@ -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} />
|
|
@ -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);
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# @kbn/key-value-metadata-table
|
||||
|
||||
Key-value metadata table
|
|
@ -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';
|
|
@ -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'],
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/key-value-metadata-table",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0"
|
||||
}
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue