diff --git a/src/platform/packages/shared/kbn-discover-utils/index.ts b/src/platform/packages/shared/kbn-discover-utils/index.ts index 968fe5aece1fb..5e13714344fc1 100644 --- a/src/platform/packages/shared/kbn-discover-utils/index.ts +++ b/src/platform/packages/shared/kbn-discover-utils/index.ts @@ -36,6 +36,7 @@ export { convertValueToString, createLogsContextService, createTracesContextService, + createApmErrorsContextService, createDegradedDocsControl, createStacktraceControl, fieldConstants, @@ -66,7 +67,7 @@ export { LogLevelBadge, } from './src'; -export type { LogsContextService, TracesContextService } from './src'; +export type { LogsContextService, TracesContextService, ApmErrorsContextService } from './src'; export * from './src/types'; diff --git a/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/apm_errors_context_service.ts b/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/apm_errors_context_service.ts new file mode 100644 index 0000000000000..2b67280d0f5cb --- /dev/null +++ b/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/apm_errors_context_service.ts @@ -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", 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 { getApmErrorsContextService } from '../data_types'; + +export const createApmErrorsContextServiceMock = () => getApmErrorsContextService('errors-*'); diff --git a/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/index.ts b/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/index.ts index e01a94822dae9..d44d71aa05486 100644 --- a/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/index.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/__mocks__/index.ts @@ -12,3 +12,4 @@ export * from './es_hits'; export * from './additional_field_groups'; export * from './logs_context_service'; export * from './traces_context_service'; +export * from './apm_errors_context_service'; diff --git a/src/platform/packages/shared/kbn-discover-utils/src/data_types/apm/errors/errors_context_service.ts b/src/platform/packages/shared/kbn-discover-utils/src/data_types/apm/errors/errors_context_service.ts new file mode 100644 index 0000000000000..e195e8d34e50f --- /dev/null +++ b/src/platform/packages/shared/kbn-discover-utils/src/data_types/apm/errors/errors_context_service.ts @@ -0,0 +1,50 @@ +/* + * 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 type { ApmSourceAccessPluginStart } from '@kbn/apm-sources-access-plugin/public'; + +export interface ApmErrorsContextService { + getErrorsIndexPattern(): string; +} + +export interface ApmErrorsContextServiceDeps { + apmSourcesAccess?: ApmSourceAccessPluginStart; +} + +// should we have defaults here? +export const DEFAULT_ALLOWED_APM_ERRORS_BASE_PATTERNS = []; + +export const createApmErrorsContextService = async ({ + apmSourcesAccess, +}: ApmErrorsContextServiceDeps): Promise => { + if (!apmSourcesAccess) { + return defaultApmErrorsContextService; + } + + try { + const indices = await apmSourcesAccess.getApmIndices(); + + if (!indices) { + return defaultApmErrorsContextService; + } + + const { error } = indices; + return getApmErrorsContextService(error); + } catch (error) { + return defaultApmErrorsContextService; + } +}; + +export const getApmErrorsContextService = (error: string) => ({ + getErrorsIndexPattern: () => error, +}); + +const defaultApmErrorsContextService = getApmErrorsContextService( + DEFAULT_ALLOWED_APM_ERRORS_BASE_PATTERNS.join() +); diff --git a/src/platform/packages/shared/kbn-discover-utils/src/data_types/apm/index.ts b/src/platform/packages/shared/kbn-discover-utils/src/data_types/apm/index.ts new file mode 100644 index 0000000000000..5644c329928f4 --- /dev/null +++ b/src/platform/packages/shared/kbn-discover-utils/src/data_types/apm/index.ts @@ -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", 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". + */ + +export * from './errors/errors_context_service'; diff --git a/src/platform/packages/shared/kbn-discover-utils/src/data_types/index.ts b/src/platform/packages/shared/kbn-discover-utils/src/data_types/index.ts index 9a283930ba84a..cc223471a8e05 100644 --- a/src/platform/packages/shared/kbn-discover-utils/src/data_types/index.ts +++ b/src/platform/packages/shared/kbn-discover-utils/src/data_types/index.ts @@ -9,3 +9,4 @@ export * from './logs'; export * from './traces'; +export * from './apm'; diff --git a/src/platform/plugins/shared/discover/public/context_awareness/__mocks__/index.tsx b/src/platform/plugins/shared/discover/public/context_awareness/__mocks__/index.tsx index 40eae4144513a..5807d61b6b721 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/__mocks__/index.tsx +++ b/src/platform/plugins/shared/discover/public/context_awareness/__mocks__/index.tsx @@ -27,6 +27,7 @@ import type { ProfileProviderServices } from '../profile_providers/profile_provi import { ProfilesManager } from '../profiles_manager'; import { DiscoverEBTManager } from '../../plugin_imports/discover_ebt_manager'; import { + createApmErrorsContextServiceMock, createLogsContextServiceMock, createTracesContextServiceMock, } from '@kbn/discover-utils/src/__mocks__'; @@ -198,6 +199,7 @@ const createProfileProviderServicesMock = () => { logsContextService: createLogsContextServiceMock(), discoverShared: discoverSharedPluginMock.createStartContract(), tracesContextService: createTracesContextServiceMock(), + apmErrorsContextService: createApmErrorsContextServiceMock(), core: { pricing: pricingServiceMock.createStartContract() as ReturnType< typeof pricingServiceMock.createStartContract diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/accessors/doc_viewer.tsx b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/accessors/doc_viewer.tsx index 23c5293f69c3a..43f4d593ad01e 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/accessors/doc_viewer.tsx +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/accessors/doc_viewer.tsx @@ -15,7 +15,10 @@ import type { DocumentProfileProvider } from '../../../../../profiles'; import type { DocViewerExtensionParams, DocViewerExtension } from '../../../../../types'; export const createGetDocViewer = - (tracesIndexPattern: string): DocumentProfileProvider['profile']['getDocViewer'] => + (indexes: { + apm: { errors: string; traces: string }; + logs: string; + }): DocumentProfileProvider['profile']['getDocViewer'] => (prev: (params: DocViewerExtensionParams) => DocViewerExtension) => (params: DocViewerExtensionParams) => { const prevDocViewer = prev(params); @@ -30,12 +33,7 @@ export const createGetDocViewer = }), order: 0, component: (props) => { - return ( - - ); + return ; }, }); diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/profile.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/profile.ts index 1eec6a3b58146..57577c57603cf 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/profile.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/span_document_profile/profile.ts @@ -19,12 +19,20 @@ const OBSERVABILITY_TRACES_SPAN_DOCUMENT_PROFILE_ID = 'observability-traces-span export const createObservabilityTracesSpanDocumentProfileProvider = ({ tracesContextService, + apmErrorsContextService, + logsContextService, }: ProfileProviderServices): DocumentProfileProvider => ({ isExperimental: true, profileId: OBSERVABILITY_TRACES_SPAN_DOCUMENT_PROFILE_ID, restrictedToProductFeature: TRACES_PRODUCT_FEATURE_ID, profile: { - getDocViewer: createGetDocViewer(tracesContextService.getAllTracesIndexPattern()), + getDocViewer: createGetDocViewer({ + apm: { + errors: apmErrorsContextService.getErrorsIndexPattern(), + traces: tracesContextService.getAllTracesIndexPattern(), + }, + logs: logsContextService.getAllLogsIndexPattern() ?? '', + }), }, resolve: ({ record, rootContext }) => { const isObservabilitySolutionView = rootContext.solutionType === SolutionType.Observability; diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/doc_viewer.tsx b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/doc_viewer.tsx index 9c5164e9927ab..e24b2da47e423 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/doc_viewer.tsx +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/accessors/doc_viewer.tsx @@ -15,7 +15,10 @@ import type { DocumentProfileProvider } from '../../../../..'; import type { DocViewerExtensionParams, DocViewerExtension } from '../../../../../types'; export const createGetDocViewer = - (tracesIndexPattern: string): DocumentProfileProvider['profile']['getDocViewer'] => + (indexes: { + apm: { errors: string; traces: string }; + logs: string; + }): DocumentProfileProvider['profile']['getDocViewer'] => (prev: (params: DocViewerExtensionParams) => DocViewerExtension) => (params: DocViewerExtensionParams) => { const prevDocViewer = prev(params); @@ -36,7 +39,7 @@ export const createGetDocViewer = return ( ); }, diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.ts index fab0a64818b04..5db7dbfafad51 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/observability/traces_document_profile/transaction_document_profile/profile.ts @@ -20,12 +20,20 @@ const OBSERVABILITY_TRACES_TRANSACTION_DOCUMENT_PROFILE_ID = export const createObservabilityTracesTransactionDocumentProfileProvider = ({ tracesContextService, + apmErrorsContextService, + logsContextService, }: ProfileProviderServices): DocumentProfileProvider => ({ isExperimental: true, profileId: OBSERVABILITY_TRACES_TRANSACTION_DOCUMENT_PROFILE_ID, restrictedToProductFeature: TRACES_PRODUCT_FEATURE_ID, profile: { - getDocViewer: createGetDocViewer(tracesContextService.getAllTracesIndexPattern() || ''), + getDocViewer: createGetDocViewer({ + apm: { + errors: apmErrorsContextService.getErrorsIndexPattern(), + traces: tracesContextService.getAllTracesIndexPattern(), + }, + logs: logsContextService.getAllLogsIndexPattern() ?? '', + }), }, resolve: ({ record, rootContext }) => { const isObservabilitySolutionView = rootContext.solutionType === SolutionType.Observability; diff --git a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/profile_provider_services.ts b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/profile_provider_services.ts index 06f30fc5bd676..c36cd87d33483 100644 --- a/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/profile_provider_services.ts +++ b/src/platform/plugins/shared/discover/public/context_awareness/profile_providers/profile_provider_services.ts @@ -12,6 +12,8 @@ import { type LogsContextService, createTracesContextService, type TracesContextService, + createApmErrorsContextService, + type ApmErrorsContextService, } from '@kbn/discover-utils'; import type { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/public'; @@ -35,6 +37,7 @@ export interface ProfileProviderServices extends DiscoverServices { */ logsContextService: LogsContextService; tracesContextService: TracesContextService; + apmErrorsContextService: ApmErrorsContextService; } /** @@ -53,5 +56,8 @@ export const createProfileProviderServices = async ( tracesContextService: await createTracesContextService({ apmSourcesAccess: discoverServices.apmSourcesAccess, }), + apmErrorsContextService: await createApmErrorsContextService({ + apmSourcesAccess: discoverServices.apmSourcesAccess, + }), }; }; diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/exit_full_screen_button/index.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/exit_full_screen_button/index.tsx new file mode 100644 index 0000000000000..6c7ec8468b44c --- /dev/null +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/exit_full_screen_button/index.tsx @@ -0,0 +1,46 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiButtonIcon, EuiWindowEvent } from '@elastic/eui'; +export interface ExitFullScreenButtonProps { + ariaLabel: string; + dataTestSubj: string; + onExitFullScreen: () => void; +} + +export const ExitFullScreenButton = ({ + ariaLabel, + dataTestSubj, + onExitFullScreen, +}: ExitFullScreenButtonProps) => { + const onKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + + onExitFullScreen(); + } + }, + [onExitFullScreen] + ); + return ( + <> + + + + ); +}; diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.tsx index 9403e2ff60a91..badabcb627df1 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/index.tsx @@ -7,10 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { EmbeddableRenderer } from '@kbn/embeddable-plugin/public'; import { - EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFocusTrap, @@ -22,8 +21,11 @@ import { import { SERVICE_NAME_FIELD, SPAN_ID_FIELD, TRANSACTION_ID_FIELD } from '@kbn/discover-utils'; import { i18n } from '@kbn/i18n'; import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types'; -import { useRootTransactionContext } from '../../doc_viewer_transaction_overview/hooks/use_root_transaction'; +import { getUnifiedDocViewerServices } from '../../../../../plugin'; import { SpanFlyout } from './span_flyout'; +import { useRootTransactionContext } from '../../doc_viewer_transaction_overview/hooks/use_root_transaction'; +import { useDataSourcesContext } from '../../hooks/use_data_sources'; +import { ExitFullScreenButton } from './exit_full_screen_button'; export interface FullScreenWaterfallProps { traceId: string; @@ -31,7 +33,7 @@ export interface FullScreenWaterfallProps { rangeTo: string; dataView: DocViewRenderProps['dataView']; tracesIndexPattern: string; - onCloseFullScreen: () => void; + onExitFullScreen: () => void; } export const FullScreenWaterfall = ({ @@ -40,12 +42,45 @@ export const FullScreenWaterfall = ({ rangeTo, dataView, tracesIndexPattern, - onCloseFullScreen, + onExitFullScreen, }: FullScreenWaterfallProps) => { const { transaction } = useRootTransactionContext(); const [spanId, setSpanId] = useState(null); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const overlayMaskRef = useRef(null); + const { + share: { + url: { locators }, + }, + data: { + query: { + timefilter: { timefilter }, + }, + }, + } = getUnifiedDocViewerServices(); + const { indexes } = useDataSourcesContext(); + + const discoverLocator = useMemo(() => locators.get('DISCOVER_APP_LOCATOR'), [locators]); + + const generateRelatedErrorsDiscoverUrl = useCallback( + (docId: string) => { + if (!discoverLocator) { + return null; + } + + const url = discoverLocator.getRedirectUrl({ + timeRange: timefilter.getAbsoluteTime(), + filters: [], + query: { + language: 'kuery', + esql: `FROM ${indexes.apm.errors},${indexes.logs} | WHERE trace.id == "${traceId}" AND span.id == "${docId}"`, + }, + }); + + return url; + }, + [discoverLocator, timefilter, indexes.apm.errors, indexes.logs, traceId] + ); const getParentApi = useCallback( () => ({ @@ -57,6 +92,7 @@ export const FullScreenWaterfall = ({ serviceName: transaction?.[SERVICE_NAME_FIELD], entryTransactionId: transaction?.[TRANSACTION_ID_FIELD] || transaction?.[SPAN_ID_FIELD], scrollElement: overlayMaskRef.current, + getRelatedErrorsHref: generateRelatedErrorsDiscoverUrl, onNodeClick: (nodeSpanId: string) => { setSpanId(nodeSpanId); setIsFlyoutVisible(true); @@ -64,7 +100,7 @@ export const FullScreenWaterfall = ({ }, }), }), - [traceId, rangeFrom, rangeTo, transaction] + [traceId, rangeFrom, rangeTo, transaction, generateRelatedErrorsDiscoverUrl] ); return ( @@ -89,18 +125,15 @@ export const FullScreenWaterfall = ({ - diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/span_flyout/span_flyout_body.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/span_flyout/span_flyout_body.tsx index c9044ae73e2b5..1bff542bd514b 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/span_flyout/span_flyout_body.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/components/full_screen_waterfall/span_flyout/span_flyout_body.tsx @@ -16,6 +16,7 @@ import SpanOverview from '../../../doc_viewer_span_overview'; import TransactionOverview from '../../../doc_viewer_transaction_overview'; import DocViewerTable from '../../../../../doc_viewer_table'; import DocViewerSource from '../../../../../doc_viewer_source'; +import { useDataSourcesContext } from '../../../hooks/use_data_sources'; const tabIds = { OVERVIEW: 'unifiedDocViewerTracesSpanFlyoutOverview', @@ -55,9 +56,10 @@ export interface SpanFlyoutProps { onCloseFlyout: () => void; } -export const SpanFlyoutBody = ({ tracesIndexPattern, hit, loading, dataView }: SpanFlyoutProps) => { +export const SpanFlyoutBody = ({ hit, loading, dataView }: SpanFlyoutProps) => { const [selectedTabId, setSelectedTabId] = useState(tabIds.OVERVIEW); const isSpan = !!hit?.flattened[PARENT_ID_FIELD]; + const { indexes } = useDataSourcesContext(); const onSelectedTabChanged = (id: string) => setSelectedTabId(id); const renderTabs = () => { @@ -84,7 +86,7 @@ export const SpanFlyoutBody = ({ tracesIndexPattern, hit, loading, dataView }: S (isSpan ? ( { + onExitFullScreen={() => { setShowFullScreenWaterfall(false); }} /> diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx index ee4928ff6116c..6d2622714d49f 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_span_overview/span_overview.tsx @@ -30,9 +30,16 @@ import { SpanDurationSummary } from './sub_components/span_duration_summary'; import { SpanSummaryField } from './sub_components/span_summary_field'; import { SpanSummaryTitle } from './sub_components/span_summary_title'; import { RootTransactionProvider } from '../doc_viewer_transaction_overview/hooks/use_root_transaction'; +import { DataSourcesProvider } from '../hooks/use_data_sources'; export type SpanOverviewProps = DocViewRenderProps & { - tracesIndexPattern: string; + indexes: { + apm: { + traces: string; + errors: string; + }; + logs: string; + }; showWaterfall?: boolean; showActions?: boolean; }; @@ -43,7 +50,7 @@ export function SpanOverview({ filter, onAddColumn, onRemoveColumn, - tracesIndexPattern, + indexes, showWaterfall = true, showActions = true, dataView, @@ -65,66 +72,68 @@ export function SpanOverview({ const transactionId = flattenedDoc[TRANSACTION_ID_FIELD]; return ( - - - - - - - - - - - {spanFields.map((fieldId) => ( - + + + + + + + + - ))} - + + + {spanFields.map((fieldId) => ( + + ))} + - {spanDuration && ( + {spanDuration && ( + + + + + )} - - - )} - - - - - - - - + + + + + + ); } diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx index 4a398f7f09b77..4bb4373b217eb 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/doc_viewer_transaction_overview/transaction_overview.tsx @@ -29,9 +29,16 @@ import { RootTransactionProvider } from './hooks/use_root_transaction'; import { Trace } from '../components/trace'; import { TransactionSummaryTitle } from './sub_components/transaction_summary_title'; import { getUnifiedDocViewerServices } from '../../../../plugin'; +import { DataSourcesProvider } from '../hooks/use_data_sources'; export type TransactionOverviewProps = DocViewRenderProps & { - tracesIndexPattern: string; + indexes: { + apm: { + traces: string; + errors: string; + }; + logs: string; + }; showWaterfall?: boolean; showActions?: boolean; }; @@ -42,7 +49,7 @@ export function TransactionOverview({ filter, onAddColumn, onRemoveColumn, - tracesIndexPattern, + indexes, showWaterfall = true, showActions = true, dataView, @@ -65,63 +72,65 @@ export function TransactionOverview({ const transactionId = flattenedDoc[TRANSACTION_ID_FIELD]; return ( - - - - - - - - - - {transactionFields.map((fieldId) => ( - + + + + + + + - ))} - - {transactionDuration !== undefined && ( + - + {transactionFields.map((fieldId) => ( + + ))} - )} - - {traceId && transactionId && ( - + {transactionDuration !== undefined && ( + + + )} - - - - - + + {traceId && transactionId && ( + + )} + + + + + + ); } diff --git a/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/hooks/use_data_sources/index.ts b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/hooks/use_data_sources/index.ts new file mode 100644 index 0000000000000..3a306547b669d --- /dev/null +++ b/src/platform/plugins/shared/unified_doc_viewer/public/components/observability/traces/hooks/use_data_sources/index.ts @@ -0,0 +1,28 @@ +/* + * 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 createContainer from 'constate'; + +type UseDataSourcesParams = DataSources; + +export interface DataSources { + indexes: { + logs: string; + apm: { + errors: string; + traces: string; + }; + }; +} + +const useDataSources = ({ indexes }: UseDataSourcesParams) => { + return { indexes }; +}; + +export const [DataSourcesProvider, useDataSourcesContext] = createContainer(useDataSources); diff --git a/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json b/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json index 7fcfb767ff254..e595f51d842ca 100644 --- a/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json +++ b/src/platform/plugins/shared/unified_doc_viewer/tsconfig.json @@ -47,7 +47,7 @@ "@kbn/embeddable-plugin", "@kbn/apm-types-shared", "@kbn/object-utils", - "@kbn/es-query", + "@kbn/es-query" ], "exclude": ["target/**/*"] } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx index 7b211b667e5df..4114f4a836c6c 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx @@ -27,6 +27,7 @@ import type { IWaterfallNodeFlatten, IWaterfall, IWaterfallSpanOrTransaction, + IWaterfallGetRelatedErrorsHref, } from './waterfall_helpers/waterfall_helpers'; import { WaterfallItem } from './waterfall_item'; import { WaterfallContextProvider } from './context/waterfall_context'; @@ -44,6 +45,7 @@ interface AccordionWaterfallProps { displayLimit?: number; isEmbeddable?: boolean; scrollElement?: Element; + getRelatedErrorsHref?: IWaterfallGetRelatedErrorsHref; } type WaterfallProps = Omit< @@ -185,7 +187,14 @@ const VirtualRow = React.memo( const WaterfallNode = React.memo((props: WaterfallNodeProps) => { const theme = useTheme(); - const { duration, waterfallItemId, onClickWaterfallItem, timelineMargins, node } = props; + const { + duration, + waterfallItemId, + onClickWaterfallItem, + getRelatedErrorsHref, + timelineMargins, + node, + } = props; const { criticalPathSegmentsById, getErrorCount, @@ -251,6 +260,7 @@ const WaterfallNode = React.memo((props: WaterfallNodeProps) => { onClick={onWaterfallItemClick} segments={segments} isEmbeddable={isEmbeddable} + getRelatedErrorsHref={getRelatedErrorsHref} /> diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx index e85a32b6f31d9..9b545b94cc45b 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx @@ -21,6 +21,7 @@ import { getErrorMarks } from '../marks/get_error_marks'; import { AccordionWaterfall } from './accordion_waterfall'; import type { IWaterfall, + IWaterfallGetRelatedErrorsHref, IWaterfallSpanOrTransaction, } from './waterfall_helpers/waterfall_helpers'; @@ -41,6 +42,7 @@ interface Props { displayLimit?: number; isEmbeddable?: boolean; scrollElement?: Element; + getRelatedErrorsHref?: IWaterfallGetRelatedErrorsHref; } function getWaterfallMaxLevel(waterfall: IWaterfall) { @@ -81,6 +83,7 @@ export function Waterfall({ displayLimit, isEmbeddable, scrollElement, + getRelatedErrorsHref, }: Props) { const theme = useTheme(); const [isAccordionOpen, setIsAccordionOpen] = useState(true); @@ -185,6 +188,7 @@ export function Waterfall({ displayLimit={displayLimit} isEmbeddable={isEmbeddable} scrollElement={scrollElement} + getRelatedErrorsHref={getRelatedErrorsHref} /> )} diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.ts index c510e6642558b..e3dfa489070b8 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers.ts @@ -91,6 +91,8 @@ export type IWaterfallSpan = IWaterfallItemBase; export type IWaterfallSpanOrTransaction = IWaterfallTransaction | IWaterfallSpan; +export type IWaterfallGetRelatedErrorsHref = (docId: string) => string; + export type IWaterfallItem = IWaterfallSpanOrTransaction; export interface IWaterfallLegend { diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx index fe94900758bd3..c585526f90ab8 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx @@ -24,7 +24,10 @@ import { SyncBadge } from './badge/sync_badge'; import { FailureBadge } from './failure_badge'; import { OrphanItemTooltipIcon } from './orphan_item_tooltip_icon'; import { SpanMissingDestinationTooltip } from './span_missing_destination_tooltip'; -import type { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers'; +import type { + IWaterfallGetRelatedErrorsHref, + IWaterfallSpanOrTransaction, +} from './waterfall_helpers/waterfall_helpers'; type ItemType = 'transaction' | 'span' | 'error'; @@ -126,6 +129,7 @@ interface IWaterfallItemProps { color: string; }>; onClick?: (flyoutDetailTab: string) => unknown; + getRelatedErrorsHref?: IWaterfallGetRelatedErrorsHref; isEmbeddable?: boolean; } @@ -226,6 +230,7 @@ export function WaterfallItem({ color, isSelected, errorCount, + getRelatedErrorsHref, marginLeftLevel, onClick, segments, @@ -300,7 +305,11 @@ export function WaterfallItem({ {isEmbeddable ? ( - + ) : ( )} @@ -320,12 +329,46 @@ export function WaterfallItem({ ); } -function EmbeddableErrorIcon({ errorCount }: { errorCount: number }) { - const theme = useEuiTheme(); - if (errorCount <= 0) { - return null; +function EmbeddableRelatedErrors({ + item, + errorCount, + getRelatedErrorsHref, +}: { + item: IWaterfallSpanOrTransaction; + errorCount: number; + getRelatedErrorsHref?: IWaterfallGetRelatedErrorsHref; +}) { + const { euiTheme } = useEuiTheme(); + + const viewRelatedErrorsLabel = i18n.translate( + 'xpack.apm.waterfall.embeddableRelatedErrors.errorCount', + { + defaultMessage: + '{errorCount, plural, one {View related error} other {View # related errors}}', + values: { errorCount }, + } + ); + + if (errorCount > 0 && getRelatedErrorsHref) { + return ( + { + e.stopPropagation(); + }} + tabIndex={0} + role="button" + aria-label={viewRelatedErrorsLabel} + onClickAriaLabel={viewRelatedErrorsLabel} + > + {viewRelatedErrorsLabel} + + ); } - return ; + + return ; } function RelatedErrors({ diff --git a/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/react_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/react_embeddable_factory.tsx index 392510f991bfd..e3eb2820eda62 100644 --- a/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/react_embeddable_factory.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/embeddable/trace_waterfall/react_embeddable_factory.tsx @@ -20,6 +20,7 @@ import type { EmbeddableDeps } from '../types'; import { APM_TRACE_WATERFALL_EMBEDDABLE } from './constant'; import { TraceWaterfallEmbeddable } from './trace_waterfall_embeddable'; import { FocusedTraceWaterfallEmbeddable } from './focused_trace_waterfall_embeddable'; +import type { IWaterfallGetRelatedErrorsHref } from '../../components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; interface BaseProps { traceId: string; @@ -37,6 +38,7 @@ export interface ApmTraceWaterfallEmbeddableEntryProps extends BaseProps, Serial displayLimit?: number; scrollElement?: Element; onNodeClick?: (nodeSpanId: string) => void; + getRelatedErrorsHref?: IWaterfallGetRelatedErrorsHref; } export type ApmTraceWaterfallEmbeddableProps = @@ -67,6 +69,9 @@ export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => { const onNodeClick$ = new BehaviorSubject( 'onNodeClick' in state ? state.onNodeClick : undefined ); + const getRelatedErrorsHref$ = new BehaviorSubject( + 'getRelatedErrorsHref' in state ? state.getRelatedErrorsHref : undefined + ); function serializeState() { return { @@ -81,6 +86,7 @@ export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => { docId: docId$.getValue(), scrollElement: scrollElement$.getValue(), onNodeClick: onNodeClick$.getValue(), + getRelatedErrorsHref: getRelatedErrorsHref$.getValue(), }, }; } @@ -99,7 +105,8 @@ export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => { displayLimit$, docId$, scrollElement$, - onNodeClick$ + onNodeClick$, + getRelatedErrorsHref$ ).pipe(map(() => undefined)), getComparators: () => { return { @@ -113,6 +120,7 @@ export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => { docId: 'referenceEquality', scrollElement: 'referenceEquality', onNodeClick: 'referenceEquality', + getRelatedErrorsHref: 'referenceEquality', }; }, onReset: (lastSaved) => { @@ -130,6 +138,7 @@ export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => { displayLimit$.next(entryState?.displayLimit ?? 0); scrollElement$.next(entryState?.scrollElement ?? undefined); onNodeClick$.next(entryState?.onNodeClick ?? undefined); + getRelatedErrorsHref$.next(entryState?.getRelatedErrorsHref ?? undefined); // reset focused state const focusedState = lastSaved?.rawState as ApmTraceWaterfallEmbeddableFocusedProps; @@ -156,6 +165,7 @@ export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => { docId, scrollElement, onNodeClick, + getRelatedErrorsHref, ] = useBatchedPublishingSubjects( serviceName$, traceId$, @@ -165,7 +175,8 @@ export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => { displayLimit$, docId$, scrollElement$, - onNodeClick$ + onNodeClick$, + getRelatedErrorsHref$ ); const content = isEmpty(docId) ? ( { displayLimit={displayLimit} onNodeClick={onNodeClick} scrollElement={scrollElement} + getRelatedErrorsHref={getRelatedErrorsHref} /> ) : ( onNodeClick?.(node.id)} isEmbeddable scrollElement={scrollElement} + getRelatedErrorsHref={getRelatedErrorsHref} />