diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..57abb1c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "Metapackage", + "Tachy", + "tachybase" + ] +} diff --git a/README.md b/README.md index 3fc1852..48d1656 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# opentelemetry-instrumentation-tachybase +# opentelemetry-auto-instrumentations -TachyBase 专属的 OpenTelemetry 插桩库 \ No newline at end of file +TachyBase 专属的 OpenTelemetry 自动插桩库 diff --git a/package.json b/package.json new file mode 100644 index 0000000..0a76a7d --- /dev/null +++ b/package.json @@ -0,0 +1,105 @@ +{ + "name": "@tachybase/opentelemetry-auto-instrumentations", + "version": "0.0.1", + "description": "Metapackage of telemetry for TachyBase which bundles OpenTelemetry node core and instrumentations", + "author": "TachyBase Team", + "homepage": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "publishConfig": { + "access": "public" + }, + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "exports": { + ".": "./build/src/index.js", + "./register": "./build/src/register.js" + }, + "repository": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations", + "scripts": { + "clean": "rimraf build/*", + "compile": "tsc -p .", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "precompile": "tsc --version && lerna run version:update --scope @opentelemetry/auto-instrumentations-node --include-dependencies", + "prewatch": "npm run precompile", + "prepublishOnly": "npm run compile", + "tdd": "yarn test -- --watch-extensions ts --watch", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'", + "watch": "tsc -w" + }, + "bugs": { + "url": "https://git.daoyoucloud.com/TomyJan/opentelemetry-auto-instrumentations/issues" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.4.1" + }, + "devDependencies": { + "@opentelemetry/api": "^1.4.1", + "@types/mocha": "7.0.2", + "@types/node": "18.6.5", + "@types/sinon": "10.0.18", + "mocha": "7.2.0", + "nyc": "15.1.0", + "rimraf": "5.0.5", + "sinon": "15.2.0", + "ts-mocha": "10.0.0", + "typescript": "4.4.4" + }, + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation-amqplib": "^0.40.0", + "@opentelemetry/instrumentation-aws-lambda": "^0.43.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.43.1", + "@opentelemetry/instrumentation-bunyan": "^0.40.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.40.0", + "@opentelemetry/instrumentation-connect": "^0.38.0", + "@opentelemetry/instrumentation-cucumber": "^0.8.0", + "@opentelemetry/instrumentation-dataloader": "^0.11.0", + "@opentelemetry/instrumentation-dns": "^0.38.0", + "@opentelemetry/instrumentation-express": "^0.41.1", + "@opentelemetry/instrumentation-fastify": "^0.38.0", + "@opentelemetry/instrumentation-fs": "^0.14.0", + "@opentelemetry/instrumentation-generic-pool": "^0.38.0", + "@opentelemetry/instrumentation-graphql": "^0.42.0", + "@opentelemetry/instrumentation-grpc": "^0.52.0", + "@opentelemetry/instrumentation-hapi": "^0.40.0", + "@opentelemetry/instrumentation-http": "^0.52.0", + "@opentelemetry/instrumentation-ioredis": "^0.42.0", + "@opentelemetry/instrumentation-kafkajs": "^0.2.0", + "@opentelemetry/instrumentation-knex": "^0.39.0", + "@opentelemetry/instrumentation-koa": "^0.42.0", + "@opentelemetry/instrumentation-lru-memoizer": "^0.39.0", + "@opentelemetry/instrumentation-memcached": "^0.38.0", + "@opentelemetry/instrumentation-mongodb": "^0.46.0", + "@opentelemetry/instrumentation-mongoose": "^0.40.0", + "@opentelemetry/instrumentation-mysql": "^0.40.0", + "@opentelemetry/instrumentation-mysql2": "^0.40.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.39.0", + "@opentelemetry/instrumentation-net": "^0.38.0", + "@opentelemetry/instrumentation-pg": "^0.43.0", + "@opentelemetry/instrumentation-pino": "^0.41.0", + "@opentelemetry/instrumentation-redis": "^0.41.0", + "@opentelemetry/instrumentation-redis-4": "^0.41.0", + "@opentelemetry/instrumentation-restify": "^0.40.0", + "@opentelemetry/instrumentation-router": "^0.39.0", + "@opentelemetry/instrumentation-socket.io": "^0.41.0", + "@opentelemetry/instrumentation-tedious": "^0.12.0", + "@opentelemetry/instrumentation-undici": "^0.4.0", + "@opentelemetry/instrumentation-winston": "^0.39.0", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.28.10", + "@opentelemetry/resource-detector-aws": "^1.5.2", + "@opentelemetry/resource-detector-azure": "^0.2.9", + "@opentelemetry/resource-detector-container": "^0.3.11", + "@opentelemetry/resource-detector-gcp": "^0.29.10", + "@opentelemetry/resources": "^1.24.0", + "@opentelemetry/sdk-node": "^0.52.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts" + ] +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..669066d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + getNodeAutoInstrumentations, + getResourceDetectorsFromEnv as getResourceDetectors, + InstrumentationConfigMap, +} from './utils'; diff --git a/src/register.ts b/src/register.ts new file mode 100644 index 0000000..a14538d --- /dev/null +++ b/src/register.ts @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as opentelemetry from '@opentelemetry/sdk-node'; +import { diag, DiagConsoleLogger } from '@opentelemetry/api'; +import { + getNodeAutoInstrumentations, + getResourceDetectorsFromEnv, +} from './utils'; + +diag.setLogger( + new DiagConsoleLogger(), + opentelemetry.core.getEnv().OTEL_LOG_LEVEL +); + +const sdk = new opentelemetry.NodeSDK({ + instrumentations: getNodeAutoInstrumentations(), + resourceDetectors: getResourceDetectorsFromEnv(), +}); + +try { + sdk.start(); + diag.info('OpenTelemetry automatic instrumentation started successfully'); +} catch (error) { + diag.error( + 'Error initializing OpenTelemetry SDK. Your application is not instrumented and will not produce telemetry', + error + ); +} + +process.on('SIGTERM', () => { + sdk + .shutdown() + .then(() => diag.debug('OpenTelemetry SDK terminated')) + .catch(error => diag.error('Error terminating OpenTelemetry SDK', error)); +}); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..b041711 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,286 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { diag } from '@opentelemetry/api'; +import { Instrumentation } from '@opentelemetry/instrumentation'; + +import { AmqplibInstrumentation } from '@opentelemetry/instrumentation-amqplib'; +import { AwsLambdaInstrumentation } from '@opentelemetry/instrumentation-aws-lambda'; +import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk'; +import { BunyanInstrumentation } from '@opentelemetry/instrumentation-bunyan'; +import { CassandraDriverInstrumentation } from '@opentelemetry/instrumentation-cassandra-driver'; +import { ConnectInstrumentation } from '@opentelemetry/instrumentation-connect'; +import { CucumberInstrumentation } from '@opentelemetry/instrumentation-cucumber'; +import { DataloaderInstrumentation } from '@opentelemetry/instrumentation-dataloader'; +import { DnsInstrumentation } from '@opentelemetry/instrumentation-dns'; +import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; +import { FastifyInstrumentation } from '@opentelemetry/instrumentation-fastify'; +import { FsInstrumentation } from '@opentelemetry/instrumentation-fs'; +import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-generic-pool'; +import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; +import { GrpcInstrumentation } from '@opentelemetry/instrumentation-grpc'; +import { HapiInstrumentation } from '@opentelemetry/instrumentation-hapi'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis'; +import { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs'; +import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex'; +import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; +import { LruMemoizerInstrumentation } from '@opentelemetry/instrumentation-lru-memoizer'; +import { MemcachedInstrumentation } from '@opentelemetry/instrumentation-memcached'; +import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; +import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; +import { MySQL2Instrumentation } from '@opentelemetry/instrumentation-mysql2'; +import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; +import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; +import { NetInstrumentation } from '@opentelemetry/instrumentation-net'; +import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; +import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino'; +import { RedisInstrumentation as RedisInstrumentationV2 } from '@opentelemetry/instrumentation-redis'; +import { RedisInstrumentation as RedisInstrumentationV4 } from '@opentelemetry/instrumentation-redis-4'; +import { RestifyInstrumentation } from '@opentelemetry/instrumentation-restify'; +import { RouterInstrumentation } from '@opentelemetry/instrumentation-router'; +import { SocketIoInstrumentation } from '@opentelemetry/instrumentation-socket.io'; +import { TediousInstrumentation } from '@opentelemetry/instrumentation-tedious'; +import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'; +import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston'; + +import { alibabaCloudEcsDetector } from '@opentelemetry/resource-detector-alibaba-cloud'; +import { + awsBeanstalkDetector, + awsEc2Detector, + awsEcsDetector, + awsEksDetector, + awsLambdaDetector, +} from '@opentelemetry/resource-detector-aws'; +import { containerDetector } from '@opentelemetry/resource-detector-container'; +import { gcpDetector } from '@opentelemetry/resource-detector-gcp'; +import { + Detector, + DetectorSync, + envDetectorSync, + hostDetectorSync, + osDetectorSync, + processDetectorSync, + serviceInstanceIdDetectorSync, +} from '@opentelemetry/resources'; +import { + azureAppServiceDetector, + azureFunctionsDetector, + azureVmDetector, +} from '@opentelemetry/resource-detector-azure'; + +const RESOURCE_DETECTOR_CONTAINER = 'container'; +const RESOURCE_DETECTOR_ENVIRONMENT = 'env'; +const RESOURCE_DETECTOR_HOST = 'host'; +const RESOURCE_DETECTOR_OS = 'os'; +const RESOURCE_DETECTOR_SERVICE_INSTANCE_ID = 'serviceinstance'; +const RESOURCE_DETECTOR_PROCESS = 'process'; +const RESOURCE_DETECTOR_ALIBABA = 'alibaba'; +const RESOURCE_DETECTOR_AWS = 'aws'; +const RESOURCE_DETECTOR_AZURE = 'azure'; +const RESOURCE_DETECTOR_GCP = 'gcp'; + +const InstrumentationMap = { + '@opentelemetry/instrumentation-amqplib': AmqplibInstrumentation, + '@opentelemetry/instrumentation-aws-lambda': AwsLambdaInstrumentation, + '@opentelemetry/instrumentation-aws-sdk': AwsInstrumentation, + '@opentelemetry/instrumentation-bunyan': BunyanInstrumentation, + '@opentelemetry/instrumentation-cassandra-driver': + CassandraDriverInstrumentation, + '@opentelemetry/instrumentation-connect': ConnectInstrumentation, + '@opentelemetry/instrumentation-cucumber': CucumberInstrumentation, + '@opentelemetry/instrumentation-dataloader': DataloaderInstrumentation, + '@opentelemetry/instrumentation-dns': DnsInstrumentation, + '@opentelemetry/instrumentation-express': ExpressInstrumentation, + '@opentelemetry/instrumentation-fastify': FastifyInstrumentation, + '@opentelemetry/instrumentation-fs': FsInstrumentation, + '@opentelemetry/instrumentation-generic-pool': GenericPoolInstrumentation, + '@opentelemetry/instrumentation-graphql': GraphQLInstrumentation, + '@opentelemetry/instrumentation-grpc': GrpcInstrumentation, + '@opentelemetry/instrumentation-hapi': HapiInstrumentation, + '@opentelemetry/instrumentation-http': HttpInstrumentation, + '@opentelemetry/instrumentation-ioredis': IORedisInstrumentation, + '@opentelemetry/instrumentation-kafkajs': KafkaJsInstrumentation, + '@opentelemetry/instrumentation-knex': KnexInstrumentation, + '@opentelemetry/instrumentation-koa': KoaInstrumentation, + '@opentelemetry/instrumentation-lru-memoizer': LruMemoizerInstrumentation, + '@opentelemetry/instrumentation-memcached': MemcachedInstrumentation, + '@opentelemetry/instrumentation-mongodb': MongoDBInstrumentation, + '@opentelemetry/instrumentation-mongoose': MongooseInstrumentation, + '@opentelemetry/instrumentation-mysql2': MySQL2Instrumentation, + '@opentelemetry/instrumentation-mysql': MySQLInstrumentation, + '@opentelemetry/instrumentation-nestjs-core': NestInstrumentation, + '@opentelemetry/instrumentation-net': NetInstrumentation, + '@opentelemetry/instrumentation-pg': PgInstrumentation, + '@opentelemetry/instrumentation-pino': PinoInstrumentation, + '@opentelemetry/instrumentation-redis': RedisInstrumentationV2, + '@opentelemetry/instrumentation-redis-4': RedisInstrumentationV4, + '@opentelemetry/instrumentation-restify': RestifyInstrumentation, + '@opentelemetry/instrumentation-router': RouterInstrumentation, + '@opentelemetry/instrumentation-socket.io': SocketIoInstrumentation, + '@opentelemetry/instrumentation-tedious': TediousInstrumentation, + '@opentelemetry/instrumentation-undici': UndiciInstrumentation, + '@opentelemetry/instrumentation-winston': WinstonInstrumentation, +}; + +// Config types inferred automatically from the first argument of the constructor +type ConfigArg = T extends new (...args: infer U) => unknown ? U[0] : never; +export type InstrumentationConfigMap = { + [Name in keyof typeof InstrumentationMap]?: ConfigArg< + (typeof InstrumentationMap)[Name] + >; +}; + +export function getNodeAutoInstrumentations( + inputConfigs: InstrumentationConfigMap = {} +): Instrumentation[] { + checkManuallyProvidedInstrumentationNames(Object.keys(inputConfigs)); + const enabledInstrumentationsFromEnv = getEnabledInstrumentationsFromEnv(); + const disabledInstrumentationsFromEnv = getDisabledInstrumentationsFromEnv(); + + const instrumentations: Instrumentation[] = []; + + for (const name of Object.keys(InstrumentationMap) as Array< + keyof typeof InstrumentationMap + >) { + const Instance = InstrumentationMap[name]; + // Defaults are defined by the instrumentation itself + const userConfig: any = inputConfigs[name] ?? {}; + + if ( + userConfig.enabled === false || + !enabledInstrumentationsFromEnv.includes(name) || + disabledInstrumentationsFromEnv.includes(name) + ) { + diag.debug(`Disabling instrumentation for ${name}`); + continue; + } + + try { + diag.debug(`Loading instrumentation for ${name}`); + instrumentations.push(new Instance(userConfig)); + } catch (e: any) { + diag.error(e); + } + } + + return instrumentations; +} + +function checkManuallyProvidedInstrumentationNames( + manuallyProvidedInstrumentationNames: string[] +) { + for (const name of manuallyProvidedInstrumentationNames) { + if (!Object.prototype.hasOwnProperty.call(InstrumentationMap, name)) { + diag.error(`Provided instrumentation name "${name}" not found`); + } + } +} + +function getInstrumentationsFromEnv(envVar: string): string[] { + const envVarValue = process.env[envVar]; + if (envVarValue == null) { + return []; + } + + const instrumentationsFromEnv = envVarValue + ?.split(',') + .map( + instrumentationPkgSuffix => + `@opentelemetry/instrumentation-${instrumentationPkgSuffix.trim()}` + ); + checkManuallyProvidedInstrumentationNames(instrumentationsFromEnv); + return instrumentationsFromEnv; +} + +/** + * Returns the list of instrumentations that are enabled based on the environment variable. + */ +function getEnabledInstrumentationsFromEnv() { + if (!process.env.OTEL_NODE_ENABLED_INSTRUMENTATIONS) { + return Object.keys(InstrumentationMap); + } + + const instrumentationsFromEnv = getInstrumentationsFromEnv( + 'OTEL_NODE_ENABLED_INSTRUMENTATIONS' + ); + return instrumentationsFromEnv; +} + +/** + * Returns the list of instrumentations that are disabled based on the environment variable. + */ +function getDisabledInstrumentationsFromEnv() { + if (!process.env.OTEL_NODE_DISABLED_INSTRUMENTATIONS) { + return []; + } + + const instrumentationsFromEnv = getInstrumentationsFromEnv( + 'OTEL_NODE_DISABLED_INSTRUMENTATIONS' + ); + return instrumentationsFromEnv; +} + +export function getResourceDetectorsFromEnv(): Array { + const resourceDetectors = new Map< + string, + Detector | DetectorSync | Detector[] | DetectorSync[] + >([ + [RESOURCE_DETECTOR_CONTAINER, containerDetector], + [RESOURCE_DETECTOR_ENVIRONMENT, envDetectorSync], + [RESOURCE_DETECTOR_HOST, hostDetectorSync], + [RESOURCE_DETECTOR_OS, osDetectorSync], + [RESOURCE_DETECTOR_SERVICE_INSTANCE_ID, serviceInstanceIdDetectorSync], + [RESOURCE_DETECTOR_PROCESS, processDetectorSync], + [RESOURCE_DETECTOR_ALIBABA, alibabaCloudEcsDetector], + [RESOURCE_DETECTOR_GCP, gcpDetector], + [ + RESOURCE_DETECTOR_AWS, + [ + awsEc2Detector, + awsEcsDetector, + awsEksDetector, + awsBeanstalkDetector, + awsLambdaDetector, + ], + ], + [ + RESOURCE_DETECTOR_AZURE, + [azureAppServiceDetector, azureFunctionsDetector, azureVmDetector], + ], + ]); + + const resourceDetectorsFromEnv = + process.env.OTEL_NODE_RESOURCE_DETECTORS?.split(',') ?? ['all']; + + if (resourceDetectorsFromEnv.includes('all')) { + return [...resourceDetectors.values()].flat(); + } + + if (resourceDetectorsFromEnv.includes('none')) { + return []; + } + + return resourceDetectorsFromEnv.flatMap(detector => { + const resourceDetector = resourceDetectors.get(detector); + if (!resourceDetector) { + diag.error( + `Invalid resource detector "${detector}" specified in the environment variable OTEL_NODE_RESOURCE_DETECTORS` + ); + } + return resourceDetector || []; + }); +} diff --git a/test/register.test.ts b/test/register.test.ts new file mode 100644 index 0000000..8476e0c --- /dev/null +++ b/test/register.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawnSync } from 'child_process'; +import * as assert from 'assert'; + +describe('Register', function () { + it('can load auto instrumentation from command line', () => { + const proc = spawnSync( + process.execPath, + ['--require', '../build/src/register.js', './test-app/app.js'], + { + cwd: __dirname, + timeout: 5000, + killSignal: 'SIGKILL', // SIGTERM is not sufficient to terminate some hangs + env: Object.assign({}, process.env, { + OTEL_NODE_RESOURCE_DETECTORS: 'none', + OTEL_TRACES_EXPORTER: 'console', + // nx (used by lerna run) defaults `FORCE_COLOR=true`, which in + // node v18.17.0, v20.3.0 and later results in ANSI color escapes + // in the ConsoleSpanExporter output that is checked below. + FORCE_COLOR: '0', + }), + } + ); + assert.ifError(proc.error); + assert.equal(proc.status, 0, `proc.status (${proc.status})`); + assert.equal(proc.signal, null, `proc.signal (${proc.signal})`); + + assert.ok( + proc.stdout.includes( + 'OpenTelemetry automatic instrumentation started successfully' + ) + ); + + // Check a span has been generated for the GET request done in app.js + assert.ok( + proc.stdout.includes("name: 'GET'"), + 'console span output in stdout' + ); + }); +}); diff --git a/test/test-app/app.js b/test/test-app/app.js new file mode 100644 index 0000000..1057acb --- /dev/null +++ b/test/test-app/app.js @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//Used in register.test.ts to mimic a JS app. +const http = require('http'); + +const options = { + hostname: 'example.com', + port: 80, + path: '/', + method: 'GET' +}; + +const req = http.request(options); + +req.end(); diff --git a/test/utils.test.ts b/test/utils.test.ts new file mode 100644 index 0000000..cc7cb24 --- /dev/null +++ b/test/utils.test.ts @@ -0,0 +1,208 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { diag } from '@opentelemetry/api'; +import { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { getNodeAutoInstrumentations } from '../src'; +import { getResourceDetectorsFromEnv } from '../src/utils'; + +describe('utils', () => { + describe('getNodeAutoInstrumentations', () => { + it('should include all installed instrumentations', () => { + const instrumentations = getNodeAutoInstrumentations(); + const installedInstrumentations = Object.keys( + require('../package.json').dependencies + ).filter(depName => { + return depName.startsWith('@opentelemetry/instrumentation-'); + }); + + assert.deepStrictEqual( + new Set(instrumentations.map(i => i.instrumentationName)), + new Set(installedInstrumentations) + ); + }); + + it('should use user config', () => { + function applyCustomAttributesOnSpan() {} + + const instrumentations = getNodeAutoInstrumentations({ + '@opentelemetry/instrumentation-http': { + applyCustomAttributesOnSpan, + }, + }); + const instrumentation = instrumentations.find( + instr => + instr.instrumentationName === '@opentelemetry/instrumentation-http' + ) as any; + const configHttp = instrumentation._config as HttpInstrumentationConfig; + + assert.strictEqual( + configHttp.applyCustomAttributesOnSpan, + applyCustomAttributesOnSpan + ); + }); + + it('should not return disabled instrumentation', () => { + const instrumentations = getNodeAutoInstrumentations({ + '@opentelemetry/instrumentation-grpc': { + enabled: false, + }, + }); + const instrumentation = instrumentations.find( + instr => + instr.instrumentationName === '@opentelemetry/instrumentation-grpc' + ); + assert.strictEqual(instrumentation, undefined); + }); + + it('should return only instrumentations enabled via OTEL_NODE_ENABLED_INSTRUMENTATIONS environment variable', () => { + process.env.OTEL_NODE_ENABLED_INSTRUMENTATIONS = + 'http,aws-sdk, nestjs-core'; // separator with and without whitespaces should be allowed + try { + const instrumentations = getNodeAutoInstrumentations(); + + assert.deepStrictEqual( + new Set(instrumentations.map(i => i.instrumentationName)), + new Set([ + '@opentelemetry/instrumentation-http', + '@opentelemetry/instrumentation-aws-sdk', + '@opentelemetry/instrumentation-nestjs-core', + ]) + ); + } finally { + delete process.env.OTEL_NODE_ENABLED_INSTRUMENTATIONS; + } + }); + + it('should include all instrumentations except those disabled via OTEL_NODE_DISABLED_INSTRUMENTATIONS environment variable', () => { + process.env.OTEL_NODE_DISABLED_INSTRUMENTATIONS = + 'fs,aws-sdk, aws-lambda'; // separator with and without whitespaces should be allowed + try { + const instrumentations = getNodeAutoInstrumentations(); + const disabledInstrumentations = new Set([ + '@opentelemetry/instrumentation-fs', + '@opentelemetry/instrumentation-aws-sdk', + '@opentelemetry/instrumentation-aws-lambda', + ]); + const enabledInstrumentationNames = new Set( + instrumentations.map(i => i.instrumentationName) + ); + + for (const disabledInstrumentation of disabledInstrumentations) { + assert.strictEqual( + enabledInstrumentationNames.has(disabledInstrumentation), + false + ); + } + } finally { + delete process.env.OTEL_NODE_DISABLED_INSTRUMENTATIONS; + } + }); + + it('should disable any instrumentations from OTEL_NODE_ENABLED_INSTRUMENTATIONS if set in OTEL_NODE_DISABLED_INSTRUMENTATIONS', () => { + process.env.OTEL_NODE_ENABLED_INSTRUMENTATIONS = 'http,express,net'; + process.env.OTEL_NODE_DISABLED_INSTRUMENTATIONS = 'fs,net'; // fs is no-op here, already disabled + try { + const instrumentations = getNodeAutoInstrumentations(); + + assert.deepStrictEqual( + new Set(instrumentations.map(i => i.instrumentationName)), + new Set([ + '@opentelemetry/instrumentation-http', + '@opentelemetry/instrumentation-express', + ]) + ); + } finally { + delete process.env.OTEL_NODE_DISABLED_INSTRUMENTATIONS; + delete process.env.OTEL_NODE_ENABLED_INSTRUMENTATIONS; + } + }); + + it('should show error for none existing instrumentation', () => { + const spy = sinon.stub(diag, 'error'); + const name = '@opentelemetry/instrumentation-http2'; + const instrumentations = getNodeAutoInstrumentations({ + // @ts-expect-error verify that wrong name works + [name]: { + enabled: false, + }, + }); + const instrumentation = instrumentations.find( + instr => instr.instrumentationName === name + ); + assert.strictEqual(instrumentation, undefined); + + assert.strictEqual( + spy.args[0][0], + `Provided instrumentation name "${name}" not found` + ); + + spy.restore(); + }); + }); + + describe('getResourceDetectorsFromEnv', () => { + it('should return all resource detectors by default', () => { + assert.equal(getResourceDetectorsFromEnv().length, 16); + }); + + it('should return all resource detectors when OTEL_NODE_RESOURCE_DETECTORS contains "all"', () => { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'all'; + assert.equal(getResourceDetectorsFromEnv().length, 16); + + delete process.env.OTEL_NODE_RESOURCE_DETECTORS; + }); + + it('should return specific resource detectors depending on OTEL_NODE_RESOURCE_DETECTORS', () => { + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'env,host,serviceinstance'; + + const resourceDetectors = getResourceDetectorsFromEnv(); + + assert.equal(resourceDetectors.length, 3); + assert.equal(resourceDetectors[0].constructor.name, 'EnvDetectorSync'); + assert.equal(resourceDetectors[1].constructor.name, 'HostDetectorSync'); + assert.equal( + resourceDetectors[2].constructor.name, + 'ServiceInstanceIdDetectorSync' + ); + + delete process.env.OTEL_NODE_RESOURCE_DETECTORS; + }); + + it('should return no resource detectors when OTEL_NODE_RESOURCE_DETECTORS contains "none" or a typo', () => { + const spy = sinon.stub(diag, 'error'); + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'none'; + + assert.equal(getResourceDetectorsFromEnv().length, 0); + + assert.strictEqual(spy.callCount, 0); + + process.env.OTEL_NODE_RESOURCE_DETECTORS = 'test'; + + assert.equal(getResourceDetectorsFromEnv().length, 0); + + assert.strictEqual( + spy.args[0][0], + 'Invalid resource detector "test" specified in the environment variable OTEL_NODE_RESOURCE_DETECTORS' + ); + + spy.restore(); + delete process.env.OTEL_NODE_RESOURCE_DETECTORS; + }); + }); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..83c35af --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "declaration": true, + "declarationMap": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": true, + "module": "commonjs", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "pretty": true, + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "target": "es2017", + "incremental": true, + "newLine": "LF" + }, + "exclude": [ + "node_modules" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6ea779f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +}