diff --git a/src/platform/packages/shared/kbn-apm-synthtrace-client/src/lib/otel/index.ts b/src/platform/packages/shared/kbn-apm-synthtrace-client/src/lib/otel/index.ts index 43d159a632203..7ef0ecfdac497 100644 --- a/src/platform/packages/shared/kbn-apm-synthtrace-client/src/lib/otel/index.ts +++ b/src/platform/packages/shared/kbn-apm-synthtrace-client/src/lib/otel/index.ts @@ -131,11 +131,13 @@ class Otel extends Serializable { }, resource: { attributes: { - 'agent.name': 'otlp', + 'agent.name': 'opentelemetry/nodejs', 'agent.version': '1.28.0', 'service.instance.id': '89117ac1-0dbf-4488-9e17-4c2c3b76943a', 'service.name': 'sendotlp-synth', 'metricset.interval': '10m', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.language': 'nodejs', }, dropped_attributes_count: 0, }, diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/index.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/index.ts index d92db7cd9489c..1b9db813d5891 100644 --- a/src/platform/packages/shared/kbn-elastic-agent-utils/index.ts +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/index.ts @@ -32,6 +32,10 @@ export { AGENT_NAMES, } from './src/agent_names'; +export { getIngestionPath } from './src/agent_ingestion_path'; + +export { getSdkNameAndLanguage } from './src/agent_sdk_name_and_language'; + export type { ElasticAgentName, OpenTelemetryAgentName, diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.test.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.test.ts index 1c9360649439b..388c2eebf843d 100644 --- a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.test.ts +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.test.ts @@ -13,6 +13,7 @@ import { isAndroidAgentName, isAWSLambdaAgentName, isAzureFunctionsAgentName, + isElasticAgentName, isIosAgentName, isJavaAgentName, isJRubyAgentName, @@ -44,6 +45,17 @@ describe('Agents guards', () => { expect(isOpenTelemetryAgentName('not-an-agent')).toBe(false); }); + it('isElasticAgentName should guard if the passed agent is an APM agent one.', () => { + expect(isElasticAgentName('nodejs')).toBe(true); + expect(isElasticAgentName('iOS/swift')).toBe(true); + expect(isElasticAgentName('java')).toBe(true); + expect(isElasticAgentName('rum-js')).toBe(true); + expect(isElasticAgentName('android/java')).toBe(true); + expect(isElasticAgentName('node-js')).toBe(false); + expect(isElasticAgentName('opentelemetry/nodejs/elastic')).toBe(false); + expect(isElasticAgentName('not-an-agent')).toBe(false); + }); + it('isJavaAgentName should guard if the passed agent is an Java one.', () => { expect(isJavaAgentName('java')).toBe(true); expect(isJavaAgentName('otlp/java')).toBe(true); diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.ts index ea9d112d9630b..eda96c9853c5d 100644 --- a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.ts +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_guards.ts @@ -9,6 +9,7 @@ import { ANDROID_AGENT_NAMES, + ELASTIC_AGENT_NAMES, IOS_AGENT_NAMES, JAVA_AGENT_NAMES, OPEN_TELEMETRY_AGENT_NAMES, @@ -17,6 +18,7 @@ import { import type { AndroidAgentName, + ElasticAgentName, IOSAgentName, JavaAgentName, OpenTelemetryAgentName, @@ -24,6 +26,8 @@ import type { ServerlessType, } from './agent_names'; +const ElasticAgentNamesSet = new Set(ELASTIC_AGENT_NAMES); + export function getAgentName( agentName: string | null, telemetryAgentName: string | null, @@ -57,6 +61,9 @@ export function isOpenTelemetryAgentName(agentName: string): agentName is OpenTe ); } +export const isElasticAgentName = (agentName: string): agentName is ElasticAgentName => + ElasticAgentNamesSet.has(agentName as ElasticAgentName); + export function isJavaAgentName(agentName?: string): agentName is JavaAgentName { return ( hasOpenTelemetryPrefix(agentName, 'java') || diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_ingestion_path.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_ingestion_path.ts new file mode 100644 index 0000000000000..a41a84792ef67 --- /dev/null +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_ingestion_path.ts @@ -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", 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 const getIngestionPath = (hasOpenTelemetryFields: boolean) => + hasOpenTelemetryFields ? 'otel_native' : 'classic_apm'; diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.test.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.test.ts new file mode 100644 index 0000000000000..9ec09740dcd87 --- /dev/null +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { getSdkNameAndLanguage } from './agent_sdk_name_and_language'; + +describe('getSdkNameAndLanguage', () => { + it.each([ + { + agentName: 'java', + result: { sdkName: 'apm', language: 'java' }, + }, + { + agentName: 'iOS/swift', + result: { sdkName: 'apm', language: 'iOS/swift' }, + }, + { + agentName: 'android/java', + result: { sdkName: 'apm', language: 'android/java' }, + }, + { + agentName: 'opentelemetry/java/test/elastic', + result: { sdkName: 'edot', language: 'java' }, + }, + { + agentName: 'opentelemetry/java/elastic', + result: { sdkName: 'edot', language: 'java' }, + }, + { + agentName: 'otlp/nodejs', + result: { sdkName: 'otel_other', language: 'nodejs' }, + }, + { + agentName: 'otlp', + result: { sdkName: 'otel_other', language: undefined }, + }, + { + agentName: 'test/test/test/something-else/elastic', + result: { sdkName: undefined, language: undefined }, + }, + { + agentName: 'test/java/test/something-else/', + result: { sdkName: undefined, language: undefined }, + }, + { + agentName: 'elastic', + result: { sdkName: undefined, language: undefined }, + }, + { + agentName: 'my-awesome-agent/otel', + result: { sdkName: undefined, language: undefined }, + }, + ])('for the agent name $agentName returns $result', ({ agentName, result }) => { + expect(getSdkNameAndLanguage(agentName)).toStrictEqual(result); + }); +}); diff --git a/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.ts b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.ts new file mode 100644 index 0000000000000..1cffd32bcab56 --- /dev/null +++ b/src/platform/packages/shared/kbn-elastic-agent-utils/src/agent_sdk_name_and_language.ts @@ -0,0 +1,36 @@ +/* + * 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 { isElasticAgentName, isOpenTelemetryAgentName } from './agent_guards'; + +interface SdkNameAndLanguage { + sdkName?: 'apm' | 'edot' | 'otel_other'; + language?: string; +} + +const LANGUAGE_INDEX = 1; + +export const getSdkNameAndLanguage = (agentName: string): SdkNameAndLanguage => { + if (isElasticAgentName(agentName)) { + return { sdkName: 'apm', language: agentName }; + } + const agentNameParts = agentName.split('/'); + + if (isOpenTelemetryAgentName(agentName)) { + if (agentNameParts[agentNameParts.length - 1] === 'elastic') { + return { sdkName: 'edot', language: agentNameParts[LANGUAGE_INDEX] }; + } + return { + sdkName: 'otel_other', + language: agentNameParts[LANGUAGE_INDEX], + }; + } + + return { sdkName: undefined, language: undefined }; +}; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.test.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.test.tsx new file mode 100644 index 0000000000000..d89409c021f55 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.test.tsx @@ -0,0 +1,155 @@ +/* + * 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 type { CoreStart } from '@kbn/core/public'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { render } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import React from 'react'; +import type { 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 type { ServiceEntitySummary } from '../../../context/apm_service/use_service_entity_summary_fetcher'; +import * as useApmDataViewHook from '../../../hooks/use_adhoc_apm_data_view'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { fromQuery } from '../../shared/links/url_helpers'; +import { Metrics } from '.'; +import type { DataView } from '@kbn/data-views-plugin/common'; + +const KibanaReactContext = createKibanaReactContext({ + settings: { client: { get: () => {} } }, +} as unknown as Partial); + +function MetricsWithWrapper() { + jest + .spyOn(useApmDataViewHook, 'useAdHocApmDataView') + .mockReturnValue({ dataView: { id: 'id-1', name: 'apm-data-view' } as DataView }); + + const history = createMemoryHistory(); + history.replace({ + pathname: '/services/testServiceName/metrics', + search: fromQuery({ + rangeFrom: 'now-15m', + rangeTo: 'now', + }), + }); + + return ( + + + + + + ); +} + +describe('Metrics', () => { + describe('render the correct metrics content for', () => { + describe('APM agent / server service', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'java', + serviceName: 'testServiceName', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + dataStreamTypes: ['metrics'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('shows java dashboard content', () => { + const result = render(); + // Check that the other content is not rendered as we don't have test id in the dashboard rendering component + const loadingBar = result.queryByRole('progressbar'); + expect(loadingBar).toBeNull(); + expect(result.queryByTestId('apmMetricsNoDashboardFound')).toBeNull(); + expect(result.queryByTestId('apmAddApmCallout')).toBeNull(); + }); + }); + + describe('APM agent / EDOT sdk with dashboard', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'opentelemetry/nodejs/elastic', + serviceName: 'testServiceName', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + dataStreamTypes: ['metrics'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('shows nodejs dashboard content', () => { + const result = render(); + // Check that the other content is not rendered as we don't have test id in the dashboard rendering component + const loadingBar = result.queryByRole('progressbar'); + expect(loadingBar).toBeNull(); + expect(result.queryByTestId('apmMetricsNoDashboardFound')).toBeNull(); + expect(result.queryByTestId('apmAddApmCallout')).toBeNull(); + }); + }); + + describe('APM agent / otel sdk with no dashboard', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'opentelemetry/go', + serviceName: 'testServiceName', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + dataStreamTypes: ['metrics'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('shows "no dashboard found" message', () => { + const result = render(); + const apmMetricsNoDashboardFound = result.getByTestId('apmMetricsNoDashboardFound'); + expect(apmMetricsNoDashboardFound).toBeInTheDocument(); + }); + }); + + describe('Logs signals', () => { + beforeEach(() => { + jest.spyOn(useApmServiceContext, 'useApmServiceContext').mockReturnValue({ + agentName: 'java', + serviceName: 'testServiceName', + transactionTypeStatus: FETCH_STATUS.SUCCESS, + transactionTypes: [], + fallbackToTransactions: true, + serviceAgentStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummaryStatus: FETCH_STATUS.SUCCESS, + serviceEntitySummary: { + dataStreamTypes: ['logs'], + } as unknown as ServiceEntitySummary, + }); + }); + + it('shows service from logs metrics content', () => { + const result = render(); + const apmAddApmCallout = result.getByTestId('apmAddApmCallout'); + expect(apmAddApmCallout).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.tsx b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.tsx index e89bffeaf5b5d..06fdee55d4cfe 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/index.tsx @@ -6,27 +6,33 @@ */ import React from 'react'; -import { - isJavaAgentName, - isJRubyAgentName, - isAWSLambdaAgentName, -} from '../../../../common/agent_name'; +import { isElasticAgentName, isJRubyAgentName } from '@kbn/elastic-agent-utils/src/agent_guards'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isAWSLambdaAgentName } from '../../../../common/agent_name'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { ServerlessMetrics } from './serverless_metrics'; import { ServiceMetrics } from './service_metrics'; -import { JvmMetricsOverview } from './jvm_metrics_overview'; import { JsonMetricsDashboard } from './static_dashboard'; -import { hasDashboardFile } from './static_dashboard/helper'; +import { hasDashboard } from './static_dashboard/helper'; import { useAdHocApmDataView } from '../../../hooks/use_adhoc_apm_data_view'; import { isLogsOnlySignal } from '../../../utils/get_signal_type'; import { ServiceTabEmptyState } from '../service_tab_empty_state'; +import { JvmMetricsOverview } from './jvm_metrics_overview'; export function Metrics() { - const { agentName, runtimeName, serverlessType } = useApmServiceContext(); + const { + agentName, + runtimeName, + serverlessType, + serviceEntitySummary, + telemetrySdkName, + telemetrySdkLanguage, + } = useApmServiceContext(); const isAWSLambda = isAWSLambdaAgentName(serverlessType); const { dataView } = useAdHocApmDataView(); - const { serviceEntitySummary } = useApmServiceContext(); + const hasDashboardFile = hasDashboard({ agentName, telemetrySdkName, telemetrySdkLanguage }); const hasLogsOnlySignal = serviceEntitySummary?.dataStreamTypes && isLogsOnlySignal(serviceEntitySummary.dataStreamTypes); @@ -38,13 +44,19 @@ export function Metrics() { return ; } - const hasStaticDashboard = hasDashboardFile({ - agentName, - runtimeName, - serverlessType, - }); + if (!hasDashboardFile && !isElasticAgentName(agentName ?? '')) { + return ( + + ); + } - if (hasStaticDashboard && dataView) { + if (hasDashboardFile && dataView) { return ( ; } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts index ea3c468a6c829..59ba771dea86c 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts @@ -5,52 +5,69 @@ * 2.0. */ -export const AGENT_NAME_DASHBOARD_FILE_MAPPING: Record = { - nodejs: 'nodejs', - 'opentelemetry/nodejs': 'opentelemetry_nodejs', - 'opentelemetry/nodejs/elastic': 'opentelemetry_nodejs', - java: 'java', - 'opentelemetry/java': 'opentelemetry_java', - 'opentelemetry/java/opentelemetry-java-instrumentation': 'opentelemetry_java', - 'opentelemetry/java/elastic': 'opentelemetry_java', - 'opentelemetry/dotnet': 'opentelemetry_dotnet', - 'opentelemetry/dotnet/opentelemetry-dotnet-instrumentation': 'opentelemetry_dotnet', - 'opentelemetry/dotnet/elastic': 'opentelemetry_dotnet', -}; +// The new dashboard file names should be added here +export const existingDashboardFileNames = new Set([ + 'classic_apm-apm-nodejs', + 'classic_apm-apm-java', + 'classic_apm-otel_other-nodejs', + 'classic_apm-otel_other-java', + 'classic_apm-otel_other-dotnet', + 'classic_apm-edot-nodejs', + 'classic_apm-edot-java', + 'classic_apm-edot-dotnet', +]); -/** - * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. - * See https://webpack.js.org/api/module-methods/#magic-comments - */ -export async function loadDashboardFile(filename: string): Promise { +// The new dashboard files should be mapped here +// + changed with the new ones (following the naming convention) +// + similar mapping for edot needed +// - example: otel_native-edot-nodejs +export async function loadDashboardFile(filename: string) { switch (filename) { - case 'nodejs': { + case 'classic_apm-apm-nodejs': { return import( - /* webpackChunkName: "lazyNodeJsDashboard" */ + /* webpackChunkName: "lazyNodeJsClassicApmDashboard" */ './nodejs.json' ); } - case 'opentelemetry_nodejs': { + case 'classic_apm-otel_other-nodejs': { return import( - /* webpackChunkName: "lazyNodeJsDashboard" */ + /* webpackChunkName: "lazyNodeJsApmOtelDashboard" */ './opentelemetry_nodejs.json' ); } - case 'java': { + case 'classic_apm-edot-nodejs': { return import( - /* webpackChunkName: "lazyJavaDashboard" */ + /* webpackChunkName: "lazyNodeJsOtelNativeDashboard" */ + './opentelemetry_nodejs.json' + ); + } + case 'classic_apm-apm-java': { + return import( + /* webpackChunkName: "lazyJavaClassicApmDashboard" */ './java.json' ); } - case 'opentelemetry_java': { + case 'classic_apm-otel_other-java': { return import( - /* webpackChunkName: "lazyJavaDashboard" */ + /* webpackChunkName: "lazyJavaApmOtelDashboard" */ './opentelemetry_java.json' ); } - case 'opentelemetry_dotnet': { + case 'classic_apm-edot-java': { + return import( + /* webpackChunkName: "lazyJavaOtelNativeDashboard" */ + './opentelemetry_java.json' + ); + } + case 'classic_apm-edot-dotnet': { + return import( + /* webpackChunkName: "lazyDotnetOtelNativeDashboard" */ + './opentelemetry_dotnet.json' + ); + } + case 'classic_apm-otel_other-dotnet': { return import( - /* webpackChunkName: "lazyOtelDotnetDashboard" */ + /* webpackChunkName: "lazyDotnetApmOtelDashboard" */ './opentelemetry_dotnet.json' ); } diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.test.ts b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.test.ts new file mode 100644 index 0000000000000..5043ff98ac855 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { getDashboardFileName } from './get_dashboard_file_name'; + +const apmAgent = [ + { + agentName: 'java', + telemetrySdkName: undefined, + telemetrySdkLanguage: undefined, + filename: 'classic_apm-apm-java', + }, + { + agentName: 'iOS/swift', + telemetrySdkName: undefined, + telemetrySdkLanguage: undefined, + filename: 'classic_apm-apm-ios_swift', + }, + { + agentName: 'java', + telemetrySdkName: 'opentelemetry', + filename: 'otel_native-apm-java', + }, +]; +const edotSdk = [ + { + agentName: 'opentelemetry/java/test/elastic', + filename: 'classic_apm-edot-java', + }, + { + agentName: 'opentelemetry/java/elastic', + filename: 'classic_apm-edot-java', + }, + { + agentName: 'opentelemetry/java/test/elastic', + filename: 'classic_apm-edot-java', + }, + { + agentName: 'opentelemetry/java/elastic', + filename: 'classic_apm-edot-java', + }, + { + agentName: 'opentelemetry/java/elastic', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'java', + filename: 'otel_native-edot-java', + }, + { + agentName: 'opentelemetry/nodejs/nodejs-agent/elastic', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'nodejs', + filename: 'otel_native-edot-nodejs', + }, +]; +const vanillaOtelSdk = [ + { + agentName: 'opentelemetry/java', + filename: 'classic_apm-otel_other-java', + }, + { + agentName: 'opentelemetry/nodejs/test/nodejs-agent', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'nodejs', + filename: 'otel_native-otel_other-nodejs', + }, + { + agentName: 'opentelemetry/java/test/something-else/', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'java', + filename: 'otel_native-otel_other-java', + }, + { + agentName: 'otlp/nodejs', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'nodejs', + filename: 'otel_native-otel_other-nodejs', + }, + { + agentName: 'otlp/Android', + telemetrySdkName: 'opentelemetry', + telemetrySdkLanguage: 'android', + filename: 'otel_native-otel_other-android', + }, +]; +const noFilenameCases = [ + { + agentName: 'test/java/test/something-else/', + telemetrySdkName: undefined, + telemetrySdkLanguage: undefined, + filename: undefined, + }, + { + agentName: 'otlp', + filename: undefined, + }, + { + agentName: 'elastic', + filename: undefined, + }, + { + agentName: 'my-awesome-agent/otel', + telemetrySdkName: 'opentelemetry', + filename: undefined, + }, +]; + +describe('getDashboardFileName', () => { + describe('apmAgent', () => { + it.each(apmAgent)( + 'for the agent name $agentName and open telemetry sdk name: $telemetrySdkName returns $filename', + ({ agentName, telemetrySdkName, telemetrySdkLanguage, filename }) => { + expect( + getDashboardFileName({ agentName, telemetrySdkName, telemetrySdkLanguage }) + ).toStrictEqual(filename); + } + ); + }); + describe('vanillaOtelSdk', () => { + it.each(vanillaOtelSdk)( + 'for the agent name $agentName and open telemetry sdk name: $telemetrySdkName and language $telemetrySdkLanguage returns $filename', + ({ agentName, telemetrySdkName, telemetrySdkLanguage, filename }) => { + expect( + getDashboardFileName({ agentName, telemetrySdkName, telemetrySdkLanguage }) + ).toStrictEqual(filename); + } + ); + }); + describe('edotSdk', () => { + it.each(edotSdk)( + 'for the agent name $agentName and open telemetry sdk name: $telemetrySdkName and language $telemetrySdkLanguage returns $filename', + ({ agentName, telemetrySdkName, telemetrySdkLanguage, filename }) => { + expect( + getDashboardFileName({ agentName, telemetrySdkName, telemetrySdkLanguage }) + ).toStrictEqual(filename); + } + ); + }); + describe('noFilenameCases', () => { + it.each(noFilenameCases)( + 'for the agent name $agentName and open telemetry sdk name: $telemetrySdkName and language $telemetrySdkLanguage returns $filename', + ({ agentName, telemetrySdkName, telemetrySdkLanguage, filename }) => { + expect( + getDashboardFileName({ agentName, telemetrySdkName, telemetrySdkLanguage }) + ).toStrictEqual(filename); + } + ); + }); +}); diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.ts b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.ts new file mode 100644 index 0000000000000..30debf725fe88 --- /dev/null +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/get_dashboard_file_name.ts @@ -0,0 +1,33 @@ +/* + * 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 { getSdkNameAndLanguage, getIngestionPath } from '@kbn/elastic-agent-utils'; + +interface DashboardFileNamePartsProps { + agentName: string; + telemetrySdkName?: string; + telemetrySdkLanguage?: string; +} + +// We use the language name in the filename so we want to have a valid filename +// Example swift/iOS -> swift_ios : lowercased and '/' is replaces by '_' +const standardizeLanguageName = (languageName?: string) => + languageName ? languageName.toLowerCase().replace('/', '_') : undefined; + +export const getDashboardFileName = ({ + agentName, + telemetrySdkName, + telemetrySdkLanguage, +}: DashboardFileNamePartsProps): string | undefined => { + const dataFormat = getIngestionPath(!!(telemetrySdkName ?? telemetrySdkLanguage)); + const { sdkName, language } = getSdkNameAndLanguage(agentName); + const sdkLanguage = standardizeLanguageName(language); + if (!sdkName || !sdkLanguage) { + return undefined; + } + return `${dataFormat}-${sdkName}-${sdkLanguage}`; +}; diff --git a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts index 2674034cd3d7e..a2b92f2b6a6ff 100644 --- a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts +++ b/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/helper.ts @@ -7,28 +7,33 @@ import type { DataView } from '@kbn/data-views-plugin/common'; import type { DashboardPanelMap } from '@kbn/dashboard-plugin/common'; -import { - AGENT_NAME_DASHBOARD_FILE_MAPPING, - loadDashboardFile, -} from './dashboards/dashboard_catalog'; - +import { existingDashboardFileNames, loadDashboardFile } from './dashboards/dashboard_catalog'; +import { getDashboardFileName } from './dashboards/get_dashboard_file_name'; interface DashboardFileProps { agentName?: string; runtimeName?: string; serverlessType?: string; + telemetrySdkName?: string; + telemetrySdkLanguage?: string; } export interface MetricsDashboardProps extends DashboardFileProps { dataView: DataView; } -export function hasDashboardFile(props: DashboardFileProps) { - return !!getDashboardFileName(props); +function getDashboardFileNameFromProps({ + agentName, + telemetrySdkName, + telemetrySdkLanguage, +}: DashboardFileProps) { + const dashboardFile = + agentName && getDashboardFileName({ agentName, telemetrySdkName, telemetrySdkLanguage }); + return dashboardFile; } -function getDashboardFileName({ agentName }: DashboardFileProps) { - const dashboardFile = agentName && AGENT_NAME_DASHBOARD_FILE_MAPPING[agentName]; - return dashboardFile; +export function hasDashboard(props: DashboardFileProps) { + const dashboardFilename = getDashboardFileNameFromProps(props); + return !!dashboardFilename && existingDashboardFileNames.has(dashboardFilename); } const getAdhocDataView = (dataView: DataView) => { @@ -43,10 +48,8 @@ export async function convertSavedDashboardToPanels( props: MetricsDashboardProps, dataView: DataView ): Promise { - const dashboardFilename = getDashboardFileName(props); - const dashboardJSON = !!dashboardFilename - ? await loadDashboardFile(dashboardFilename) - : undefined; + const dashboardFilename = getDashboardFileNameFromProps(props); + const dashboardJSON = !!dashboardFilename ? await loadDashboardFile(dashboardFilename) : false; if (!dashboardFilename || !dashboardJSON) { return undefined; diff --git a/x-pack/solutions/observability/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/solutions/observability/plugins/apm/public/context/apm_service/apm_service_context.tsx index e0c09c5756a4e..3f4ae1a2fa1d2 100644 --- a/x-pack/solutions/observability/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/solutions/observability/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -28,6 +28,8 @@ import { export interface APMServiceContextValue { serviceName: string; agentName?: string; + telemetrySdkName?: string; + telemetrySdkLanguage?: string; serverlessType?: ServerlessType; transactionType?: string; transactionTypeStatus: FETCH_STATUS; @@ -63,6 +65,8 @@ export function ApmServiceContextProvider({ children }: { children: ReactNode }) agentName, runtimeName, serverlessType, + telemetrySdkName, + telemetrySdkLanguage, status: serviceAgentStatus, } = useServiceAgentFetcher({ serviceName, @@ -108,6 +112,8 @@ export function ApmServiceContextProvider({ children }: { children: ReactNode }) serviceName, agentName, serverlessType, + telemetrySdkName, + telemetrySdkLanguage, transactionType: currentTransactionType, transactionTypeStatus, transactionTypes, diff --git a/x-pack/solutions/observability/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts b/x-pack/solutions/observability/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts index a7d22e24e1a72..909ba6b36bdaa 100644 --- a/x-pack/solutions/observability/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts +++ b/x-pack/solutions/observability/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts @@ -11,6 +11,8 @@ const INITIAL_STATE = { agentName: undefined, runtimeName: undefined, serverlessType: undefined, + telemetrySdkName: undefined, + telemetrySdkLanguage: undefined, }; export function useServiceAgentFetcher({ diff --git a/x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_agent.ts b/x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_agent.ts index 72c15d2ca66ae..75503530f969f 100644 --- a/x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_agent.ts +++ b/x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_agent.ts @@ -15,6 +15,8 @@ import { SERVICE_RUNTIME_NAME, CLOUD_PROVIDER, CLOUD_SERVICE_NAME, + TELEMETRY_SDK_NAME, + TELEMETRY_SDK_LANGUAGE, } from '../../../common/es_fields/apm'; import type { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import type { ServerlessType } from '../../../common/serverless'; @@ -24,6 +26,8 @@ import { maybe } from '../../../common/utils/maybe'; export interface ServiceAgentResponse { agentName?: string; runtimeName?: string; + telemetrySdkName?: string; + telemetrySdkLanguage?: string; serverlessType?: ServerlessType; } @@ -40,6 +44,8 @@ export async function getServiceAgent({ }): Promise { const fields = asMutableArray([ AGENT_NAME, + TELEMETRY_SDK_NAME, + TELEMETRY_SDK_LANGUAGE, SERVICE_RUNTIME_NAME, CLOUD_PROVIDER, CLOUD_SERVICE_NAME, @@ -48,7 +54,12 @@ export async function getServiceAgent({ const params = { terminate_after: 1, apm: { - events: [ProcessorEvent.error, ProcessorEvent.transaction, ProcessorEvent.metric], + events: [ + ProcessorEvent.span, + ProcessorEvent.error, + ProcessorEvent.transaction, + ProcessorEvent.metric, + ], }, body: { track_total_hits: 1, @@ -99,11 +110,13 @@ export async function getServiceAgent({ const event = unflattenKnownApmEventFields(hit.fields); - const { agent, service, cloud } = event; + const { agent, service, cloud, telemetry } = event; const serverlessType = getServerlessTypeFromCloudData(cloud?.provider, cloud?.service?.name); return { agentName: agent?.name, + telemetrySdkName: telemetry?.sdk?.name, + telemetrySdkLanguage: telemetry?.sdk?.language, runtimeName: service?.runtime?.name, serverlessType, }; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/agent.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/agent.spec.ts index 8d96b00867e80..22953fced1fee 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/agent.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/services/agent.spec.ts @@ -58,7 +58,10 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon expect(response.status).to.be(200); - expect(response.body).to.eql({ agentName: 'nodejs', runtimeName: 'node' }); + expect(response.body).to.eql({ + agentName: 'nodejs', + runtimeName: 'node', + }); }); }); });