packages/opentelemetry-node/lib/instrumentations.js (191 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and contributors * SPDX-License-Identifier: Apache-2.0 */ const { getStringListFromEnv, getBooleanFromEnv, } = require('@opentelemetry/core'); const {log} = require('./logging'); /** * @typedef {import('@opentelemetry/instrumentation').Instrumentation} Instrumentation * * @typedef {{ * "@elastic/opentelemetry-instrumentation-openai": import('@elastic/opentelemetry-instrumentation-openai').OpenAIInstrumentationConfig, * "@opentelemetry/instrumentation-amqplib": import('@opentelemetry/instrumentation-amqplib').AmqplibInstrumentation, * "@opentelemetry/instrumentation-aws-sdk": import('@opentelemetry/instrumentation-aws-sdk').AwsSdkInstrumentationConfig, * "@opentelemetry/instrumentation-bunyan": import('@opentelemetry/instrumentation-bunyan').BunyanInstrumentationConfig, * "@opentelemetry/instrumentation-connect": import('@opentelemetry/instrumentation').InstrumentationConfig, * "@opentelemetry/instrumentation-cassandra-driver": import('@opentelemetry/instrumentation-cassandra-driver').CassandraDriverInstrumentation, * "@opentelemetry/instrumentation-cucumber": import('@opentelemetry/instrumentation-cucumber').CucumberInstrumentationConfig, * "@opentelemetry/instrumentation-dataloader": import('@opentelemetry/instrumentation-dataloader').DataloaderInstrumentationConfig, * "@opentelemetry/instrumentation-dns": import('@opentelemetry/instrumentation-dns').DnsInstrumentationConfig, * "@opentelemetry/instrumentation-express": import('@opentelemetry/instrumentation-express').ExpressInstrumentationConfig, * "@opentelemetry/instrumentation-fastify": import('@opentelemetry/instrumentation-fastify').FastifyInstrumentationConfig, * "@opentelemetry/instrumentation-fs": import('@opentelemetry/instrumentation-fs').FsInstrumentationConfig, * "@opentelemetry/instrumentation-generic-pool": import('@opentelemetry/instrumentation').InstrumentationConfig, * "@opentelemetry/instrumentation-graphql": import('@opentelemetry/instrumentation-graphql').GraphQLInstrumentation, * "@opentelemetry/instrumentation-grpc": import('@opentelemetry/instrumentation-grpc').GrpcInstrumentationConfig, * "@opentelemetry/instrumentation-hapi": import('@opentelemetry/instrumentation').InstrumentationConfig, * "@opentelemetry/instrumentation-http": import('@opentelemetry/instrumentation-http').HttpInstrumentationConfig, * "@opentelemetry/instrumentation-ioredis": import('@opentelemetry/instrumentation-ioredis').IORedisInstrumentationConfig, * "@opentelemetry/instrumentation-kafkajs": import('@opentelemetry/instrumentation-kafkajs').KafkaJsInstrumentation, * "@opentelemetry/instrumentation-knex": import('@opentelemetry/instrumentation-knex').KnexInstrumentationConfig, * "@opentelemetry/instrumentation-koa": import('@opentelemetry/instrumentation-koa').KoaInstrumentationConfig, * "@opentelemetry/instrumentation-lru-memoizer": import('@opentelemetry/instrumentation').InstrumentationConfig, * "@opentelemetry/instrumentation-memcached": import('@opentelemetry/instrumentation-memcached').InstrumentationConfig, * "@opentelemetry/instrumentation-mongodb": import('@opentelemetry/instrumentation-mongodb').MongoDBInstrumentationConfig, * "@opentelemetry/instrumentation-mongoose": import('@opentelemetry/instrumentation-mongoose').MongooseInstrumentationConfig, * "@opentelemetry/instrumentation-mysql": import('@opentelemetry/instrumentation-mysql').MySQLInstrumentation, * "@opentelemetry/instrumentation-mysql2": import('@opentelemetry/instrumentation-mysql2').MySQL2Instrumentation, * "@opentelemetry/instrumentation-nestjs-core": import('@opentelemetry/instrumentation').InstrumentationConfig, * "@opentelemetry/instrumentation-net": import('@opentelemetry/instrumentation').InstrumentationConfig, * "@opentelemetry/instrumentation-pg": import('@opentelemetry/instrumentation-pg').PgInstrumentationConfig * "@opentelemetry/instrumentation-pino": import('@opentelemetry/instrumentation-pino').PinoInstrumentationConfig * "@opentelemetry/instrumentation-redis": import('@opentelemetry/instrumentation-redis').RedisInstrumentationConfig, * "@opentelemetry/instrumentation-redis-4": import('@opentelemetry/instrumentation-redis-4').RedisInstrumentationConfig, * "@opentelemetry/instrumentation-restify": import('@opentelemetry/instrumentation-restify').RestifyInstrumentationConfig, * "@opentelemetry/instrumentation-router": import('@opentelemetry/instrumentation').InstrumentationConfig, * "@opentelemetry/instrumentation-runtime-node": import('@opentelemetry/instrumentation-runtime-node').RuntimeNodeInstrumentationConfig, * "@opentelemetry/instrumentation-socket.io": import('@opentelemetry/instrumentation-socket.io').SocketIoInstrumentationConfig, * "@opentelemetry/instrumentation-tedious": import('@opentelemetry/instrumentation-tedious').TediousInstrumentationConfig, * "@opentelemetry/instrumentation-undici": import('@opentelemetry/instrumentation-undici').UndiciInstrumentationConfig, * "@opentelemetry/instrumentation-winston": import('@opentelemetry/instrumentation-winston').WinstonInstrumentationConfig, * }} InstrumentaionsMap */ /* eslint-disable prettier/prettier */ const {OpenAIInstrumentation} = require('@elastic/opentelemetry-instrumentation-openai'); const {AwsInstrumentation} = require('@opentelemetry/instrumentation-aws-sdk'); const {AmqplibInstrumentation} = require('@opentelemetry/instrumentation-amqplib'); const {BunyanInstrumentation} = require('@opentelemetry/instrumentation-bunyan'); const {ConnectInstrumentation} = require('@opentelemetry/instrumentation-connect'); const {CassandraDriverInstrumentation} = require('@opentelemetry/instrumentation-cassandra-driver'); const {CucumberInstrumentation} = require('@opentelemetry/instrumentation-cucumber'); const {DataloaderInstrumentation} = require('@opentelemetry/instrumentation-dataloader'); const {DnsInstrumentation} = require('@opentelemetry/instrumentation-dns'); const {ExpressInstrumentation} = require('@opentelemetry/instrumentation-express'); const {FsInstrumentation} = require('@opentelemetry/instrumentation-fs'); const {FastifyInstrumentation} = require('@opentelemetry/instrumentation-fastify'); const {GenericPoolInstrumentation} = require('@opentelemetry/instrumentation-generic-pool'); const {GraphQLInstrumentation} = require('@opentelemetry/instrumentation-graphql'); const {GrpcInstrumentation} = require('@opentelemetry/instrumentation-grpc'); const {HapiInstrumentation} = require('@opentelemetry/instrumentation-hapi'); const {HttpInstrumentation} = require('@opentelemetry/instrumentation-http'); const {IORedisInstrumentation} = require('@opentelemetry/instrumentation-ioredis'); const {KnexInstrumentation} = require('@opentelemetry/instrumentation-knex'); const {KafkaJsInstrumentation} = require('@opentelemetry/instrumentation-kafkajs'); const {KoaInstrumentation} = require('@opentelemetry/instrumentation-koa'); const {LruMemoizerInstrumentation} = require('@opentelemetry/instrumentation-lru-memoizer'); const {MemcachedInstrumentation} = require('@opentelemetry/instrumentation-memcached'); const {MongoDBInstrumentation} = require('@opentelemetry/instrumentation-mongodb'); const {MongooseInstrumentation} = require('@opentelemetry/instrumentation-mongoose'); const {MySQLInstrumentation} = require('@opentelemetry/instrumentation-mysql'); const {MySQL2Instrumentation} = require('@opentelemetry/instrumentation-mysql2'); const {NestInstrumentation} = require('@opentelemetry/instrumentation-nestjs-core'); const {NetInstrumentation} = require('@opentelemetry/instrumentation-net'); const {PgInstrumentation} = require('@opentelemetry/instrumentation-pg'); const {PinoInstrumentation} = require('@opentelemetry/instrumentation-pino'); const {RedisInstrumentation} = require('@opentelemetry/instrumentation-redis'); const {RedisInstrumentation: RedisFourInstrumentation} = require('@opentelemetry/instrumentation-redis-4'); const {RestifyInstrumentation} = require('@opentelemetry/instrumentation-restify'); const {RouterInstrumentation} = require('@opentelemetry/instrumentation-router'); const {RuntimeNodeInstrumentation} = require('@opentelemetry/instrumentation-runtime-node'); const {SocketIoInstrumentation} = require('@opentelemetry/instrumentation-socket.io'); const {TediousInstrumentation} = require('@opentelemetry/instrumentation-tedious'); const {UndiciInstrumentation} = require('@opentelemetry/instrumentation-undici'); const {WinstonInstrumentation} = require('@opentelemetry/instrumentation-winston'); // Instrumentations attach their Hook (for require-in-the-middle or import-in-the-middle) // when the `enable` method is called and this happens inside their constructor // https://github.com/open-telemetry/opentelemetry-js/blob/1b4999f386e0240b7f65350e8360ccc2930b0fe6/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts#L71 // // The SDK cannot construct any instrumentation until it has resolved the config. So to // do a lazy creation of instrumentations we have factory functions that can receive // the user's config and can default to something else if needed. /** @type {Record<keyof InstrumentaionsMap, (cfg: any) => Instrumentation>} */ const instrumentationsMap = { '@elastic/opentelemetry-instrumentation-openai': (cfg) => new OpenAIInstrumentation(cfg), '@opentelemetry/instrumentation-amqplib': (cfg) => new AmqplibInstrumentation(cfg), '@opentelemetry/instrumentation-aws-sdk': (cfg) => new AwsInstrumentation(cfg), '@opentelemetry/instrumentation-bunyan': (cfg) => new BunyanInstrumentation(cfg), '@opentelemetry/instrumentation-connect': (cfg) => new ConnectInstrumentation(cfg), '@opentelemetry/instrumentation-cassandra-driver': (cfg) => new CassandraDriverInstrumentation(cfg), '@opentelemetry/instrumentation-cucumber': (cfg) => new CucumberInstrumentation(cfg), '@opentelemetry/instrumentation-dataloader': (cfg) => new DataloaderInstrumentation(cfg), '@opentelemetry/instrumentation-dns': (cfg) => new DnsInstrumentation(cfg), '@opentelemetry/instrumentation-express': (cfg) => new ExpressInstrumentation(cfg), '@opentelemetry/instrumentation-fastify': (cfg) => new FastifyInstrumentation(cfg), '@opentelemetry/instrumentation-fs': (cfg) => new FsInstrumentation(cfg), '@opentelemetry/instrumentation-generic-pool': (cfg) => new GenericPoolInstrumentation(cfg), '@opentelemetry/instrumentation-graphql': (cfg) => new GraphQLInstrumentation(cfg), '@opentelemetry/instrumentation-grpc': (cfg) => new GrpcInstrumentation(cfg), '@opentelemetry/instrumentation-hapi': (cfg) => new HapiInstrumentation(cfg), '@opentelemetry/instrumentation-http': (cfg) => new HttpInstrumentation(cfg), '@opentelemetry/instrumentation-ioredis': (cfg) => new IORedisInstrumentation(cfg), '@opentelemetry/instrumentation-knex': (cfg) => new KnexInstrumentation(cfg), '@opentelemetry/instrumentation-kafkajs': (cfg) => new KafkaJsInstrumentation(cfg), '@opentelemetry/instrumentation-koa': (cfg) => new KoaInstrumentation(cfg), '@opentelemetry/instrumentation-lru-memoizer': (cfg) => new LruMemoizerInstrumentation(cfg), '@opentelemetry/instrumentation-memcached': (cfg) => new MemcachedInstrumentation(cfg), '@opentelemetry/instrumentation-mongodb': (cfg) => new MongoDBInstrumentation(cfg), '@opentelemetry/instrumentation-mongoose': (cfg) => new MongooseInstrumentation(cfg), '@opentelemetry/instrumentation-mysql': (cfg) => new MySQLInstrumentation(cfg), '@opentelemetry/instrumentation-mysql2': (cfg) => new MySQL2Instrumentation(cfg), '@opentelemetry/instrumentation-nestjs-core': (cfg) => new NestInstrumentation(cfg), '@opentelemetry/instrumentation-net': (cfg) => new NetInstrumentation(cfg), '@opentelemetry/instrumentation-pg': (cfg) => new PgInstrumentation(cfg), '@opentelemetry/instrumentation-pino': (cfg) => new PinoInstrumentation(cfg), '@opentelemetry/instrumentation-redis': (cfg) => new RedisInstrumentation(cfg), '@opentelemetry/instrumentation-redis-4': (cfg) => new RedisFourInstrumentation(cfg), '@opentelemetry/instrumentation-restify': (cfg) => new RestifyInstrumentation(cfg), '@opentelemetry/instrumentation-router': (cfg) => new RouterInstrumentation(cfg), '@opentelemetry/instrumentation-runtime-node': (cfg) => new RuntimeNodeInstrumentation(cfg), '@opentelemetry/instrumentation-socket.io': (cfg) => new SocketIoInstrumentation(cfg), '@opentelemetry/instrumentation-tedious': (cfg) => new TediousInstrumentation(cfg), '@opentelemetry/instrumentation-undici': (cfg) => new UndiciInstrumentation(cfg), '@opentelemetry/instrumentation-winston': (cfg) => new WinstonInstrumentation(cfg), }; /* eslint-enable prettier/prettier */ const excludedInstrumentations = new Set([ '@opentelemetry/instrumentation-fastify', '@opentelemetry/instrumentation-fs', ]); const otelInstrPrefix = '@opentelemetry/instrumentation-'; const otelInstrShortNames = new Set(); const nonOtelInstrNames = new Set(); for (const name of Object.keys(instrumentationsMap)) { if (name.startsWith(otelInstrPrefix)) { otelInstrShortNames.add(name.replace(otelInstrPrefix, '')); } else { nonOtelInstrNames.add(name); } } /** * Reads a string in the format `value1,value2` and parses * it into an array. This is the format specified for comma separated * list for OTEL environment vars. Example: * https://opentelemetry.io/docs/languages/sdk-configuration/general/#otel_propagators * * If the param is not defined or falsy it returns an empty array. The resulting * array has only nonempty string. * * @param {string} envvar * @returns {Array<string> | undefined} */ function getInstrumentationsFromEnv(envvar) { const names = getStringListFromEnv(envvar); if (names) { const instrumentations = []; for (const name of names) { if (otelInstrShortNames.has(name)) { instrumentations.push(`${otelInstrPrefix}${name}`); } else if (nonOtelInstrNames.has(name)) { instrumentations.push(name); } else { log.warn( `Unknown instrumentation "${name}" specified in the environment variable ${envvar}` ); } } return instrumentations; } return undefined; } /** * With this method you can disable, configure and replace the instrumentations * supported by ElastiNodeSDK. The result is an array of all the * active instrumentations based on the options parameter which is an object * of `instrumentation_name` as keys and objects or functions as values. * - if instrumentation name is not present in keys default instrumentation is * returned * - if instrumentation name is present in keys and has an object as value this * will be used as configuration. Note you can disable with `{ enable: false }` * - if instrumentation name is present in keys and has an function as value this * will be used as a factory and the object retuned by th function will replace * the instrumentation * * You can use this function if are developing your own telemetry script as an aid * to configure your instrumentations array * * Example: * * ```js * const customInstrumentations = getInstrumentations({ * // HTTP instrumentation will get a specific config * '@opentelemetry/instrumentation-http': { * serverName: 'foo' * }, * // Express insrumentation will be disabled and not returned * '@opentelemetry/instrumentation-express': { * enabled: false, * }, * }); * * startNodeSDK({ * instrumentations: [ * ...customInstrumentations, * // You can add here instrumentations from other sources * ] * }); * ``` * * @param {Partial<InstrumentaionsMap>} [opts={}] * @returns {Array<Instrumentation>} */ function getInstrumentations(opts = {}) { /** @type {Array<Instrumentation>} */ const instrumentations = []; const enabledFromEnv = getInstrumentationsFromEnv( 'OTEL_NODE_ENABLED_INSTRUMENTATIONS' ); const disabledFromEnv = getInstrumentationsFromEnv( 'OTEL_NODE_DISABLED_INSTRUMENTATIONS' ); // `@opentelemetry/instrumentation-http` defaults to emit old semconv attributes. // Set the default to stable HTTP semconv if not defined by the user (http, http/dup) // TODO: remove this once https://github.com/open-telemetry/opentelemetry-js/pull/5552 // is merged and published. const semconvOptIn = getStringListFromEnv('OTEL_SEMCONV_STABILITY_OPT_IN') || []; if (!semconvOptIn.includes('http') && !semconvOptIn.includes('http/dup')) { semconvOptIn.push('http'); process.env.OTEL_SEMCONV_STABILITY_OPT_IN = semconvOptIn.join(','); } const defaultInstrConfigFromName = {}; // Handle `ELASTIC_OTEL_NODE_ENABLE_LOG_SENDING`. The user must opt-in to // the "log sending" feature of the OTel log framework instrumentations // (pino, bunyan, winston). This *differs* from OTel JS, but matches // OTel Java Agent behaviour. // // This sets a default `{disableLogSending: true}` default config for // `instrumentation-{bunyan,pino,winston}`. // // See: https://github.com/elastic/elastic-otel-node/issues/680 // TODO: link instead to configuration doc for this when have it. const enableLogSending = getBooleanFromEnv( 'ELASTIC_OTEL_NODE_ENABLE_LOG_SENDING' ); if (!enableLogSending) { const logInstrNames = ['bunyan', 'pino', 'winston']; logInstrNames.forEach((name) => { defaultInstrConfigFromName[ `@opentelemetry/instrumentation-${name}` ] = {disableLogSending: true}; }); } // TODO: check `opts` and warn if it includes entries for unknown instrumentations (this is what `checkManuallyProvidedInstrumentationNames` does in auto-instrumentations-node). Object.keys(instrumentationsMap).forEach((name) => { // Skip if env has an `enabled` list and does not include this one if (enabledFromEnv && !enabledFromEnv.includes(name)) { return; } // Skip if env has an `disabled` list and it's present (overriding enabled list) if (disabledFromEnv && disabledFromEnv.includes(name)) { return; } // Skip if metrics are disabled by env var const isMetricsDisabled = getBooleanFromEnv('ELASTIC_OTEL_METRICS_DISABLED') ?? false; if ( isMetricsDisabled && name === '@opentelemetry/instrumentation-runtime-node' ) { return; } const isObject = typeof opts[name] === 'object'; if (!(opts[name] == null || isObject)) { log.warn( {instrConfig: opts[name]}, `invalid value for getInstrumentations() '${name}' option: must be object, got ${typeof opts[ name ]}` ); } let instrConfig = isObject ? opts[name] : undefined; instrConfig = {...defaultInstrConfigFromName[name], ...instrConfig}; // We should instantiate a instrumentation: // - if set via OTEL_NODE_ENABLED_INSTRUMENTATIONS // - overriding any config that might be passed // NOTE: factories are not overwritten // - otherwise // - if there is no config passed (elastic SDK will use its defaults) // - if the configuration passed is not disabling it let instr; if (enabledFromEnv) { instrConfig = {...instrConfig, enabled: true}; } else if (excludedInstrumentations.has(name)) { // if excluded instrumentations not present in envvar the instrumentation // is disabled unless an explicit config says the opposite // ref: https://github.com/open-telemetry/opentelemetry-js-contrib/pull/2467 instrConfig = {enabled: false, ...instrConfig}; } if (!instrConfig || instrConfig.enabled !== false) { instr = instrumentationsMap[name](instrConfig); } if (instr) { // Note that this doesn't log *functions* in instrConfig. log.debug({instrConfig}, `Enabling instrumentation "${name}"`); instrumentations.push(instr); } }); return instrumentations; } module.exports = { getInstrumentations, };