[ObsUX] [APM] [OTel] Runtime metrics show dashboards with different ingest path (#211822)

Closes #211783
Part of https://github.com/elastic/kibana/issues/195857

## Summary

This PR expands the logic to get the dashboard files based on the agent.
We have many different ways to ingest data so we want to add more
metrics dashboards to the APM metrics tab. The different ingest paths we
have:
Classic APM Agent   --> APM Server       --> ES
Vanilla OTel SDKs     --> APM Server       --> ES
EDOT OTel SDKs      --> APM Server       --> ES
Classic APM Agent   --> EDOT Collector --> ES
Vanilla OTel SDKs.    --> EDOT Collector --> ES
EDOT OTel SDKs      --> EDOT Collector --> ES
We agreed on having a dashboard filename pattern to make showing the
correct dashboard easier described
[here](https://github.com/elastic/kibana/issues/195857#issue-2580733648)

First, we determine if the ingest path is through APM Server or EDOT
Collector by checking the `telemtry.sdk` fields.

## TODOs / Reviewer notes
- [ ] Currently, we have a fallback to metrics charts which is valid
only if we have APM agent so this PR adds an empty state message:
"Runtime metrics are not available for this Agent / SDK type." in case
there is no dashboard for the service language. To be improved in
https://github.com/elastic/kibana/issues/211774 and will be updated in
this PR when ready - I will still open it for review as the other logic
can be reviewed
- The dashboards are to be updated (by the agent team so not part of the
changes here)

## Testing: 
- Using e2e PoC 
- The available dashboard cases can be found in
[loadDashboardFile](91f169e19a/x-pack/solutions/observability/plugins/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts (L40))
- Cases to be checked:
- OTel native with Vanilla OTel SDKs with available dashboard (example
case file: `otel_native-otel_other-nodejs`, `...-java`, `...-dotnet`)
   
<img width="1903" alt="image"
src="https://github.com/user-attachments/assets/44d37b05-a8e7-4f14-a1de-2c631f1843bb"
/>

- APM server with Vanilla OTel SDKs service with available dashboard
(example case file: `classic_apm-otel_other-nodejs`, `...-java`,
`...-dotnet`)
   

![image](https://github.com/user-attachments/assets/caef88ea-2603-41ad-b815-f4c0c3647809)

- APM server with Classic APM Agent (example case file:
`classic_apm-apm-nodejs`, `...-java`)
   
<img width="962" alt="image"
src="https://github.com/user-attachments/assets/f9e96dce-55c8-467a-93f0-a09fa219597e"
/>

- OTel native with Vanilla OTel SDKs without available dashboard (empty
state case example: python service)
 

![image](https://github.com/user-attachments/assets/4cb6cca3-240e-422b-9288-701ef080f9cc)

- APM server with Vanilla OTel SDKs service without available dashboard
(empty state)
   
<img width="1910" alt="image"
src="https://github.com/user-attachments/assets/5219cf94-5013-4874-aaea-e558cca69281"
/>

- APM server with Classic APM Agent without available dashboard (Current
metrics fallback)
   
<img width="1914" alt="image"
src="https://github.com/user-attachments/assets/66342f49-876c-4ad5-a4d1-1414c3abac75"
/>

- ⚠️ OTel native Dashboards are still not available (at the time of
adding the description)

---------

Co-authored-by: Sergi Romeu <sergi.romeu@elastic.co>
Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
jennypavlova 2025-03-06 13:52:44 +01:00 committed by GitHub
parent 02c313ca06
commit f195570049
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 590 additions and 61 deletions

View file

@ -131,11 +131,13 @@ class Otel extends Serializable<OtelDocument> {
},
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,
},

View file

@ -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,

View file

@ -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);

View file

@ -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') ||

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", 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';

View file

@ -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);
});
});

View file

@ -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 };
};

View file

@ -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<CoreStart>);
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 (
<KibanaReactContext.Provider>
<MockApmPluginContextWrapper
history={history}
value={mockApmPluginContextValue as unknown as ApmPluginContextValue}
>
<Metrics />
</MockApmPluginContextWrapper>
</KibanaReactContext.Provider>
);
}
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(<MetricsWithWrapper />);
// 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(<MetricsWithWrapper />);
// 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(<MetricsWithWrapper />);
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(<MetricsWithWrapper />);
const apmAddApmCallout = result.getByTestId('apmAddApmCallout');
expect(apmAddApmCallout).toBeInTheDocument();
});
});
});
});

View file

@ -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 <ServerlessMetrics />;
}
const hasStaticDashboard = hasDashboardFile({
agentName,
runtimeName,
serverlessType,
});
if (!hasDashboardFile && !isElasticAgentName(agentName ?? '')) {
return (
<EuiCallOut
title={i18n.translate('xpack.apm.metrics.emptyState.title', {
defaultMessage: 'Runtime metrics are not available for this Agent / SDK type.',
})}
iconType="iInCircle"
data-test-subj="apmMetricsNoDashboardFound"
/>
);
}
if (hasStaticDashboard && dataView) {
if (hasDashboardFile && dataView) {
return (
<JsonMetricsDashboard
agentName={agentName}
@ -55,7 +67,7 @@ export function Metrics() {
);
}
if (!isAWSLambda && (isJavaAgentName(agentName) || isJRubyAgentName(agentName, runtimeName))) {
if (!isAWSLambda && isJRubyAgentName(agentName, runtimeName)) {
return <JvmMetricsOverview />;
}

View file

@ -5,52 +5,69 @@
* 2.0.
*/
export const AGENT_NAME_DASHBOARD_FILE_MAPPING: Record<string, string> = {
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<any> {
// 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: "lazyOtelDotnetDashboard" */
/* 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: "lazyDotnetApmOtelDashboard" */
'./opentelemetry_dotnet.json'
);
}

View file

@ -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);
}
);
});
});

View file

@ -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}`;
};

View file

@ -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<DashboardPanelMap | undefined> {
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;

View file

@ -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,

View file

@ -11,6 +11,8 @@ const INITIAL_STATE = {
agentName: undefined,
runtimeName: undefined,
serverlessType: undefined,
telemetrySdkName: undefined,
telemetrySdkLanguage: undefined,
};
export function useServiceAgentFetcher({

View file

@ -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<ServiceAgentResponse> {
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,
],
},
track_total_hits: 1,
size: 1,
@ -97,11 +108,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,
};

View file

@ -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',
});
});
});
});