Skip to content

Commit

Permalink
[APM UI] Fix OpenTelemetry agent names (#193134)
Browse files Browse the repository at this point in the history
## Summary

Fixes #180444

This PR fixes the agent names not being able to properly be retrieved by
the APM UI, changing the way we map OpenTelemetry agent names.
As the format changed from `(opentelemetry|otlp)/{agentName}` to
`(opentelemetry|otlp)/{agentName}/{details}`, we now get the second part
splitting by `/`.

Added mappings for RUM, Android, and iOS OpenTelemetry client, also
fixed `get_service_metadata_details` to get the correct OpenTelemetry
details.

|Before|After|
|-|-|

|![image](https://github.com/user-attachments/assets/28732018-511b-44e0-ac86-cdbe7ed0d1e0)|![image](https://github.com/user-attachments/assets/45a29cc6-f939-4c52-bcc7-54dc15b1a403)|

## How to test
1. Checkout to this branch
2. Run `node scripts/synthtrace many_otel_services.ts --live --clean`
which will fill some APM Otel services.
3. Check that the icon is now rendering
  • Loading branch information
rmyz committed Sep 20, 2024
1 parent 0ddbe0e commit 735e216
Show file tree
Hide file tree
Showing 16 changed files with 551 additions and 39 deletions.
102 changes: 102 additions & 0 deletions packages/kbn-apm-synthtrace/src/scenarios/many_otel_services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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 { ApmFields, apm, Instance } from '@kbn/apm-synthtrace-client';
import { flatten, random, times } from 'lodash';
import { Scenario } from '../cli/scenario';
import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment';
import { withClient } from '../lib/utils/with_client';
import { getRandomNameForIndex } from './helpers/random_names';

const ENVIRONMENT = getSynthtraceEnvironment(__filename);

const scenario: Scenario<ApmFields> = async ({ logger, scenarioOpts = { services: 2000 } }) => {
const numServices = scenarioOpts.services;
const transactionName = 'GET /order/{id}';
const languages = [
'go',
'dotnet',
'java',
'python',
'nodejs',
'php',
'webjs',
'swift',
'android',
];
const agentVersions: Record<string, string[]> = {
go: ['2.1.0', '2.0.0', '1.15.0', '1.14.0', '1.13.1'],
dotnet: ['1.18.0', '1.17.0', '1.16.1', '1.16.0', '1.15.0'],
java: ['1.34.1', '1.34.0', '1.33.0', '1.32.0', '1.32.0'],
python: ['6.12.0', '6.11.0', '6.10.2', '6.10.1', '6.10.0'],
nodejs: ['1.34.1', '1.34.0', '1.33.0', '1.32.0', '1.32.0'],
php: ['1.34.1', '1.34.0', '1.33.0', '1.32.0', '1.32.0'],
webjs: ['6.12.0', '6.11.0', '6.10.2', '6.10.1', '6.10.0'],
swift: ['1.18.0', '1.17.0', '1.16.1', '1.16.0', '1.15.0'],
android: ['6.12.0', '6.11.0', '6.10.2', '6.10.1', '6.10.0'],
};

return {
generate: ({ range, clients: { apmEsClient } }) => {
const instances = flatten(
times(numServices).map((index) => {
const language = languages[index % languages.length];
const agentLanguageVersions = agentVersions[language];
const agentVersion = agentLanguageVersions[index % agentLanguageVersions.length];

const numOfInstances = (index % 3) + 1;
return times(numOfInstances).map((instanceIndex) =>
apm
.service({
name: `${getRandomNameForIndex(index)}-${language}-${index}`,
environment: ENVIRONMENT,
agentName:
index % 2 ? `opentelemetry/${language}/elastic` : `otlp/${language}/elastic`,
})
.instance(`instance-${index}-${instanceIndex}`)
.defaults({ 'agent.version': agentVersion, 'service.language.name': language })
);
})
);

const instanceSpans = (instance: Instance) => {
const hasHighDuration = Math.random() > 0.5;
const throughput = random(1, 10);

return range.ratePerMinute(throughput).generator((timestamp) => {
const parentDuration = hasHighDuration ? random(1000, 5000) : random(100, 1000);
const generateError = random(1, 4) % 3 === 0;
const span = instance
.transaction({ transactionName })
.timestamp(timestamp)
.duration(parentDuration);

return !generateError
? span.success()
: span.failure().errors(
instance
.error({
message: `No handler for ${transactionName}`,
type: 'No handler',
culprit: 'request',
})
.timestamp(timestamp + 50)
);
});
};

return withClient(
apmEsClient,
logger.perf('generating_apm_events', () => instances.map(instanceSpans))
);
},
};
};

export default scenario;
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ const examples = {
'opentelemetry/python': 'python',
'opentelemetry/ruby': 'ruby',
'opentelemetry/rust': 'rust',
'opentelemetry/cpp/elastic': 'cpp', // Tests for additional arguments on OpenTelemetry agents
'opentelemetry/dotnet/elastic': 'dotnet',
'opentelemetry/erlang/elastic': 'erlang',
'opentelemetry/go/elastic': 'go',
'opentelemetry/nodejs/elastic': 'nodejs',
'opentelemetry/php/elastic': 'php',
'opentelemetry/python/elastic': 'python',
'opentelemetry/ruby/elastic': 'ruby',
'opentelemetry/rust/elastic': 'rust',
opentelemetry: 'opentelemetry',
otlp: 'opentelemetry',
php: 'php',
python: 'python',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isIosAgentName,
isJavaAgentName,
isRumAgentName,
hasOpenTelemetryPrefix,
OpenTelemetryAgentName,
OPEN_TELEMETRY_AGENT_NAMES,
} from '@kbn/elastic-agent-utils';
Expand Down Expand Up @@ -66,6 +67,15 @@ const darkAgentIcons: { [key: string]: string } = {
rust: darkRustIcon,
};

const sanitizeAgentName = (agentName: string) => {
if (hasOpenTelemetryPrefix(agentName)) {
// for OpenTelemetry only split the agent name by `/` and take the second part, format is `(opentelemetry|otlp)/{agentName}/{details}`
return agentName.split('/')[1];
}

return agentName;
};

// This only needs to be exported for testing purposes, since we stub the SVG
// import values in test.
export function getAgentIconKey(agentName: string) {
Expand All @@ -90,11 +100,10 @@ export function getAgentIconKey(agentName: string) {
return 'android';
}

// Remove "opentelemetry/" prefix
const agentNameWithoutPrefix = lowercasedAgentName.replace(/^opentelemetry\//, '');
const cleanAgentName = sanitizeAgentName(lowercasedAgentName);

if (Object.keys(agentIcons).includes(agentNameWithoutPrefix)) {
return agentNameWithoutPrefix;
if (Object.keys(agentIcons).includes(cleanAgentName)) {
return cleanAgentName;
}

// OpenTelemetry-only agents
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-elastic-agent-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

export {
isOpenTelemetryAgentName,
hasOpenTelemetryPrefix,
isJavaAgentName,
isRumAgentName,
isMobileAgentName,
Expand Down
40 changes: 36 additions & 4 deletions packages/kbn-elastic-agent-utils/src/agent_guards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import {
hasOpenTelemetryPrefix,
isAndroidAgentName,
isAWSLambdaAgentName,
isAzureFunctionsAgentName,
Expand All @@ -22,23 +23,42 @@ import {
} from './agent_guards';

describe('Agents guards', () => {
it('hasOpenTelemetryPrefix should guard if the passed agent has an OpenTelemetry prefix.', () => {
expect(hasOpenTelemetryPrefix('otlp')).toBe(false);
expect(hasOpenTelemetryPrefix('otlp/nodejs')).toBe(true);
expect(hasOpenTelemetryPrefix('otlp/nodejs/elastic')).toBe(true);
expect(hasOpenTelemetryPrefix('opentelemetry')).toBe(false);
expect(hasOpenTelemetryPrefix('opentelemetry/nodejs')).toBe(true);
expect(hasOpenTelemetryPrefix('opentelemetry/nodejs/elastic')).toBe(true);
expect(hasOpenTelemetryPrefix('not-an-agent')).toBe(false);
});

it('isOpenTelemetryAgentName should guard if the passed agent is an OpenTelemetry one.', () => {
expect(isOpenTelemetryAgentName('otlp')).toBe(true);
expect(isOpenTelemetryAgentName('opentelemetry/java')).toBe(true);
expect(isOpenTelemetryAgentName('opentelemetry/java/opentelemetry-java-instrumentation')).toBe(
true
);
expect(isOpenTelemetryAgentName('otlp/nodejs')).toBe(true);
expect(isOpenTelemetryAgentName('otlp/nodejs/elastic')).toBe(true);
expect(isOpenTelemetryAgentName('opentelemetry')).toBe(true);
expect(isOpenTelemetryAgentName('opentelemetry/nodejs')).toBe(true);
expect(isOpenTelemetryAgentName('opentelemetry/nodejs/elastic')).toBe(true);
expect(isOpenTelemetryAgentName('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);
expect(isJavaAgentName('otlp/java/opentelemetry-java-instrumentation')).toBe(true);
expect(isJavaAgentName('opentelemetry/java')).toBe(true);
expect(isJavaAgentName('opentelemetry/java/opentelemetry-java-instrumentation')).toBe(true);
expect(isJavaAgentName('not-an-agent')).toBe(false);
});

it('isRumAgentName should guard if the passed agent is an Rum one.', () => {
expect(isRumAgentName('otlp/webjs')).toBe(true);
expect(isRumAgentName('otlp/webjs/elastic')).toBe(true);
expect(isRumAgentName('otlp/fail')).toBe(false);
expect(isRumAgentName('opentelemetry/webjs')).toBe(true);
expect(isRumAgentName('opentelemetry/webjs/elastic')).toBe(true);
expect(isRumAgentName('opentelemetry/fail')).toBe(false);
expect(isRumAgentName('rum-js')).toBe(true);
expect(isRumAgentName('not-an-agent')).toBe(false);
});
Expand All @@ -57,11 +77,23 @@ describe('Agents guards', () => {
});

it('isIosAgentName should guard if the passed agent is an Ios one.', () => {
expect(isIosAgentName('otlp/swift/elastic')).toBe(true);
expect(isIosAgentName('otlp/swift')).toBe(true);
expect(isIosAgentName('otlp/fail')).toBe(false);
expect(isIosAgentName('opentelemetry/swift/elastic')).toBe(true);
expect(isIosAgentName('opentelemetry/swift')).toBe(true);
expect(isIosAgentName('opentelemetry/fail')).toBe(false);
expect(isIosAgentName('ios/swift')).toBe(true);
expect(isIosAgentName('not-an-agent')).toBe(false);
});

it('isAndroidAgentName should guard if the passed agent is an Android one.', () => {
expect(isAndroidAgentName('otlp/android/elastic')).toBe(true);
expect(isAndroidAgentName('otlp/android')).toBe(true);
expect(isAndroidAgentName('otlp/fail')).toBe(false);
expect(isAndroidAgentName('opentelemetry/android/elastic')).toBe(true);
expect(isAndroidAgentName('opentelemetry/android')).toBe(true);
expect(isAndroidAgentName('opentelemetry/fail')).toBe(false);
expect(isAndroidAgentName('android/java')).toBe(true);
expect(isAndroidAgentName('not-an-agent')).toBe(false);
});
Expand Down
44 changes: 37 additions & 7 deletions packages/kbn-elastic-agent-utils/src/agent_guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,52 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { JAVA_AGENT_NAMES, OPEN_TELEMETRY_AGENT_NAMES, RUM_AGENT_NAMES } from './agent_names';
import {
ANDROID_AGENT_NAMES,
IOS_AGENT_NAMES,
JAVA_AGENT_NAMES,
OPEN_TELEMETRY_AGENT_NAMES,
RUM_AGENT_NAMES,
} from './agent_names';

import type {
AndroidAgentName,
IOSAgentName,
JavaAgentName,
OpenTelemetryAgentName,
RumAgentName,
ServerlessType,
} from './agent_names';

export function hasOpenTelemetryPrefix(agentName?: string, language: string = '') {
if (!agentName) {
return false;
}

return (
agentName.startsWith(`opentelemetry/${language}`) || agentName.startsWith(`otlp/${language}`)
);
}

export function isOpenTelemetryAgentName(agentName: string): agentName is OpenTelemetryAgentName {
return (
agentName?.startsWith('opentelemetry/') ||
hasOpenTelemetryPrefix(agentName) ||
OPEN_TELEMETRY_AGENT_NAMES.includes(agentName as OpenTelemetryAgentName)
);
}

export function isJavaAgentName(agentName?: string): agentName is JavaAgentName {
return (
agentName?.startsWith('opentelemetry/java') ||
hasOpenTelemetryPrefix(agentName, 'java') ||
JAVA_AGENT_NAMES.includes(agentName! as JavaAgentName)
);
}

export function isRumAgentName(agentName?: string): agentName is RumAgentName {
return RUM_AGENT_NAMES.includes(agentName! as RumAgentName);
return (
hasOpenTelemetryPrefix(agentName, 'webjs') ||
RUM_AGENT_NAMES.includes(agentName! as RumAgentName)
);
}

export function isMobileAgentName(agentName?: string) {
Expand All @@ -43,12 +64,21 @@ export function isRumOrMobileAgentName(agentName?: string) {
}

export function isIosAgentName(agentName?: string) {
return agentName?.toLowerCase() === 'ios/swift';
const lowercasedAgentName = agentName && agentName.toLowerCase();

return (
hasOpenTelemetryPrefix(lowercasedAgentName, 'swift') ||
IOS_AGENT_NAMES.includes(lowercasedAgentName! as IOSAgentName)
);
}

export function isAndroidAgentName(agentName?: string) {
const lowercased = agentName && agentName.toLowerCase();
return lowercased === 'android/java';
const lowercasedAgentName = agentName && agentName.toLowerCase();

return (
hasOpenTelemetryPrefix(lowercasedAgentName, 'android') ||
ANDROID_AGENT_NAMES.includes(lowercasedAgentName! as AndroidAgentName)
);
}

export function isJRubyAgentName(agentName?: string, runtimeName?: string) {
Expand Down
Loading

0 comments on commit 735e216

Please sign in to comment.