diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/index.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/index.test.tsx deleted file mode 100644 index 3f229fe827559..0000000000000 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/index.test.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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 { isMetricsTabHidden, isInfraTabHidden } from '.'; -import { ServerlessType } from '../../../../../common/serverless'; - -describe('APM service template', () => { - describe('isMetricsTabHidden', () => { - describe('hides metrics tab', () => { - [ - { agentName: undefined }, - { agentName: 'js-base' }, - { agentName: 'rum-js' }, - { agentName: 'opentelemetry/webjs' }, - { serverlessType: ServerlessType.AWS_LAMBDA }, - { serverlessType: ServerlessType.AZURE_FUNCTIONS }, - ].map((input) => { - it(`when input ${JSON.stringify(input)}`, () => { - expect(isMetricsTabHidden(input)).toBeTruthy(); - }); - }); - }); - describe('shows metrics tab', () => { - [ - { agentName: 'ruby', runtimeName: 'ruby' }, - { agentName: 'ruby' }, - { agentName: 'dotnet' }, - { agentName: 'go' }, - { agentName: 'nodejs' }, - { agentName: 'php' }, - { agentName: 'python' }, - { agentName: 'ruby', runtimeName: 'jruby' }, - { agentName: 'java' }, - { agentName: 'opentelemetry/java' }, - ].map((input) => { - it(`when input ${JSON.stringify(input)}`, () => { - expect(isMetricsTabHidden(input)).toBeFalsy(); - }); - }); - }); - }); - describe('isInfraTabHidden', () => { - describe('hides infra tab', () => { - [ - { agentName: undefined, isInfraTabAvailable: true }, - { agentName: 'js-base', isInfraTabAvailable: true }, - { agentName: 'rum-js', isInfraTabAvailable: true }, - { agentName: 'opentelemetry/webjs', isInfraTabAvailable: true }, - { - serverlessType: ServerlessType.AWS_LAMBDA, - isInfraTabAvailable: true, - }, - { - serverlessType: ServerlessType.AZURE_FUNCTIONS, - isInfraTabAvailable: true, - }, - { agentName: 'nodejs', isInfraTabAvailable: false }, - ].map((input) => { - it(`when input ${JSON.stringify(input)}`, () => { - expect(isInfraTabHidden(input)).toBeTruthy(); - }); - }); - }); - describe('shows infra tab', () => { - [ - { agentName: 'ruby', runtimeName: 'ruby', isInfraTabAvailable: true }, - { agentName: 'ruby', runtimeName: 'jruby', isInfraTabAvailable: true }, - { agentName: 'ruby', isInfraTabAvailable: true }, - { agentName: 'dotnet', isInfraTabAvailable: true }, - { agentName: 'go', isInfraTabAvailable: true }, - { agentName: 'nodejs', isInfraTabAvailable: true }, - { agentName: 'php', isInfraTabAvailable: true }, - { agentName: 'python', isInfraTabAvailable: true }, - { agentName: 'java', isInfraTabAvailable: true }, - { agentName: 'opentelemetry/java', isInfraTabAvailable: true }, - ].map((input) => { - it(`when input ${JSON.stringify(input)}`, () => { - expect(isInfraTabHidden(input)).toBeFalsy(); - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/index.tsx index 16db47b6b4f21..6c2fdaea96687 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -5,67 +5,24 @@ * 2.0. */ -import { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingLogo, - EuiPageHeaderProps, - EuiSpacer, - EuiTitle, - EuiToolTip, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { enableAwsLambdaMetrics } from '@kbn/observability-plugin/common'; -import { omit } from 'lodash'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingLogo, EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import { useProfilingIntegrationSetting } from '../../../../hooks/use_profiling_integration_setting'; -import { - isAWSLambdaAgentName, - isAzureFunctionsAgentName, - isMobileAgentName, - isRumAgentName, - isRumOrMobileAgentName, - isServerlessAgentName, -} from '../../../../../common/agent_name'; -import { ApmFeatureFlagName } from '../../../../../common/apm_feature_flags'; -import { ServerlessType } from '../../../../../common/serverless'; -import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { isMobileAgentName } from '../../../../../common/agent_name'; import { ApmServiceContextProvider } from '../../../../context/apm_service/apm_service_context'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useBreadcrumb } from '../../../../context/breadcrumbs/use_breadcrumb'; import { ServiceAnomalyTimeseriesContextProvider } from '../../../../context/service_anomaly_timeseries/service_anomaly_timeseries_context'; -import { useApmFeatureFlag } from '../../../../hooks/use_apm_feature_flag'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { useApmRouter } from '../../../../hooks/use_apm_router'; -import { isPending, useFetcher } from '../../../../hooks/use_fetcher'; +import { isPending } from '../../../../hooks/use_fetcher'; import { useTimeRange } from '../../../../hooks/use_time_range'; -import { getAlertingCapabilities } from '../../../alerting/utils/get_alerting_capabilities'; -import { BetaBadge } from '../../../shared/beta_badge'; import { replace } from '../../../shared/links/url_helpers'; import { SearchBar } from '../../../shared/search_bar/search_bar'; import { ServiceIcons } from '../../../shared/service_icons'; -import { TechnicalPreviewBadge } from '../../../shared/technical_preview_badge'; import { ApmMainTemplate } from '../apm_main_template'; import { AnalyzeDataButton } from './analyze_data_button'; - -type Tab = NonNullable[0] & { - key: - | 'overview' - | 'transactions' - | 'dependencies' - | 'errors' - | 'metrics' - | 'nodes' - | 'infrastructure' - | 'service-map' - | 'logs' - | 'alerts' - | 'profiling' - | 'dashboards'; - hidden?: boolean; -}; +import { Tab, useTabs } from './use_tabs'; interface Props { title: string; @@ -167,236 +124,3 @@ function TemplateWithContext({ title, children, selectedTab, searchBarOptions }: ); } - -export function isMetricsTabHidden({ - agentName, - serverlessType, - isAwsLambdaEnabled, -}: { - agentName?: string; - serverlessType?: ServerlessType; - isAwsLambdaEnabled?: boolean; -}) { - if (isAWSLambdaAgentName(serverlessType)) { - return !isAwsLambdaEnabled; - } - return !agentName || isRumAgentName(agentName) || isAzureFunctionsAgentName(serverlessType); -} - -export function isInfraTabHidden({ - agentName, - serverlessType, - isInfraTabAvailable, -}: { - agentName?: string; - serverlessType?: ServerlessType; - isInfraTabAvailable: boolean; -}) { - return ( - !agentName || - isRumAgentName(agentName) || - isServerlessAgentName(serverlessType) || - !isInfraTabAvailable - ); -} - -function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { - const { agentName, serverlessType } = useApmServiceContext(); - const { core, plugins } = useApmPluginContext(); - const { capabilities } = core.application; - const { isAlertingAvailable, canReadAlerts } = getAlertingCapabilities(plugins, capabilities); - - const router = useApmRouter(); - const isInfraTabAvailable = useApmFeatureFlag(ApmFeatureFlagName.InfrastructureTabAvailable); - - const isProfilingIntegrationEnabled = useProfilingIntegrationSetting(); - - const isAwsLambdaEnabled = core.uiSettings.get(enableAwsLambdaMetrics, true); - - const { - path: { serviceName }, - query: queryFromUrl, - } = useApmParams(`/services/{serviceName}/${selectedTab}` as const); - - const { rangeFrom, rangeTo, environment } = queryFromUrl; - const { start, end } = useTimeRange({ rangeFrom, rangeTo }); - - const { data: serviceAlertsCount = { alertsCount: 0 } } = useFetcher( - (callApmApi) => { - return callApmApi('GET /internal/apm/services/{serviceName}/alerts_count', { - params: { - path: { - serviceName, - }, - query: { - start, - end, - environment, - }, - }, - }); - }, - [serviceName, start, end, environment] - ); - - const query = omit(queryFromUrl, 'page', 'pageSize', 'sortField', 'sortDirection'); - - const tabs: Tab[] = [ - { - key: 'overview', - href: router.link('/services/{serviceName}/overview', { - path: { serviceName }, - query, - }), - label: i18n.translate('xpack.apm.serviceDetails.overviewTabLabel', { - defaultMessage: 'Overview', - }), - }, - { - key: 'transactions', - href: router.link('/services/{serviceName}/transactions', { - path: { serviceName }, - query, - }), - label: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', { - defaultMessage: 'Transactions', - }), - }, - { - key: 'dependencies', - href: router.link('/services/{serviceName}/dependencies', { - path: { serviceName }, - query, - }), - label: i18n.translate('xpack.apm.serviceDetails.dependenciesTabLabel', { - defaultMessage: 'Dependencies', - }), - hidden: !agentName || isRumAgentName(agentName), - }, - { - key: 'errors', - href: router.link('/services/{serviceName}/errors', { - path: { serviceName }, - query, - }), - label: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', { - defaultMessage: 'Errors', - }), - }, - { - key: 'metrics', - href: router.link('/services/{serviceName}/metrics', { - path: { serviceName }, - query, - }), - label: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', { - defaultMessage: 'Metrics', - }), - append: isServerlessAgentName(serverlessType) && , - hidden: isMetricsTabHidden({ - agentName, - serverlessType, - isAwsLambdaEnabled, - }), - }, - { - key: 'infrastructure', - href: router.link('/services/{serviceName}/infrastructure', { - path: { serviceName }, - query, - }), - append: , - label: i18n.translate('xpack.apm.home.infraTabLabel', { - defaultMessage: 'Infrastructure', - }), - hidden: isInfraTabHidden({ - agentName, - serverlessType, - isInfraTabAvailable, - }), - }, - { - key: 'service-map', - href: router.link('/services/{serviceName}/service-map', { - path: { serviceName }, - query, - }), - label: i18n.translate('xpack.apm.home.serviceMapTabLabel', { - defaultMessage: 'Service Map', - }), - }, - { - key: 'logs', - href: router.link('/services/{serviceName}/logs', { - path: { serviceName }, - query, - }), - label: i18n.translate('xpack.apm.home.serviceLogsTabLabel', { - defaultMessage: 'Logs', - }), - append: isServerlessAgentName(serverlessType) && , - hidden: !agentName || isRumAgentName(agentName) || isAzureFunctionsAgentName(serverlessType), - }, - { - key: 'alerts', - href: router.link('/services/{serviceName}/alerts', { - path: { serviceName }, - query, - }), - append: - serviceAlertsCount.alertsCount > 0 ? ( - - {serviceAlertsCount.alertsCount} - - ) : null, - label: i18n.translate('xpack.apm.home.alertsTabLabel', { - defaultMessage: 'Alerts', - }), - hidden: !(isAlertingAvailable && canReadAlerts), - }, - { - key: 'profiling', - href: router.link('/services/{serviceName}/profiling', { - path: { serviceName }, - query, - }), - label: i18n.translate('xpack.apm.home.profilingTabLabel', { - defaultMessage: 'Universal Profiling', - }), - hidden: - !isProfilingIntegrationEnabled || - isRumOrMobileAgentName(agentName) || - isAWSLambdaAgentName(serverlessType), - }, - { - key: 'dashboards', - href: router.link('/services/{serviceName}/dashboards', { - path: { serviceName }, - query, - }), - label: i18n.translate('xpack.apm.home.dashboardsTabLabel', { - defaultMessage: 'Dashboards', - }), - append: , - }, - ]; - - return tabs - .filter((t) => !t.hidden) - .map(({ href, key, label, prepend, append }) => ({ - href, - label, - prepend, - append, - isSelected: key === selectedTab, - 'data-test-subj': `${key}Tab`, - })); -} diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/use_tabs.test.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/use_tabs.test.tsx new file mode 100644 index 0000000000000..26749e68afb6d --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/use_tabs.test.tsx @@ -0,0 +1,279 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { renderHook } from '@testing-library/react-hooks'; +import { createMemoryHistory } from 'history'; +import React, { ReactNode } from 'react'; +import { ServerlessType } from '../../../../../common/serverless'; +import { APIEndpoint } from '../../../../../server'; +import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context'; +import { + MockApmPluginContextWrapper, + mockApmPluginContextValue, +} from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import * as useApmServiceContext from '../../../../context/apm_service/use_apm_service_context'; +import { ServiceEntitySummary } from '../../../../context/apm_service/use_service_entity_summary_fetcher'; +import * as fetcherHook from '../../../../hooks/use_fetcher'; +import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { fromQuery } from '../../../shared/links/url_helpers'; +import { isInfraTabHidden, isMetricsTabHidden, useTabs } from './use_tabs'; + +jest.mock('../../../../hooks/use_profiling_integration_setting', () => ({ + useProfilingIntegrationSetting: () => true, +})); + +jest.mock('../../../alerting/utils/get_alerting_capabilities', () => ({ + getAlertingCapabilities: () => ({ isAlertingAvailable: true, canReadAlerts: true }), +})); + +const KibanaReactContext = createKibanaReactContext({ + settings: { client: { get: () => {} } }, +} as unknown as Partial); + +function wrapper({ children }: { children?: ReactNode }) { + const history = createMemoryHistory(); + history.replace({ + pathname: '/services/foo/overview', + search: fromQuery({ + rangeFrom: 'now-15m', + rangeTo: 'now', + }), + }); + + return ( + + + {children} + + + ); +} + +describe('APM service template', () => { + describe('isMetricsTabHidden', () => { + describe('hides metrics tab', () => { + [ + { agentName: undefined }, + { agentName: 'js-base' }, + { agentName: 'rum-js' }, + { agentName: 'opentelemetry/webjs' }, + { serverlessType: ServerlessType.AWS_LAMBDA }, + { serverlessType: ServerlessType.AZURE_FUNCTIONS }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isMetricsTabHidden(input)).toBeTruthy(); + }); + }); + }); + describe('shows metrics tab', () => { + [ + { agentName: 'ruby', runtimeName: 'ruby' }, + { agentName: 'ruby' }, + { agentName: 'dotnet' }, + { agentName: 'go' }, + { agentName: 'nodejs' }, + { agentName: 'php' }, + { agentName: 'python' }, + { agentName: 'ruby', runtimeName: 'jruby' }, + { agentName: 'java' }, + { agentName: 'opentelemetry/java' }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isMetricsTabHidden(input)).toBeFalsy(); + }); + }); + }); + }); + describe('isInfraTabHidden', () => { + describe('hides infra tab', () => { + [ + { agentName: undefined, isInfraTabAvailable: true }, + { agentName: 'js-base', isInfraTabAvailable: true }, + { agentName: 'rum-js', isInfraTabAvailable: true }, + { agentName: 'opentelemetry/webjs', isInfraTabAvailable: true }, + { + serverlessType: ServerlessType.AWS_LAMBDA, + isInfraTabAvailable: true, + }, + { + serverlessType: ServerlessType.AZURE_FUNCTIONS, + isInfraTabAvailable: true, + }, + { agentName: 'nodejs', isInfraTabAvailable: false }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isInfraTabHidden(input)).toBeTruthy(); + }); + }); + }); + describe('shows infra tab', () => { + [ + { agentName: 'ruby', runtimeName: 'ruby', isInfraTabAvailable: true }, + { agentName: 'ruby', runtimeName: 'jruby', isInfraTabAvailable: true }, + { agentName: 'ruby', isInfraTabAvailable: true }, + { agentName: 'dotnet', isInfraTabAvailable: true }, + { agentName: 'go', isInfraTabAvailable: true }, + { agentName: 'nodejs', isInfraTabAvailable: true }, + { agentName: 'php', isInfraTabAvailable: true }, + { agentName: 'python', isInfraTabAvailable: true }, + { agentName: 'java', isInfraTabAvailable: true }, + { agentName: 'opentelemetry/java', isInfraTabAvailable: true }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isInfraTabHidden(input)).toBeFalsy(); + }); + }); + }); + }); + + describe('useTabs order', () => { + const standardTabOrder = [ + 'Overview', + 'Transactions', + 'Dependencies', + 'Errors', + 'Metrics', + 'Infrastructure', + 'Service Map', + 'Logs', + 'Alerts', + 'Universal Profiling', + 'Dashboards', + ]; + const apisMockData: Partial> = { + 'GET /internal/apm/services/{serviceName}/alerts_count': { + data: { + alertsCount: 1, + }, + status: fetcherHook.FETCH_STATUS.SUCCESS, + refetch: jest.fn(), + }, + }; + + beforeEach(() => { + const callApmApi = () => (endpoint: APIEndpoint) => { + return apisMockData[endpoint]; + }; + jest.spyOn(fetcherHook, 'useFetcher').mockImplementation((func: Function, deps: string[]) => { + return func(callApmApi()) || {}; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('APM signal only', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'java', + serviceName: 'foo', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + signalTypes: ['metrics'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('keeps standard tab order', () => { + const { result } = renderHook(() => useTabs({ selectedTab: 'overview' }), { + wrapper, + }); + expect(result.current.map((tab) => tab.label)).toEqual(standardTabOrder); + }); + }); + + describe('APM and Logs signals', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'java', + serviceName: 'foo', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + signalTypes: ['metrics', 'logs'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('keeps standard tab order', () => { + const { result } = renderHook(() => useTabs({ selectedTab: 'overview' }), { + wrapper, + }); + expect(result.current.map((tab) => tab.label)).toEqual(standardTabOrder); + }); + }); + + describe('Non-Entity service', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'java', + serviceName: 'foo', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + }); + }); + + it('keeps standard tab order', () => { + const { result } = renderHook(() => useTabs({ selectedTab: 'overview' }), { + wrapper, + }); + expect(result.current.map((tab) => tab.label)).toEqual(standardTabOrder); + }); + }); + + describe('Logs signal only', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'java', + serviceName: 'foo', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + signalTypes: ['logs'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('Reorders Logs and Dashboard tabs', () => { + const { result } = renderHook(() => useTabs({ selectedTab: 'overview' }), { + wrapper, + }); + expect(result.current.map((tab) => tab.label)).toEqual([ + 'Overview', + 'Logs', + 'Dashboards', + 'Transactions', + 'Dependencies', + 'Errors', + 'Metrics', + 'Infrastructure', + 'Service Map', + 'Alerts', + 'Universal Profiling', + ]); + }); + }); + }); +}); diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/use_tabs.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/use_tabs.tsx new file mode 100644 index 0000000000000..09eda60db0bc2 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_service_template/use_tabs.tsx @@ -0,0 +1,321 @@ +/* + * 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 { EuiBadge, EuiPageHeaderProps, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { enableAwsLambdaMetrics } from '@kbn/observability-plugin/common'; +import { keyBy, omit } from 'lodash'; +import React from 'react'; +import { + isAWSLambdaAgentName, + isAzureFunctionsAgentName, + isRumAgentName, + isRumOrMobileAgentName, + isServerlessAgentName, +} from '../../../../../common/agent_name'; +import { ApmFeatureFlagName } from '../../../../../common/apm_feature_flags'; +import { SignalTypes } from '../../../../../common/entities/types'; +import { ServerlessType } from '../../../../../common/serverless'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { useApmFeatureFlag } from '../../../../hooks/use_apm_feature_flag'; +import { useApmParams } from '../../../../hooks/use_apm_params'; +import { useApmRouter } from '../../../../hooks/use_apm_router'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useProfilingIntegrationSetting } from '../../../../hooks/use_profiling_integration_setting'; +import { useTimeRange } from '../../../../hooks/use_time_range'; +import { isApmSignal, isLogsSignal } from '../../../../utils/get_signal_type'; +import { getAlertingCapabilities } from '../../../alerting/utils/get_alerting_capabilities'; +import { BetaBadge } from '../../../shared/beta_badge'; +import { TechnicalPreviewBadge } from '../../../shared/technical_preview_badge'; + +export type Tab = NonNullable[0] & { + key: + | 'overview' + | 'transactions' + | 'dependencies' + | 'errors' + | 'metrics' + | 'nodes' + | 'infrastructure' + | 'service-map' + | 'logs' + | 'alerts' + | 'profiling' + | 'dashboards'; + hidden?: boolean; +}; + +const apmOrderedTabs: Array = [ + 'overview', + 'transactions', + 'dependencies', + 'errors', + 'metrics', + 'infrastructure', + 'service-map', + 'logs', + 'alerts', + 'profiling', + 'dashboards', +]; +const logsOnlyOrderedTabs: Array = [ + 'overview', + 'logs', + 'dashboards', + 'transactions', + 'dependencies', + 'errors', + 'metrics', + 'infrastructure', + 'service-map', + 'alerts', + 'profiling', +]; + +export function isMetricsTabHidden({ + agentName, + serverlessType, + isAwsLambdaEnabled, +}: { + agentName?: string; + serverlessType?: ServerlessType; + isAwsLambdaEnabled?: boolean; +}) { + if (isAWSLambdaAgentName(serverlessType)) { + return !isAwsLambdaEnabled; + } + return !agentName || isRumAgentName(agentName) || isAzureFunctionsAgentName(serverlessType); +} + +export function isInfraTabHidden({ + agentName, + serverlessType, + isInfraTabAvailable, +}: { + agentName?: string; + serverlessType?: ServerlessType; + isInfraTabAvailable: boolean; +}) { + return ( + !agentName || + isRumAgentName(agentName) || + isServerlessAgentName(serverlessType) || + !isInfraTabAvailable + ); +} + +export function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { + const router = useApmRouter(); + const { agentName, serverlessType, serviceEntitySummary } = useApmServiceContext(); + const { core, plugins } = useApmPluginContext(); + const { capabilities } = core.application; + const isAwsLambdaEnabled = core.uiSettings.get(enableAwsLambdaMetrics, true); + const { isAlertingAvailable, canReadAlerts } = getAlertingCapabilities(plugins, capabilities); + const isInfraTabAvailable = useApmFeatureFlag(ApmFeatureFlagName.InfrastructureTabAvailable); + const isProfilingIntegrationEnabled = useProfilingIntegrationSetting(); + const { + path: { serviceName }, + query: queryFromUrl, + } = useApmParams(`/services/{serviceName}/${selectedTab}` as const); + const query = omit(queryFromUrl, 'page', 'pageSize', 'sortField', 'sortDirection'); + + const { rangeFrom, rangeTo, environment } = queryFromUrl; + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + + const { data: serviceAlertsCount = { alertsCount: 0 } } = useFetcher( + (callApmApi) => { + return callApmApi('GET /internal/apm/services/{serviceName}/alerts_count', { + params: { + path: { + serviceName, + }, + query: { + start, + end, + environment, + }, + }, + }); + }, + [serviceName, start, end, environment] + ); + + const allTabsDefinitions: Tab[] = [ + { + key: 'overview', + href: router.link('/services/{serviceName}/overview', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.serviceDetails.overviewTabLabel', { + defaultMessage: 'Overview', + }), + }, + { + key: 'transactions', + href: router.link('/services/{serviceName}/transactions', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', { + defaultMessage: 'Transactions', + }), + }, + { + key: 'dependencies', + href: router.link('/services/{serviceName}/dependencies', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.serviceDetails.dependenciesTabLabel', { + defaultMessage: 'Dependencies', + }), + hidden: !agentName || isRumAgentName(agentName), + }, + { + key: 'errors', + href: router.link('/services/{serviceName}/errors', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', { + defaultMessage: 'Errors', + }), + }, + { + key: 'metrics', + href: router.link('/services/{serviceName}/metrics', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', { + defaultMessage: 'Metrics', + }), + append: isServerlessAgentName(serverlessType) && , + hidden: isMetricsTabHidden({ + agentName, + serverlessType, + isAwsLambdaEnabled, + }), + }, + { + key: 'infrastructure', + href: router.link('/services/{serviceName}/infrastructure', { + path: { serviceName }, + query, + }), + append: , + label: i18n.translate('xpack.apm.home.infraTabLabel', { + defaultMessage: 'Infrastructure', + }), + hidden: isInfraTabHidden({ + agentName, + serverlessType, + isInfraTabAvailable, + }), + }, + { + key: 'service-map', + href: router.link('/services/{serviceName}/service-map', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.home.serviceMapTabLabel', { + defaultMessage: 'Service Map', + }), + }, + { + key: 'logs', + href: router.link('/services/{serviceName}/logs', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.home.serviceLogsTabLabel', { + defaultMessage: 'Logs', + }), + append: isServerlessAgentName(serverlessType) && , + hidden: !agentName || isRumAgentName(agentName) || isAzureFunctionsAgentName(serverlessType), + }, + { + key: 'alerts', + href: router.link('/services/{serviceName}/alerts', { + path: { serviceName }, + query, + }), + append: + serviceAlertsCount.alertsCount > 0 ? ( + + {serviceAlertsCount.alertsCount} + + ) : null, + label: i18n.translate('xpack.apm.home.alertsTabLabel', { + defaultMessage: 'Alerts', + }), + hidden: !(isAlertingAvailable && canReadAlerts), + }, + { + key: 'profiling', + href: router.link('/services/{serviceName}/profiling', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.home.profilingTabLabel', { + defaultMessage: 'Universal Profiling', + }), + + hidden: + !isProfilingIntegrationEnabled || + isRumOrMobileAgentName(agentName) || + isAWSLambdaAgentName(serverlessType), + }, + { + key: 'dashboards', + href: router.link('/services/{serviceName}/dashboards', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.home.dashboardsTabLabel', { + defaultMessage: 'Dashboards', + }), + append: , + }, + ]; + + const hasLogsSignal = + serviceEntitySummary?.signalTypes && + isLogsSignal(serviceEntitySummary.signalTypes as SignalTypes[]); + + const hasApmSignal = + serviceEntitySummary?.signalTypes && + isApmSignal(serviceEntitySummary.signalTypes as SignalTypes[]); + + const isLogsOnlyView = hasLogsSignal && !hasApmSignal; + + const tabsGroupedByKey = keyBy(allTabsDefinitions, 'key'); + const tabKeys = isLogsOnlyView ? logsOnlyOrderedTabs : apmOrderedTabs; + + return tabKeys + .map((key) => tabsGroupedByKey[key]) + .filter((t) => !t.hidden) + .map(({ href, key, label, prepend, append }) => ({ + href, + label, + prepend, + append, + isSelected: key === selectedTab, + 'data-test-subj': `${key}Tab`, + })); +} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_service_entity_summary.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/get_service_entity_summary.ts index 0ea5d27e68971..37e3ddef9ecc0 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_service_entity_summary.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/get_service_entity_summary.ts @@ -7,7 +7,7 @@ import { EntitiesESClient } from '../../lib/helpers/create_es_client/create_assets_es_client/create_assets_es_clients'; import { withApmSpan } from '../../utils/with_apm_span'; -import { getEntities } from './get_entities'; +import { getServiceLatestEntity } from './get_service_latest_entity'; import { ServiceEntities } from './types'; interface Params { @@ -18,15 +18,15 @@ interface Params { end: number; } -export async function getServiceEntitySummary({ +export function getServiceEntitySummary({ end, entitiesESClient, environment, serviceName, start, -}: Params): Promise { - return withApmSpan('get_service_entity_summary', async () => { - const entities = await getEntities({ +}: Params): Promise { + return withApmSpan('get_service_entity_summary', () => { + return getServiceLatestEntity({ end, entitiesESClient, environment, @@ -34,7 +34,5 @@ export async function getServiceEntitySummary({ start, serviceName, }); - - return entities[0]; }); } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/get_service_latest_entity.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/get_service_latest_entity.ts new file mode 100644 index 0000000000000..f4a4e4d1a81a4 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/get_service_latest_entity.ts @@ -0,0 +1,74 @@ +/* + * 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 { kqlQuery, termQuery } from '@kbn/observability-plugin/server'; +import { + AGENT_NAME, + DATA_STEAM_TYPE, + SERVICE_ENVIRONMENT, + SERVICE_NAME, +} from '../../../common/es_fields/apm'; +import { ENTITY, ENTITY_TYPE } from '../../../common/es_fields/entities'; +import { environmentQuery } from '../../../common/utils/environment_query'; +import { isFiniteNumber } from '../../../common/utils/is_finite_number'; +import { EntitiesESClient } from '../../lib/helpers/create_es_client/create_assets_es_client/create_assets_es_clients'; +import { entitiesRangeQuery } from './get_entities'; +import { EntitiesRaw, EntityType, ServiceEntities } from './types'; + +export async function getServiceLatestEntity({ + entitiesESClient, + start, + end, + environment, + kuery, + size, + serviceName, +}: { + entitiesESClient: EntitiesESClient; + start: number; + end: number; + environment: string; + kuery?: string; + size: number; + serviceName: string; +}): Promise { + const entities = ( + await entitiesESClient.searchLatest(`get_latest_entity`, { + body: { + size, + track_total_hits: false, + _source: [AGENT_NAME, ENTITY, DATA_STEAM_TYPE, SERVICE_NAME, SERVICE_ENVIRONMENT], + query: { + bool: { + filter: [ + ...kqlQuery(kuery), + ...environmentQuery(environment, SERVICE_ENVIRONMENT), + ...entitiesRangeQuery(start, end), + ...termQuery(ENTITY_TYPE, EntityType.SERVICE), + ...termQuery(SERVICE_NAME, serviceName), + ], + }, + }, + }, + }) + ).hits.hits.map((hit) => hit._source as EntitiesRaw); + + return entities.map((entity) => { + const logRate = entity.entity.metrics.logRate; + return { + serviceName: entity.service.name, + environment: Array.isArray(entity.service?.environment) + ? entity.service.environment[0] + : entity.service.environment, + agentName: entity.agent.name[0], + signalTypes: entity.data_stream.type, + entity: { + ...entity.entity, + hasLogMetrics: isFiniteNumber(logRate) ? logRate > 0 : false, + }, + }; + })?.[0]; +} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts index 0f52a9956af5b..027bd5bd09c98 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/services/routes.ts @@ -28,7 +28,7 @@ const serviceEntitiesSummaryRoute = createApmServerRoute({ query: t.intersection([environmentRt, rangeRt]), }), options: { tags: ['access:apm'] }, - async handler(resources): Promise { + async handler(resources): Promise { const { context, params, request } = resources; const coreContext = await context.core; diff --git a/x-pack/plugins/observability_solution/apm/server/routes/historical_data/route.ts b/x-pack/plugins/observability_solution/apm/server/routes/historical_data/route.ts index 489151704254a..484590a00a36e 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/historical_data/route.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/historical_data/route.ts @@ -15,17 +15,7 @@ const hasDataRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/has_data', options: { tags: ['access:apm'] }, handler: async (resources): Promise<{ hasData: boolean }> => { - const { context, request } = resources; - const coreContext = await context.core; - - const [apmEventClient] = await Promise.all([ - getApmEventClient(resources), - createEntitiesESClient({ - request, - esClient: coreContext.elasticsearch.client.asCurrentUser, - }), - ]); - + const apmEventClient = await getApmEventClient(resources); const hasData = await hasHistoricalAgentData(apmEventClient); return { hasData }; }, diff --git a/x-pack/plugins/observability_solution/apm/server/routes/services/route.ts b/x-pack/plugins/observability_solution/apm/server/routes/services/route.ts index b95c4a5384cb1..f80c58c19ed93 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/services/route.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/services/route.ts @@ -15,7 +15,7 @@ import { import { Annotation } from '@kbn/observability-plugin/common/annotations'; import { ScopedAnnotationsClient } from '@kbn/observability-plugin/server'; import * as t from 'io-ts'; -import { mergeWith, uniq } from 'lodash'; +import { isEmpty, mergeWith, uniq } from 'lodash'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { ServiceAnomalyTimeseries } from '../../../common/anomaly_detection/service_anomaly_timeseries'; import { offsetRt } from '../../../common/comparison_rt'; @@ -78,6 +78,9 @@ import { ServiceTransactionTypesResponse, } from './get_service_transaction_types'; import { getThroughput, ServiceThroughputResponse } from './get_throughput'; +import { getServiceEntitySummary } from '../entities/get_service_entity_summary'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { createEntitiesESClient } from '../../lib/helpers/create_es_client/create_assets_es_client/create_assets_es_clients'; const servicesRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/services', @@ -294,17 +297,39 @@ const serviceAgentRoute = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources): Promise => { - const apmEventClient = await getApmEventClient(resources); + const { context, request } = resources; + const coreContext = await context.core; + + const [apmEventClient, entitiesESClient] = await Promise.all([ + getApmEventClient(resources), + createEntitiesESClient({ + request, + esClient: coreContext.elasticsearch.client.asCurrentUser, + }), + ]); const { params } = resources; const { serviceName } = params.path; const { start, end } = params.query; - return getServiceAgent({ - serviceName, - apmEventClient, - start, - end, - }); + const [apmServiceAgent, serviceEntitySummary] = await Promise.all([ + getServiceAgent({ + serviceName, + apmEventClient, + start, + end, + }), + getServiceEntitySummary({ + end, + start, + serviceName, + entitiesESClient, + environment: ENVIRONMENT_ALL.value, + }), + ]); + + return isEmpty(apmServiceAgent) + ? { agentName: serviceEntitySummary?.agentName } + : apmServiceAgent; }, });