From bbecbdf3c1f446b2623c4090c3ef26277dab23b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Fri, 12 Jan 2024 13:04:36 +0100 Subject: [PATCH 1/2] [APM] Synthtrace improvements --- .../src/scenarios/helpers/random_names.ts | 139 ++++++++++++++++++ .../src/scenarios/many_errors.ts | 53 +++++++ .../src/scenarios/many_instances.ts | 91 ++++++++++++ .../src/scenarios/many_services.ts | 71 +++------ ...ns_per_service.ts => many_transactions.ts} | 59 +++++--- 5 files changed, 340 insertions(+), 73 deletions(-) create mode 100644 packages/kbn-apm-synthtrace/src/scenarios/helpers/random_names.ts create mode 100644 packages/kbn-apm-synthtrace/src/scenarios/many_errors.ts create mode 100644 packages/kbn-apm-synthtrace/src/scenarios/many_instances.ts rename packages/kbn-apm-synthtrace/src/scenarios/{many_transactions_per_service.ts => many_transactions.ts} (62%) diff --git a/packages/kbn-apm-synthtrace/src/scenarios/helpers/random_names.ts b/packages/kbn-apm-synthtrace/src/scenarios/helpers/random_names.ts new file mode 100644 index 0000000000000..14ea4f778a5c4 --- /dev/null +++ b/packages/kbn-apm-synthtrace/src/scenarios/helpers/random_names.ts @@ -0,0 +1,139 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export const randomNames = [ + 'Adonis', + 'Agamemnon', + 'Ajax', + 'Alexander', + 'Aphrodite', + 'Apollo', + 'Ares', + 'Aristophanes', + 'Artemis', + 'Athena', + 'Atlas', + 'Boreas', + 'Calliope', + 'Cassandra', + 'Clio', + 'Daphne', + 'Demeter', + 'Dionysius', + 'Dionysus', + 'Eileithyia', + 'Electra', + 'Eos', + 'Erato', + 'Erebos', + 'Eros', + 'Euterpe', + 'Gaia', + 'Hades', + 'Hecate', + 'Helios', + 'Hephaestus', + 'Hera', + 'Heracles (Hercules)', + 'Hercules', + 'Hermes', + 'Hestia', + 'Hypatia', + 'Hypnos', + 'Iphigenia', + 'Iris', + 'Kratos', + 'Leonidas', + 'Medusa', + 'Mnemosyne', + 'Morpheus', + 'Narcissus', + 'Nemesis', + 'Nike', + 'Notus', + 'Nyx', + 'Oceanus', + 'Odysseus', + 'Orpheus', + 'Pan', + 'Pandora', + 'Penelope', + 'Pericles', + 'Persephone', + 'Perseus', + 'Phoebe', + 'Polyphemus', + 'Pontus', + 'Poseidon', + 'Priam', + 'Prometheus', + 'Rhea', + 'Sappho', + 'Selene', + 'Terpsichore', + 'Tethys', + 'Thalia', + 'Thanatos', + 'Theia', + 'Theseus', + 'Thetis', + 'Triton', + 'Tyche', + 'Uranus', + 'Zephyrus', + 'Zeus', + 'Augustus', + 'Bacchus', + 'Brutus', + 'Caesar', + 'Caligula', + 'Caracalla', + 'Cassius', + 'Cato', + 'Cicero', + 'Cleopatra', + 'Commodus', + 'Constantine', + 'Diana', + 'Domitian', + 'Hadrian', + 'Horace', + 'Julius', + 'Juno', + 'Jupiter', + 'Livy', + 'Marcus Aurelius', + 'Mars', + 'Mercury', + 'Minerva', + 'Neptune', + 'Nero', + 'Octavian', + 'Ovid', + 'Pliny', + 'Pluto', + 'Pompey', + 'Remus', + 'Romulus', + 'Saturn', + 'Scipio', + 'Seneca', + 'Spartacus', + 'Theodosius', + 'Tiberius', + 'Titus', + 'Trajan', + 'Venus', + 'Vespasian', + 'Vesta', + 'Virgil', +]; + +export function getRandomNameForIndex(index: number) { + return randomNames[index % randomNames.length]; +} diff --git a/packages/kbn-apm-synthtrace/src/scenarios/many_errors.ts b/packages/kbn-apm-synthtrace/src/scenarios/many_errors.ts new file mode 100644 index 0000000000000..7948688610f56 --- /dev/null +++ b/packages/kbn-apm-synthtrace/src/scenarios/many_errors.ts @@ -0,0 +1,53 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ +import { ApmFields, apm } from '@kbn/apm-synthtrace-client'; +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 = async (runOptions) => { + const { logger } = runOptions; + + const severities = ['critical', 'error', 'warning', 'info', 'debug', 'trace']; + + return { + generate: ({ range, clients: { apmEsClient } }) => { + const transactionName = 'DELETE /api/orders/{id}'; + + const instance = apm + .service({ name: `synth-node`, environment: ENVIRONMENT, agentName: 'nodejs' }) + .instance('instance'); + + const failedTraceEvents = range + .interval('1m') + .rate(2000) + .generator((timestamp, index) => { + const severity = severities[index % severities.length]; + const errorMessage = `${severity}: ${getRandomNameForIndex(index)} ${index}`; + return instance + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(1000) + .failure() + .errors( + instance.error({ message: errorMessage, type: 'My Type' }).timestamp(timestamp + 50) + ); + }); + + return withClient( + apmEsClient, + logger.perf('generating_apm_events', () => failedTraceEvents) + ); + }, + }; +}; + +export default scenario; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/many_instances.ts b/packages/kbn-apm-synthtrace/src/scenarios/many_instances.ts new file mode 100644 index 0000000000000..ee216d7b4af1a --- /dev/null +++ b/packages/kbn-apm-synthtrace/src/scenarios/many_instances.ts @@ -0,0 +1,91 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { ApmFields, apm, Instance } from '@kbn/apm-synthtrace-client'; +import { 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 { randomNames } from './helpers/random_names'; + +const ENVIRONMENT = getSynthtraceEnvironment(__filename); + +const scenario: Scenario = async ({ logger, scenarioOpts = { instances: 2000 } }) => { + const numInstances = scenarioOpts.instances; + const serviceNames = randomNames; + const agentVersions = ['2.1.0', '2.0.0', '1.15.0', '1.14.0', '1.13.1']; + const language = 'go'; + const serviceName = 'synth-many-instances'; + const transactionName = 'GET /order/{id}'; + + return { + generate: ({ range, clients: { apmEsClient } }) => { + const instances = times(numInstances).map((index) => { + const agentVersion = agentVersions[index % agentVersions.length]; + const randomName = serviceNames[index % serviceNames.length]; + return apm + .service({ + name: serviceName, + environment: ENVIRONMENT, + agentName: language, + }) + .instance(`instance-${randomName}-${index}`) + .defaults({ 'agent.version': agentVersion, 'service.language.name': language }); + }); + + const instanceSpans = (instance: Instance) => { + const hasHighDuration = Math.random() > 0.5; + const throughput = random(1, 10); + + const traces = 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}` }) + .timestamp(timestamp + 50) + ); + }); + + const cpuPct = random(0, 1); + const memoryFree = random(0, 1000); + const metricsets = range + .interval('30s') + .rate(1) + .generator((timestamp) => + instance + .appMetrics({ + 'system.memory.actual.free': memoryFree, + 'system.memory.total': 1000, + 'system.cpu.total.norm.pct': cpuPct, + 'system.process.cpu.total.norm.pct': 0.7, + }) + .timestamp(timestamp) + ); + + return [traces, metricsets]; + }; + + return withClient( + apmEsClient, + logger.perf('generating_apm_events', () => instances.flatMap(instanceSpans)) + ); + }, + }; +}; + +export default scenario; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts b/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts index 68a5432f1a29a..1b048b10a35d6 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts @@ -7,17 +7,19 @@ */ import { ApmFields, apm, Instance } from '@kbn/apm-synthtrace-client'; -import { flatten, random } from 'lodash'; +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 { randomNames } from './helpers/random_names'; const ENVIRONMENT = getSynthtraceEnvironment(__filename); -const scenario: Scenario = async ({ logger, scenarioOpts = { services: 500 } }) => { +const scenario: Scenario = async ({ logger, scenarioOpts = { services: 2000 } }) => { const numServices = scenarioOpts.services; + const transactionName = 'GET /order/{id}'; const languages = ['go', 'dotnet', 'java', 'python']; - const services = ['web', 'order-processing', 'api-backend', 'proxy']; + const serviceNames = randomNames; const agentVersions: Record = { 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'], @@ -27,82 +29,53 @@ const scenario: Scenario = async ({ logger, scenarioOpts = { services return { generate: ({ range, clients: { apmEsClient } }) => { - const successfulTimestamps = range.ratePerMinute(180); - const instances = flatten( - [...Array(numServices).keys()].map((index) => { + 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 [...Array(numOfInstances).keys()].map((instanceIndex) => + return times(numOfInstances).map((instanceIndex) => apm .service({ - name: `${services[index % services.length]}-${language}-${index}`, + name: `${serviceNames[index % serviceNames.length]}-${language}-${index}`, environment: ENVIRONMENT, agentName: language, }) .instance(`instance-${index}-${instanceIndex}`) - .defaults({ - 'agent.version': agentLanguageVersions[index % agentLanguageVersions.length], - 'service.language.name': language, - }) + .defaults({ 'agent.version': agentVersion, 'service.language.name': language }) ); }) ); - const urls = ['GET /order/{id}', 'POST /basket/{id}', 'DELETE /basket', 'GET /products']; + const instanceSpans = (instance: Instance) => { + const hasHighDuration = Math.random() > 0.5; + const throughput = random(1, 10); - const instanceSpans = (instance: Instance, url: string) => { - const successfulTraceEvents = successfulTimestamps.generator((timestamp) => { - const randomHigh = random(1000, 4000); - const randomLow = random(100, randomHigh / 5); - const duration = random(randomLow, randomHigh); - const childDuration = random(randomLow, duration); - const remainderDuration = duration - childDuration; + return range.ratePerMinute(throughput).generator((timestamp) => { + const parentDuration = hasHighDuration ? random(1000, 5000) : random(100, 1000); const generateError = random(1, 4) % 3 === 0; - const generateChildError = random(0, 5) % 2 === 0; const span = instance - .transaction({ transactionName: url }) + .transaction({ transactionName }) .timestamp(timestamp) - .duration(duration) - .children( - instance - .span({ - spanName: 'GET apm-*/_search', - spanType: 'db', - spanSubtype: 'elasticsearch', - }) - .duration(childDuration) - .destination('elasticsearch') - .timestamp(timestamp) - .outcome(generateError && generateChildError ? 'failure' : 'success'), - instance - .span({ spanName: 'custom_operation', spanType: 'custom' }) - .duration(remainderDuration) - .success() - .timestamp(timestamp + childDuration) - ); + .duration(parentDuration); + return !generateError ? span.success() : span .failure() .errors( - instance.error({ message: `No handler for ${url}` }).timestamp(timestamp + 50) + instance + .error({ message: `No handler for ${transactionName}` }) + .timestamp(timestamp + 50) ); }); - - return successfulTraceEvents; }; return withClient( apmEsClient, - logger.perf('generating_apm_events', () => - instances - .flatMap((instance) => urls.map((url) => ({ instance, url }))) - .map(({ instance, url }) => instanceSpans(instance, url)) - ) + logger.perf('generating_apm_events', () => instances.map(instanceSpans)) ); }, }; diff --git a/packages/kbn-apm-synthtrace/src/scenarios/many_transactions_per_service.ts b/packages/kbn-apm-synthtrace/src/scenarios/many_transactions.ts similarity index 62% rename from packages/kbn-apm-synthtrace/src/scenarios/many_transactions_per_service.ts rename to packages/kbn-apm-synthtrace/src/scenarios/many_transactions.ts index 565ac8ea4f1ab..895d413235512 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/many_transactions_per_service.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/many_transactions.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ import { ApmFields, apm, Instance } from '@kbn/apm-synthtrace-client'; +import { random, times } from 'lodash'; import { Scenario } from '../cli/scenario'; import { getSynthtraceEnvironment } from '../lib/utils/get_synthtrace_environment'; import { withClient } from '../lib/utils/with_client'; @@ -14,36 +15,48 @@ const ENVIRONMENT = getSynthtraceEnvironment(__filename); const scenario: Scenario = async (runOptions) => { const { logger } = runOptions; - const { numServices = 3 } = runOptions.scenarioOpts || {}; - const numTransactions = 100; + const { numServices = 1 } = runOptions.scenarioOpts || {}; + const numTransactions = 2000; + + const transactionNames = ['GET', 'PUT', 'DELETE', 'UPDATE'].flatMap((method) => + [ + '/users', + '/products', + '/orders', + '/customers', + '/profile', + '/categories', + '/invoices', + '/payments', + '/cart', + '/reviews', + ].map((resource) => `${method} ${resource}`) + ); return { generate: ({ range, clients: { apmEsClient } }) => { - const urls = ['GET /order', 'POST /basket', 'DELETE /basket', 'GET /products']; - - const successfulTimestamps = range.ratePerMinute(180); - const failedTimestamps = range.interval('1m').rate(180); - - const instances = [...Array(numServices).keys()].map((index) => + const instances = times(numServices).map((index) => apm .service({ name: `synth-go-${index}`, environment: ENVIRONMENT, agentName: 'go' }) .instance(`instance-${index}`) ); - const transactionNames = [...Array(numTransactions).keys()].map( - (index) => `${urls[index % urls.length]}/${index}` - ); - const instanceSpans = (instance: Instance, transactionName: string) => { - const successfulTraceEvents = successfulTimestamps.generator((timestamp) => - instance.transaction({ transactionName }).timestamp(timestamp).duration(1000).success() - ); + const successfulTraceEvents = range + .ratePerMinute(random(1, 60)) + .generator((timestamp) => + instance + .transaction({ transactionName }) + .timestamp(timestamp) + .duration(random(100, 10_000)) + .success() + ); - const failedTraceEvents = failedTimestamps.generator((timestamp) => + const failedTraceEvents = range.ratePerMinute(random(1, 60)).generator((timestamp) => instance .transaction({ transactionName }) .timestamp(timestamp) - .duration(1000) + .duration(random(100, 10_000)) .failure() .errors( instance @@ -72,13 +85,11 @@ const scenario: Scenario = async (runOptions) => { return withClient( apmEsClient, logger.perf('generating_apm_events', () => - instances - .flatMap((instance) => - transactionNames.map((transactionName) => ({ instance, transactionName })) - ) - .flatMap(({ instance, transactionName }, index) => - instanceSpans(instance, transactionName) - ) + instances.flatMap((instance) => + times(numTransactions) + .map((index) => `${transactionNames[index % transactionNames.length]}-${index}`) + .flatMap((transactionName) => instanceSpans(instance, transactionName)) + ) ) ); }, From a6b9413365ecea9ad0365d8eaf60d6116415b545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Fri, 12 Jan 2024 14:45:31 +0100 Subject: [PATCH 2/2] Address feedback --- .../kbn-apm-synthtrace/src/scenarios/helpers/random_names.ts | 4 ++-- packages/kbn-apm-synthtrace/src/scenarios/many_instances.ts | 5 ++--- packages/kbn-apm-synthtrace/src/scenarios/many_services.ts | 5 ++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/kbn-apm-synthtrace/src/scenarios/helpers/random_names.ts b/packages/kbn-apm-synthtrace/src/scenarios/helpers/random_names.ts index 14ea4f778a5c4..cb03428a8d032 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/helpers/random_names.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/helpers/random_names.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -export const randomNames = [ +export const randomGreekishNames = [ 'Adonis', 'Agamemnon', 'Ajax', @@ -135,5 +135,5 @@ export const randomNames = [ ]; export function getRandomNameForIndex(index: number) { - return randomNames[index % randomNames.length]; + return randomGreekishNames[index % randomGreekishNames.length]; } diff --git a/packages/kbn-apm-synthtrace/src/scenarios/many_instances.ts b/packages/kbn-apm-synthtrace/src/scenarios/many_instances.ts index ee216d7b4af1a..4774839c71727 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/many_instances.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/many_instances.ts @@ -11,13 +11,12 @@ import { 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 { randomNames } from './helpers/random_names'; +import { getRandomNameForIndex } from './helpers/random_names'; const ENVIRONMENT = getSynthtraceEnvironment(__filename); const scenario: Scenario = async ({ logger, scenarioOpts = { instances: 2000 } }) => { const numInstances = scenarioOpts.instances; - const serviceNames = randomNames; const agentVersions = ['2.1.0', '2.0.0', '1.15.0', '1.14.0', '1.13.1']; const language = 'go'; const serviceName = 'synth-many-instances'; @@ -27,7 +26,7 @@ const scenario: Scenario = async ({ logger, scenarioOpts = { instance generate: ({ range, clients: { apmEsClient } }) => { const instances = times(numInstances).map((index) => { const agentVersion = agentVersions[index % agentVersions.length]; - const randomName = serviceNames[index % serviceNames.length]; + const randomName = getRandomNameForIndex(index); return apm .service({ name: serviceName, diff --git a/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts b/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts index 1b048b10a35d6..1af9f2f9e3b5a 100644 --- a/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts +++ b/packages/kbn-apm-synthtrace/src/scenarios/many_services.ts @@ -11,7 +11,7 @@ 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 { randomNames } from './helpers/random_names'; +import { getRandomNameForIndex } from './helpers/random_names'; const ENVIRONMENT = getSynthtraceEnvironment(__filename); @@ -19,7 +19,6 @@ const scenario: Scenario = async ({ logger, scenarioOpts = { services const numServices = scenarioOpts.services; const transactionName = 'GET /order/{id}'; const languages = ['go', 'dotnet', 'java', 'python']; - const serviceNames = randomNames; const agentVersions: Record = { 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'], @@ -39,7 +38,7 @@ const scenario: Scenario = async ({ logger, scenarioOpts = { services return times(numOfInstances).map((instanceIndex) => apm .service({ - name: `${serviceNames[index % serviceNames.length]}-${language}-${index}`, + name: `${getRandomNameForIndex(index)}-${language}-${index}`, environment: ENVIRONMENT, agentName: language, })