diff --git a/examples/tracer-web/package.json b/examples/tracer-web/package.json index dc193fe18b..83075b3ef0 100644 --- a/examples/tracer-web/package.json +++ b/examples/tracer-web/package.json @@ -40,9 +40,10 @@ "@opentelemetry/exporter-zipkin": "^0.14.0", "@opentelemetry/metrics": "^0.14.0", "@opentelemetry/propagator-b3": "^0.14.0", - "@opentelemetry/plugin-document-load": "^0.9.0", + "@opentelemetry/plugin-document-load": "^0.11.0", "@opentelemetry/plugin-fetch": "^0.14.0", - "@opentelemetry/plugin-user-interaction": "^0.9.0", + "@opentelemetry/plugin-user-interaction": "^0.11.0", + "@opentelemetry/instrumentation": "^0.14.0", "@opentelemetry/instrumentation-xml-http-request": "^0.14.0", "@opentelemetry/tracing": "^0.14.0", "@opentelemetry/web": "^0.14.0" diff --git a/lerna.json b/lerna.json index 3c885c959f..96c7f149dc 100644 --- a/lerna.json +++ b/lerna.json @@ -2,6 +2,8 @@ "lerna": "3.13.4", "npmClient": "npm", "packages": [ + "examples/test", + "examples/tracer-web", "benchmark/*", "backwards-compatability/*", "metapackages/*", diff --git a/packages/opentelemetry-instrumentation/.gitignore b/packages/opentelemetry-instrumentation/.gitignore new file mode 100644 index 0000000000..473e814b60 --- /dev/null +++ b/packages/opentelemetry-instrumentation/.gitignore @@ -0,0 +1,2 @@ +# Dependency directories +!test/node/node_modules diff --git a/packages/opentelemetry-instrumentation/README.md b/packages/opentelemetry-instrumentation/README.md index 9988303d16..cd9ef8798c 100644 --- a/packages/opentelemetry-instrumentation/README.md +++ b/packages/opentelemetry-instrumentation/README.md @@ -113,6 +113,7 @@ const myPLugin = new MyPlugin(); myPLugin.setTracerProvider(provider); // this is optional myPLugin.setMeterProvider(meterProvider); // this is optional myPLugin.enable(); +// or use Auto Loader ``` ## Usage in Web @@ -154,6 +155,113 @@ const myPLugin = new MyPlugin(); myPLugin.setTracerProvider(provider); myPLugin.setMeterProvider(meterProvider); myPLugin.enable(); +// or use Auto Loader +``` + +## AutoLoader + +Successor of loading plugins through TracerProvider "plugins" option. +It also supersedes PluginLoader for node. The old configurations usually looks like + +### NODE - old way using TracerProvider + +```javascript +const { NodeTracerProvider } = require('@opentelemetry/node'); +const { B3Propagator } = require('@opentelemetry/propagator-b3'); +const provider = new NodeTracerProvider({ + plugins: { + http: { enabled: false }, + }, +}); +provider.register({ + propagator: new B3Propagator(), +}); +``` + +### WEB - old way using TracerProvider + +```javascript +const { WebTracerProvider } = require('@opentelemetry/web'); +const { UserInteractionPlugin } = require('@opentelemetry/plugin-user-interaction'); +const { XMLHttpRequestInstrumentation } = require('@opentelemetry/instrumentation-xml-http-request'); +const { B3Propagator } = require('@opentelemetry/propagator-b3'); +const provider = new WebTracerProvider({ + plugins: [ + new UserInteractionPlugin(), + new XMLHttpRequestInstrumentation({ + ignoreUrls: [/localhost/], + propagateTraceHeaderCorsUrls: [ + 'http://localhost:8090', + ], + }), + ], +}); +provider.register({ + propagator: new B3Propagator(), +}); +``` + +After change it will look like this - mixing plugins and instrumentations together +All plugins will be bound to TracerProvider as well as instrumentations + +### NODE - Auto Loader + +```javascript +const { B3Propagator } = require('@opentelemetry/propagator-b3'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); +const { GraphQLInstrumentation } = require('@opentelemetry/instrumentation-graphql'); +const { NodeTracerProvider } = require('@opentelemetry/node'); +const tracerProvider = new NodeTracerProvider(); + +registerInstrumentations({ + instrumentations: [ + new UserInteractionPlugin(), + new XMLHttpRequestInstrumentation({ + ignoreUrls: [/localhost/], + propagateTraceHeaderCorsUrls: [ + 'http://localhost:8090', + ], + }), + ], + meterProvider: meterProvider, + tracerProvider: tracerProvider, + logger: new ConsoleLogger(), // optional +}); + +tracerProvider.register({ + propagator: new B3Propagator(), +}); + +``` + +### WEB - Auto Loader + +```javascript +const { B3Propagator } = require('@opentelemetry/propagator-b3'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); +import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'; +const { UserInteractionPlugin } = require('@opentelemetry/plugin-user-interaction'); +const { WebTracerProvider } = require('@opentelemetry/web'); +const tracerProvider = new WebTracerProvider(); + +registerInstrumentations({ + instrumentations: [ + new GraphQLInstrumentation(), + { + plugins: { + http: { enabled: false }, + }, + } + ], + meterProvider: meterProvider, + tracerProvider: tracerProvider, + logger: new ConsoleLogger(), // optional +}); + +tracerProvider.register({ + propagator: new B3Propagator(), +}); + ``` ## License diff --git a/packages/opentelemetry-instrumentation/src/autoLoader.ts b/packages/opentelemetry-instrumentation/src/autoLoader.ts new file mode 100644 index 0000000000..24dc70e99c --- /dev/null +++ b/packages/opentelemetry-instrumentation/src/autoLoader.ts @@ -0,0 +1,67 @@ +/* + * 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 api from '@opentelemetry/api'; +import { + disableInstrumentations, + enableInstrumentations, + parseInstrumentationOptions, +} from './autoLoaderUtils'; +import { loadOldPlugins } from './platform'; +import { AutoLoaderOptions } from './types_internal'; + +/** + * It will register instrumentations and plugins + * @param options + * @return returns function to unload instrumentation and plugins that were + * registered + */ +export function registerInstrumentations( + options: AutoLoaderOptions +): () => void { + const { + instrumentations, + pluginsNode, + pluginsWeb, + } = parseInstrumentationOptions(options.instrumentations); + const tracerWithLogger = (options.tracerProvider as unknown) as { + logger: api.Logger; + }; + const tracerProvider = + options.tracerProvider || api.trace.getTracerProvider(); + const meterProvider = options.meterProvider || api.metrics.getMeterProvider(); + const logger = + options.logger || tracerWithLogger?.logger || new api.NoopLogger(); + + enableInstrumentations( + instrumentations, + logger, + tracerProvider, + meterProvider + ); + + const unload = loadOldPlugins( + pluginsNode, + pluginsWeb, + logger, + tracerProvider + ); + + return () => { + unload(); + disableInstrumentations(instrumentations); + }; +} diff --git a/packages/opentelemetry-instrumentation/src/autoLoaderUtils.ts b/packages/opentelemetry-instrumentation/src/autoLoaderUtils.ts new file mode 100644 index 0000000000..4d0660478a --- /dev/null +++ b/packages/opentelemetry-instrumentation/src/autoLoaderUtils.ts @@ -0,0 +1,94 @@ +/* + * 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 { Logger, MeterProvider, TracerProvider } from '@opentelemetry/api'; +import { Instrumentation } from './types'; +import { AutoLoaderResult, InstrumentationOption } from './types_internal'; + +import { + NodePlugins, + NodePluginsTracerConfiguration, + OldClassPlugin, +} from './types_plugin_only'; + +/** + * Parses the options and returns instrumentations, node plugins and + * web plugins + * @param options + */ +export function parseInstrumentationOptions( + options: InstrumentationOption[] = [] +): AutoLoaderResult { + let instrumentations: Instrumentation[] = []; + let pluginsNode: NodePlugins = {}; + let pluginsWeb: OldClassPlugin[] = []; + for (let i = 0, j = options.length; i < j; i++) { + const option = options[i] as any; + if (Array.isArray(option)) { + const results = parseInstrumentationOptions(option); + instrumentations = instrumentations.concat(results.instrumentations); + pluginsWeb = pluginsWeb.concat(results.pluginsWeb); + pluginsNode = Object.assign({}, pluginsNode, results.pluginsNode); + } else if ((option as NodePluginsTracerConfiguration).plugins) { + pluginsNode = Object.assign( + {}, + pluginsNode, + (option as NodePluginsTracerConfiguration).plugins + ); + } else if (typeof option === 'function') { + instrumentations.push(new option()); + } else if ((option as Instrumentation).instrumentationName) { + instrumentations.push(option); + } else if ((option as OldClassPlugin).moduleName) { + pluginsWeb.push(option as OldClassPlugin); + } + } + + return { instrumentations, pluginsNode, pluginsWeb }; +} + +/** + * Enable instrumentations + * @param instrumentations + * @param logger + * @param tracerProvider + * @param meterProvider + */ +export function enableInstrumentations( + instrumentations: Instrumentation[], + logger: Logger, + tracerProvider?: TracerProvider, + meterProvider?: MeterProvider +) { + for (let i = 0, j = instrumentations.length; i < j; i++) { + const instrumentation = instrumentations[i]; + if (tracerProvider) { + instrumentation.setTracerProvider(tracerProvider); + } + if (meterProvider) { + instrumentation.setMeterProvider(meterProvider); + } + instrumentation.enable(); + } +} + +/** + * Disable instrumentations + * @param instrumentations + */ +export function disableInstrumentations(instrumentations: Instrumentation[]) { + instrumentations.forEach(instrumentation => instrumentation.disable()); +} diff --git a/packages/opentelemetry-instrumentation/src/index.ts b/packages/opentelemetry-instrumentation/src/index.ts index acb0986848..078c877124 100644 --- a/packages/opentelemetry-instrumentation/src/index.ts +++ b/packages/opentelemetry-instrumentation/src/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './autoLoader'; export * from './platform/index'; export * from './types'; export * from './utils'; diff --git a/packages/opentelemetry-instrumentation/src/platform/browser/index.ts b/packages/opentelemetry-instrumentation/src/platform/browser/index.ts index 24c76056a1..fc42da7384 100644 --- a/packages/opentelemetry-instrumentation/src/platform/browser/index.ts +++ b/packages/opentelemetry-instrumentation/src/platform/browser/index.ts @@ -15,3 +15,4 @@ */ export * from './instrumentation'; +export * from './old/autoLoader'; diff --git a/packages/opentelemetry-instrumentation/src/platform/browser/old/autoLoader.ts b/packages/opentelemetry-instrumentation/src/platform/browser/old/autoLoader.ts new file mode 100644 index 0000000000..1b95510efe --- /dev/null +++ b/packages/opentelemetry-instrumentation/src/platform/browser/old/autoLoader.ts @@ -0,0 +1,44 @@ +/* + * 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. + */ + +// This should be removed after plugins are gone + +import * as api from '@opentelemetry/api'; +import { NodePlugins, OldClassPlugin } from '../../../types_plugin_only'; + +/** + * Loads provided web plugins + * @param pluginsNode + * @param pluginsWeb + * @param logger + * @param tracerProvider + * @return returns function to disable all plugins + */ +export function loadOldPlugins( + pluginsNode: NodePlugins, + pluginsWeb: OldClassPlugin[], + logger: api.Logger, + tracerProvider: api.TracerProvider +): () => void { + pluginsWeb.forEach(plugin => { + plugin.enable([], tracerProvider, logger); + }); + return () => { + pluginsWeb.forEach(plugin => { + plugin.disable(); + }); + }; +} diff --git a/packages/opentelemetry-instrumentation/src/platform/node/index.ts b/packages/opentelemetry-instrumentation/src/platform/node/index.ts index 42310c807a..e5f3252405 100644 --- a/packages/opentelemetry-instrumentation/src/platform/node/index.ts +++ b/packages/opentelemetry-instrumentation/src/platform/node/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './old/autoLoader'; export * from './instrumentation'; export * from './instrumentationNodeModuleDefinition'; export * from './instrumentationNodeModuleFile'; diff --git a/packages/opentelemetry-instrumentation/src/platform/node/old/PluginLoader.ts b/packages/opentelemetry-instrumentation/src/platform/node/old/PluginLoader.ts new file mode 100644 index 0000000000..06b8068ac9 --- /dev/null +++ b/packages/opentelemetry-instrumentation/src/platform/node/old/PluginLoader.ts @@ -0,0 +1,229 @@ +/* + * 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. + */ + +// This is copy from previous version, should be removed after plugins are gone + +import { Logger, TracerProvider } from '@opentelemetry/api'; +import * as RequireInTheMiddle from 'require-in-the-middle'; +import { OldClassPlugin, OldPluginConfig } from '../../../types_plugin_only'; +import * as utils from './utils'; + +// States for the Plugin Loader +export enum HookState { + UNINITIALIZED, + ENABLED, + DISABLED, +} + +/** + * Environment variable which will contain list of modules to not load corresponding plugins for + * e.g.OTEL_NO_PATCH_MODULES=pg,https,mongodb + */ +export const ENV_PLUGIN_DISABLED_LIST = 'OTEL_NO_PATCH_MODULES'; + +/** + * Wildcard symbol. If ignore list is set to this, disable all plugins + */ +const DISABLE_ALL_PLUGINS = '*'; + +export interface Plugins { + [pluginName: string]: OldPluginConfig; +} + +/** + * Returns the Plugins object that meet the below conditions. + * Valid criteria: 1. It should be enabled. 2. Should have non-empty path. + */ +function filterPlugins(plugins: Plugins): Plugins { + const keys = Object.keys(plugins); + return keys.reduce((acc: Plugins, key: string) => { + if (plugins[key].enabled && (plugins[key].path || plugins[key].plugin)) { + acc[key] = plugins[key]; + } + return acc; + }, {}); +} + +/** + * Parse process.env[ENV_PLUGIN_DISABLED_LIST] for a list of modules + * not to load corresponding plugins for. + */ +function getIgnoreList(): string[] | typeof DISABLE_ALL_PLUGINS { + const envIgnoreList: string = process.env[ENV_PLUGIN_DISABLED_LIST] || ''; + if (envIgnoreList === DISABLE_ALL_PLUGINS) { + return envIgnoreList; + } + return envIgnoreList.split(',').map(v => v.trim()); +} + +/** + * The PluginLoader class can load instrumentation plugins that use a patch + * mechanism to enable automatic tracing for specific target modules. + */ +export class PluginLoader { + /** A list of loaded plugins. */ + plugins: OldClassPlugin[] = []; + /** + * A field that tracks whether the require-in-the-middle hook has been loaded + * for the first time, as well as whether the hook body is activated or not. + */ + private _hookState = HookState.UNINITIALIZED; + + /** Constructs a new PluginLoader instance. */ + constructor(readonly provider: TracerProvider, readonly logger: Logger) {} + + /** + * Loads a list of plugins. Each plugin module should implement the core + * {@link Plugin} interface and export an instance named as 'plugin'. This + * function will attach a hook to be called the first time the module is + * loaded. + * @param Plugins an object whose keys are plugin names and whose + * {@link OldPluginConfig} values indicate several configuration options. + */ + load(plugins: Plugins): PluginLoader { + if (this._hookState === HookState.UNINITIALIZED) { + const pluginsToLoad = filterPlugins(plugins); + const modulesToHook = Object.keys(pluginsToLoad); + const modulesToIgnore = getIgnoreList(); + // Do not hook require when no module is provided. In this case it is + // not necessary. With skipping this step we lower our footprint in + // customer applications and require-in-the-middle won't show up in CPU + // frames. + if (modulesToHook.length === 0) { + this._hookState = HookState.DISABLED; + return this; + } + + const alreadyRequiredModules = Object.keys(require.cache); + const requiredModulesToHook = modulesToHook.filter( + name => + alreadyRequiredModules.find(cached => { + try { + return require.resolve(name) === cached; + } catch (err) { + return false; + } + }) !== undefined + ); + if (requiredModulesToHook.length > 0) { + this.logger.warn( + `Some modules (${requiredModulesToHook.join( + ', ' + )}) were already required when their respective plugin was loaded, some plugins might not work. Make sure the SDK is setup before you require in other modules.` + ); + } + + // Enable the require hook. + RequireInTheMiddle(modulesToHook, (exports, name, baseDir) => { + if (this._hookState !== HookState.ENABLED) return exports; + const config = pluginsToLoad[name]; + const modulePath = config.path!; + const modulePlugin = config.plugin; + let version = null; + + if (!baseDir) { + // basedir is the directory where the module is located, + // or undefined for core modules. + // lets plugins restrict what they support for core modules (see plugin.supportedVersions) + version = process.versions.node; + } else { + // Get the module version. + version = utils.getPackageVersion(this.logger, baseDir); + } + + // Skip loading of all modules if '*' is provided + if (modulesToIgnore === DISABLE_ALL_PLUGINS) { + this.logger.info( + `PluginLoader#load: skipped patching module ${name} because all plugins are disabled (${ENV_PLUGIN_DISABLED_LIST})` + ); + return exports; + } + + if (modulesToIgnore.includes(name)) { + this.logger.info( + `PluginLoader#load: skipped patching module ${name} because it was on the ignore list (${ENV_PLUGIN_DISABLED_LIST})` + ); + return exports; + } + + this.logger.info( + `PluginLoader#load: trying to load ${name}@${version}` + ); + + if (!version) return exports; + + this.logger.debug( + `PluginLoader#load: applying patch to ${name}@${version} using ${modulePath} module` + ); + + // Expecting a plugin from module; + try { + const plugin: OldClassPlugin = + modulePlugin ?? require(modulePath).plugin; + if (!utils.isSupportedVersion(version, plugin.supportedVersions)) { + this.logger.warn( + `PluginLoader#load: Plugin ${name} only supports module ${plugin.moduleName} with the versions: ${plugin.supportedVersions}` + ); + return exports; + } + if (plugin.moduleName !== name) { + this.logger.error( + `PluginLoader#load: Entry ${name} use a plugin that instruments ${plugin.moduleName}` + ); + return exports; + } + + this.plugins.push(plugin); + // Enable each supported plugin. + return plugin.enable(exports, this.provider, this.logger, config); + } catch (e) { + this.logger.error( + `PluginLoader#load: could not load plugin ${modulePath} of module ${name}. Error: ${e.message}` + ); + return exports; + } + }); + this._hookState = HookState.ENABLED; + } else if (this._hookState === HookState.DISABLED) { + this.logger.error( + 'PluginLoader#load: Currently cannot re-enable plugin loader.' + ); + } else { + this.logger.error('PluginLoader#load: Plugin loader already enabled.'); + } + return this; + } + + /** Unloads plugins. */ + unload(): PluginLoader { + if (this._hookState === HookState.ENABLED) { + for (const plugin of this.plugins) { + plugin.disable(); + } + this.plugins = []; + this._hookState = HookState.DISABLED; + } + return this; + } +} + +/** + * Adds a search path for plugin modules. Intended for testing purposes only. + * @param searchPath The path to add. + */ +export function searchPathForTest(searchPath: string) { + module.paths.push(searchPath); +} diff --git a/packages/opentelemetry-instrumentation/src/platform/node/old/autoLoader.ts b/packages/opentelemetry-instrumentation/src/platform/node/old/autoLoader.ts new file mode 100644 index 0000000000..c3880b96ec --- /dev/null +++ b/packages/opentelemetry-instrumentation/src/platform/node/old/autoLoader.ts @@ -0,0 +1,83 @@ +/* + * 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. + */ + +// This should be removed after plugins are gone + +import * as api from '@opentelemetry/api'; +import { NodePlugins, OldClassPlugin } from '../../../types_plugin_only'; +import { PluginLoader } from './PluginLoader'; + +/** List of all default supported plugins */ +export const DEFAULT_INSTRUMENTATION_PLUGINS: NodePlugins = { + mongodb: { enabled: true, path: '@opentelemetry/plugin-mongodb' }, + grpc: { enabled: true, path: '@opentelemetry/plugin-grpc' }, + '@grpc/grpc-js': { enabled: true, path: '@opentelemetry/plugin-grpc-js' }, + http: { enabled: true, path: '@opentelemetry/plugin-http' }, + https: { enabled: true, path: '@opentelemetry/plugin-https' }, + mysql: { enabled: true, path: '@opentelemetry/plugin-mysql' }, + pg: { enabled: true, path: '@opentelemetry/plugin-pg' }, + redis: { enabled: true, path: '@opentelemetry/plugin-redis' }, + ioredis: { enabled: true, path: '@opentelemetry/plugin-ioredis' }, + 'pg-pool': { enabled: true, path: '@opentelemetry/plugin-pg-pool' }, + express: { enabled: true, path: '@opentelemetry/plugin-express' }, + '@hapi/hapi': { enabled: true, path: '@opentelemetry/hapi-instrumentation' }, + koa: { enabled: true, path: '@opentelemetry/koa-instrumentation' }, + dns: { enabled: true, path: '@opentelemetry/plugin-dns' }, +}; + +/** + * Loads provided node plugins + * @param pluginsNode + * @param pluginsWeb + * @param logger + * @param tracerProvider + * @return returns function to disable all plugins + */ +export function loadOldPlugins( + pluginsNode: NodePlugins, + pluginsWeb: OldClassPlugin[], + logger: api.Logger, + tracerProvider: api.TracerProvider +): () => void { + const allPlugins = mergePlugins(DEFAULT_INSTRUMENTATION_PLUGINS, pluginsNode); + const pluginLoader = new PluginLoader(tracerProvider, logger); + pluginLoader.load(allPlugins); + return () => { + pluginLoader.unload(); + }; +} + +function mergePlugins( + defaultPlugins: NodePlugins, + userSuppliedPlugins: NodePlugins +): NodePlugins { + const mergedUserSuppliedPlugins: NodePlugins = {}; + + for (const pluginName in userSuppliedPlugins) { + mergedUserSuppliedPlugins[pluginName] = { + // Any user-supplied non-default plugin should be enabled by default + ...(DEFAULT_INSTRUMENTATION_PLUGINS[pluginName] || { enabled: true }), + ...userSuppliedPlugins[pluginName], + }; + } + + const mergedPlugins: NodePlugins = { + ...defaultPlugins, + ...mergedUserSuppliedPlugins, + }; + + return mergedPlugins; +} diff --git a/packages/opentelemetry-instrumentation/src/platform/node/old/utils.ts b/packages/opentelemetry-instrumentation/src/platform/node/old/utils.ts new file mode 100644 index 0000000000..f02e48a703 --- /dev/null +++ b/packages/opentelemetry-instrumentation/src/platform/node/old/utils.ts @@ -0,0 +1,76 @@ +/* + * 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. + */ + +// This is copy from previous version, should be removed after plugins are gone + +import { Logger } from '@opentelemetry/api'; +import * as path from 'path'; +import * as semver from 'semver'; + +/** + * Gets the package version. + * @param logger The logger to use. + * @param basedir The base directory. + */ +export function getPackageVersion( + logger: Logger, + basedir: string +): string | null { + const pjsonPath = path.join(basedir, 'package.json'); + try { + const version = require(pjsonPath).version; + // Attempt to parse a string as a semantic version, returning either a + // SemVer object or null. + if (!semver.parse(version)) { + logger.error( + `getPackageVersion: [${pjsonPath}|${version}] Version string could not be parsed.` + ); + return null; + } + return version; + } catch (e) { + logger.error( + `getPackageVersion: [${pjsonPath}] An error occurred while retrieving version string. ${e.message}` + ); + return null; + } +} + +/** + * Determines if a version is supported + * @param moduleVersion a version in [semver](https://semver.org/spec/v2.0.0.html) format. + * @param [supportedVersions] a list of supported versions ([semver](https://semver.org/spec/v2.0.0.html) format). + */ +export function isSupportedVersion( + moduleVersion: string, + supportedVersions?: string[] +) { + if (!Array.isArray(supportedVersions) || supportedVersions.length === 0) { + return true; + } + + return supportedVersions.some(supportedVersion => + semver.satisfies(moduleVersion, supportedVersion) + ); +} + +/** + * Adds a search path for plugin modules. Intended for testing purposes only. + * @param searchPath The path to add. + */ +export function searchPathForTest(searchPath: string) { + module.paths.push(searchPath); +} diff --git a/packages/opentelemetry-instrumentation/src/types_internal.ts b/packages/opentelemetry-instrumentation/src/types_internal.ts new file mode 100644 index 0000000000..49a328db81 --- /dev/null +++ b/packages/opentelemetry-instrumentation/src/types_internal.ts @@ -0,0 +1,46 @@ +/* + * 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 { Logger, MeterProvider, TracerProvider } from '@opentelemetry/api'; +import { InstrumentationBase } from './platform'; +import { Instrumentation } from './types'; +import { + NodePlugins, + NodePluginsTracerConfiguration, + OldClassPlugin, +} from './types_plugin_only'; + +export type InstrumentationOption = + | typeof InstrumentationBase + | typeof InstrumentationBase[] + | Instrumentation + | Instrumentation[] + | NodePluginsTracerConfiguration + | OldClassPlugin + | OldClassPlugin[]; + +export interface AutoLoaderResult { + instrumentations: Instrumentation[]; + pluginsNode: NodePlugins; + pluginsWeb: OldClassPlugin[]; +} + +export interface AutoLoaderOptions { + instrumentations?: InstrumentationOption[]; + tracerProvider?: TracerProvider; + meterProvider?: MeterProvider; + logger?: Logger; +} diff --git a/packages/opentelemetry-instrumentation/src/types_plugin_only.ts b/packages/opentelemetry-instrumentation/src/types_plugin_only.ts new file mode 100644 index 0000000000..967ea3559b --- /dev/null +++ b/packages/opentelemetry-instrumentation/src/types_plugin_only.ts @@ -0,0 +1,116 @@ +/* + * 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 { Logger, TracerProvider } from '@opentelemetry/api'; + +export interface NodePlugins { + [pluginName: string]: OldPluginConfig; +} + +export interface NodePluginsTracerConfiguration { + plugins: NodePlugins; +} + +/** Interface Plugin to apply patch. */ +export interface OldClassPlugin { + /** + * Contains all supported versions. + * All versions must be compatible with [semver](https://semver.org/spec/v2.0.0.html) format. + * If the version is not supported, we won't apply instrumentation patch (see `enable` method). + * If omitted, all versions of the module will be patched. + */ + supportedVersions?: string[]; + + /** + * Name of the module that the plugin instrument. + */ + moduleName: string; + + /** + * Method that enables the instrumentation patch. + * @param moduleExports The value of the `module.exports` property that would + * normally be exposed by the required module. ex: `http`, `https` etc. + * @param TracerProvider a tracer provider. + * @param logger a logger instance. + * @param [config] an object to configure the plugin. + */ + enable( + moduleExports: T, + TracerProvider: TracerProvider, + logger: Logger, + config?: OldPluginConfig + ): T; + + /** Method to disable the instrumentation */ + disable(): void; +} + +export interface OldPluginConfig { + /** + * Whether to enable the plugin. + * @default true + */ + enabled?: boolean; + + /** + * Path of the trace plugin to load. + * @default '@opentelemetry/plugin-http' in case of http. + */ + path?: string; + + /** + * Plugin to load + * @example import {plugin} from '@opentelemetry/plugin-http' in case of http. + */ + plugin?: OldClassPlugin; + + /** + * Request methods that match any string in ignoreMethods will not be traced. + */ + ignoreMethods?: string[]; + + /** + * URLs that partially match any regex in ignoreUrls will not be traced. + * In addition, URLs that are _exact matches_ of strings in ignoreUrls will + * also not be traced. + */ + ignoreUrls?: Array; + + /** + * List of internal files that need patch and are not exported by + * default. + */ + internalFilesExports?: PluginInternalFiles; + + /** + * If true, additional information about query parameters and + * results will be attached (as `attributes`) to spans representing + * database operations. + */ + enhancedDatabaseReporting?: boolean; +} + +export interface PluginInternalFilesVersion { + [pluginName: string]: string; +} + +/** + * Each key should be the name of the module to trace, and its value + * a mapping of a property name to a internal plugin file name. + */ +export interface PluginInternalFiles { + [versions: string]: PluginInternalFilesVersion; +} diff --git a/packages/opentelemetry-instrumentation/test/browser/autoLoader.test.ts b/packages/opentelemetry-instrumentation/test/browser/autoLoader.test.ts new file mode 100644 index 0000000000..1f3e6d5cc6 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/browser/autoLoader.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { NOOP_METER_PROVIDER, NOOP_TRACER_PROVIDER } from '@opentelemetry/api'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; + +import { registerInstrumentations } from '../../src'; + +import { OldClassPlugin } from '../../src/types_plugin_only'; + +class WebPlugin implements OldClassPlugin { + moduleName = 'WebPlugin'; + enable() {} + disable() {} +} + +describe('autoLoader', () => { + let sandbox: sinon.SinonSandbox; + let unload: Function | undefined; + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + afterEach(() => { + sandbox.restore(); + if (typeof unload === 'function') { + unload(); + unload = undefined; + } + }); + + describe('registerInstrumentations', () => { + describe('Old Plugins', () => { + let enableSpy: sinon.SinonSpy; + const tracerProvider = NOOP_TRACER_PROVIDER; + const meterProvider = NOOP_METER_PROVIDER; + let webPlugin: WebPlugin; + beforeEach(() => { + webPlugin = new WebPlugin(); + enableSpy = sandbox.spy(webPlugin, 'enable'); + unload = registerInstrumentations({ + instrumentations: [webPlugin], + tracerProvider, + meterProvider, + }); + }); + afterEach(() => { + if (typeof unload === 'function') { + unload(); + unload = undefined; + } + }); + + it('should enable a required plugin', () => { + assert.strictEqual(enableSpy.callCount, 1); + }); + + it('should set TracerProvider', () => { + assert.ok(enableSpy.lastCall.args[1] === tracerProvider); + }); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts b/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts new file mode 100644 index 0000000000..6916c61157 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { NOOP_METER_PROVIDER, NOOP_TRACER_PROVIDER } from '@opentelemetry/api'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; + +import { InstrumentationBase, registerInstrumentations } from '../../src'; + +class FooInstrumentation extends InstrumentationBase { + init() { + return []; + } + enable() {} + disable() {} +} + +describe('autoLoader', () => { + let sandbox: sinon.SinonSandbox; + let unload: Function | undefined; + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + afterEach(() => { + sandbox.restore(); + if (typeof unload === 'function') { + unload(); + unload = undefined; + } + }); + + describe('registerInstrumentations', () => { + describe('InstrumentationBase', () => { + let instrumentation: InstrumentationBase; + let enableSpy: sinon.SinonSpy; + let setTracerProviderSpy: sinon.SinonSpy; + let setsetMeterProvider: sinon.SinonSpy; + const tracerProvider = NOOP_TRACER_PROVIDER; + const meterProvider = NOOP_METER_PROVIDER; + beforeEach(() => { + instrumentation = new FooInstrumentation('foo', '1', {}); + enableSpy = sandbox.spy(instrumentation, 'enable'); + setTracerProviderSpy = sandbox.stub( + instrumentation, + 'setTracerProvider' + ); + setsetMeterProvider = sandbox.stub(instrumentation, 'setMeterProvider'); + unload = registerInstrumentations({ + instrumentations: [instrumentation], + tracerProvider, + meterProvider, + }); + }); + + afterEach(() => { + Object.keys(require.cache).forEach(key => delete require.cache[key]); + if (typeof unload === 'function') { + unload(); + unload = undefined; + } + }); + + it('should enable instrumentation', () => { + assert.strictEqual(enableSpy.callCount, 1); + }); + + it('should set TracerProvider', () => { + assert.strictEqual(setTracerProviderSpy.callCount, 1); + assert.ok(setTracerProviderSpy.lastCall.args[0] === tracerProvider); + assert.strictEqual(setTracerProviderSpy.lastCall.args.length, 1); + }); + + it('should set MeterProvider', () => { + assert.strictEqual(setsetMeterProvider.callCount, 1); + assert.ok(setsetMeterProvider.lastCall.args[0] === meterProvider); + assert.strictEqual(setsetMeterProvider.lastCall.args.length, 1); + }); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation/test/common/autoLoaderUtils.test.ts b/packages/opentelemetry-instrumentation/test/common/autoLoaderUtils.test.ts new file mode 100644 index 0000000000..f234ad9c16 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/common/autoLoaderUtils.test.ts @@ -0,0 +1,117 @@ +/* + * 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 assert from 'assert'; +import * as sinon from 'sinon'; +import { InstrumentationBase } from '../../src'; +import { parseInstrumentationOptions } from '../../src/autoLoaderUtils'; +import { InstrumentationOption } from '../../src/types_internal'; +import { OldClassPlugin } from '../../src/types_plugin_only'; + +class FooInstrumentation extends InstrumentationBase { + constructor() { + super('foo', '1', {}); + } + + init() { + return []; + } + + enable() {} + + disable() {} +} + +class FooWebPlugin implements OldClassPlugin { + moduleName = 'foo'; + + enable() {} + + disable() {} +} + +// const fooInstrumentation = new FooInstrumentation(); + +describe('autoLoaderUtils', () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('parseInstrumentationOptions', () => { + it('should create a new instrumentation from class', () => { + const { instrumentations } = parseInstrumentationOptions([ + FooInstrumentation, + ]); + assert.strictEqual(instrumentations.length, 1); + const instrumentation = instrumentations[0]; + assert.ok(instrumentation instanceof InstrumentationBase); + }); + + it('should return an instrumentation from Instrumentation', () => { + const { instrumentations } = parseInstrumentationOptions([ + new FooInstrumentation(), + ]); + assert.strictEqual(instrumentations.length, 1); + const instrumentation = instrumentations[0]; + assert.ok(instrumentation instanceof InstrumentationBase); + }); + + it('should return node old plugin', () => { + const { pluginsNode } = parseInstrumentationOptions([ + { + plugins: { + http: { enabled: false }, + }, + }, + ]); + assert.strictEqual(Object.keys(pluginsNode).length, 1); + }); + + it('should return web old plugin', () => { + const { pluginsWeb } = parseInstrumentationOptions([new FooWebPlugin()]); + assert.strictEqual(pluginsWeb.length, 1); + }); + + it('should handle mix of plugins and instrumentations', () => { + const nodePlugins = { + plugins: { + http: { enabled: false }, + https: { enabled: false }, + }, + }; + const options: InstrumentationOption[] = []; + + options.push(new FooWebPlugin()); + options.push(nodePlugins); + options.push([new FooInstrumentation(), new FooInstrumentation()]); + options.push([new FooWebPlugin(), new FooWebPlugin()]); + + const { + pluginsWeb, + pluginsNode, + instrumentations, + } = parseInstrumentationOptions(options); + + assert.strictEqual(pluginsWeb.length, 3); + assert.strictEqual(Object.keys(pluginsNode).length, 2); + assert.strictEqual(instrumentations.length, 2); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation/test/node/BasePlugin.ts b/packages/opentelemetry-instrumentation/test/node/BasePlugin.ts new file mode 100644 index 0000000000..462dc6cb65 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/BasePlugin.ts @@ -0,0 +1,45 @@ +/* + * 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 { Logger, TracerProvider } from '@opentelemetry/api'; +import { OldClassPlugin, OldPluginConfig } from '../../src/types_plugin_only'; + +/** This class represent the base to patch plugin. */ +export abstract class BasePlugin implements OldClassPlugin { + abstract readonly moduleName: string; // required for internalFilesExports + protected _moduleExports!: T; + constructor( + protected readonly _tracerName: string, + protected readonly _tracerVersion?: string + ) {} + + enable( + moduleExports: T, + tracerProvider: TracerProvider, + logger: Logger, + config?: OldPluginConfig + ): T { + this._moduleExports = moduleExports; + return this.patch(); + } + + disable(): void { + this.unpatch(); + } + + protected abstract patch(): T; + protected abstract unpatch(): void; +} diff --git a/packages/opentelemetry-instrumentation/test/node/PluginLoader.test.ts b/packages/opentelemetry-instrumentation/test/node/PluginLoader.test.ts new file mode 100644 index 0000000000..6407f15029 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/PluginLoader.test.ts @@ -0,0 +1,364 @@ +/* + * 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 { NoopLogger, NoopTracerProvider } from '@opentelemetry/api'; +import * as assert from 'assert'; +import * as path from 'path'; +import { + HookState, + PluginLoader, + Plugins, + searchPathForTest, + ENV_PLUGIN_DISABLED_LIST, +} from '../../src/platform/node/old/PluginLoader'; + +const INSTALLED_PLUGINS_PATH = path.join(__dirname, 'node_modules'); +/* eslint-disable node/no-extraneous-require */ +const simplePlugins: Plugins = { + 'simple-module': { + enabled: true, + path: '@opentelemetry/plugin-simple-module', + ignoreMethods: [], + ignoreUrls: [], + }, +}; + +const httpPlugins: Plugins = { + http: { + enabled: true, + path: '@opentelemetry/plugin-http-module', + ignoreMethods: [], + ignoreUrls: [], + }, +}; + +const disablePlugins: Plugins = { + 'simple-module': { + enabled: false, + path: '@opentelemetry/plugin-simple-module', + }, + nonexistent: { + enabled: false, + path: '@opentelemetry/plugin-nonexistent-module', + }, +}; + +const nonexistentPlugins: Plugins = { + nonexistent: { + enabled: true, + path: '@opentelemetry/plugin-nonexistent-module', + }, +}; + +const missingPathPlugins: Plugins = { + 'simple-module': { + enabled: true, + }, + nonexistent: { + enabled: true, + }, +}; + +const supportedVersionPlugins: Plugins = { + 'supported-module': { + enabled: true, + path: '@opentelemetry/plugin-supported-module', + }, +}; + +const notSupportedVersionPlugins: Plugins = { + 'notsupported-module': { + enabled: true, + path: 'notsupported-module', + }, +}; + +const alreadyRequiredPlugins: Plugins = { + 'already-require-module': { + enabled: true, + path: '@opentelemetry/plugin-supported-module', + }, +}; + +const differentNamePlugins: Plugins = { + 'random-module': { + enabled: true, + path: '@opentelemetry/plugin-http-module', + }, +}; + +describe('PluginLoader', () => { + const provider = new NoopTracerProvider(); + const logger = new NoopLogger(); + + before(() => { + module.paths.push(INSTALLED_PLUGINS_PATH); + searchPathForTest(INSTALLED_PLUGINS_PATH); + }); + + afterEach(() => { + // clear require cache + Object.keys(require.cache).forEach(key => delete require.cache[key]); + }); + + describe('.state()', () => { + it('returns UNINITIALIZED when first called', () => { + const pluginLoader = new PluginLoader(provider, logger); + assert.strictEqual(pluginLoader['_hookState'], HookState.UNINITIALIZED); + }); + + it('transitions from UNINITIALIZED to ENABLED', () => { + const pluginLoader = new PluginLoader(provider, logger); + pluginLoader.load(simplePlugins); + assert.strictEqual(pluginLoader['_hookState'], HookState.ENABLED); + pluginLoader.unload(); + }); + + it('transitions from ENABLED to DISABLED', () => { + const pluginLoader = new PluginLoader(provider, logger); + pluginLoader.load(simplePlugins).unload(); + assert.strictEqual(pluginLoader['_hookState'], HookState.DISABLED); + }); + }); + + describe('.load()', () => { + afterEach(() => { + delete process.env[ENV_PLUGIN_DISABLED_LIST]; + }); + + it('sanity check', () => { + // Ensure that module fixtures contain values that we expect. + const simpleModule = require('simple-module'); + const simpleModule001 = require('supported-module'); + const simpleModule100 = require('notsupported-module'); + + assert.strictEqual(simpleModule.name(), 'simple-module'); + assert.strictEqual(simpleModule001.name(), 'supported-module'); + assert.strictEqual(simpleModule100.name(), 'notsupported-module'); + + assert.strictEqual(simpleModule.value(), 0); + assert.strictEqual(simpleModule001.value(), 0); + assert.strictEqual(simpleModule100.value(), 0); + + assert.throws(() => require('nonexistent-module')); + }); + + it('should not load a plugin on the ignore list environment variable', () => { + // Set ignore list env var + process.env[ENV_PLUGIN_DISABLED_LIST] = 'simple-module'; + const pluginLoader = new PluginLoader(provider, logger); + pluginLoader.load({ ...simplePlugins, ...supportedVersionPlugins }); + + assert.strictEqual(pluginLoader['plugins'].length, 0); + + const simpleModule = require('simple-module'); + assert.strictEqual(pluginLoader['plugins'].length, 0); + assert.strictEqual(simpleModule.value(), 0); + assert.strictEqual(simpleModule.name(), 'simple-module'); + + const supportedModule = require('supported-module'); + assert.strictEqual(pluginLoader['plugins'].length, 1); + assert.strictEqual(supportedModule.value(), 1); + assert.strictEqual(supportedModule.name(), 'patched-supported-module'); + + pluginLoader.unload(); + }); + + it('should not load plugins on the ignore list environment variable', () => { + // Set ignore list env var + process.env[ENV_PLUGIN_DISABLED_LIST] = 'simple-module,http'; + const pluginLoader = new PluginLoader(provider, logger); + pluginLoader.load({ + ...simplePlugins, + ...supportedVersionPlugins, + ...httpPlugins, + }); + + assert.strictEqual(pluginLoader['plugins'].length, 0); + + const simpleModule = require('simple-module'); + assert.strictEqual(pluginLoader['plugins'].length, 0); + assert.strictEqual(simpleModule.value(), 0); + assert.strictEqual(simpleModule.name(), 'simple-module'); + + const httpModule = require('http'); + assert.ok(httpModule); + assert.strictEqual(pluginLoader['plugins'].length, 0); + + const supportedModule = require('supported-module'); + assert.strictEqual(pluginLoader['plugins'].length, 1); + assert.strictEqual(supportedModule.value(), 1); + assert.strictEqual(supportedModule.name(), 'patched-supported-module'); + + pluginLoader.unload(); + }); + + it('should not load any plugins if ignore list environment variable is set to "*"', () => { + // Set ignore list env var + process.env[ENV_PLUGIN_DISABLED_LIST] = '*'; + const pluginLoader = new PluginLoader(provider, logger); + pluginLoader.load({ + ...simplePlugins, + ...supportedVersionPlugins, + ...httpPlugins, + }); + + assert.strictEqual(pluginLoader['plugins'].length, 0); + + const simpleModule = require('simple-module'); + const httpModule = require('http'); + const supportedModule = require('supported-module'); + + assert.strictEqual( + pluginLoader['plugins'].length, + 0, + 'No plugins were loaded' + ); + assert.strictEqual(simpleModule.value(), 0); + assert.strictEqual(simpleModule.name(), 'simple-module'); + assert.ok(httpModule); + assert.strictEqual(supportedModule.value(), 0); + assert.strictEqual(supportedModule.name(), 'supported-module'); + + pluginLoader.unload(); + }); + + it('should load a plugin and patch the target modules', () => { + const pluginLoader = new PluginLoader(provider, logger); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.load(simplePlugins); + // The hook is only called the first time the module is loaded. + const simpleModule = require('simple-module'); + assert.strictEqual(pluginLoader['plugins'].length, 1); + assert.strictEqual(simpleModule.value(), 1); + assert.strictEqual(simpleModule.name(), 'patched-simple-module'); + pluginLoader.unload(); + }); + + it('should load a plugin and patch the core module', () => { + const pluginLoader = new PluginLoader(provider, logger); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.load(httpPlugins); + // The hook is only called the first time the module is loaded. + const httpModule = require('http'); + assert.strictEqual(pluginLoader['plugins'].length, 1); + assert.strictEqual(httpModule.get(), 'patched'); + pluginLoader.unload(); + }); + // @TODO: simplify this test once we can load module with custom path + it('should not load the plugin when supported versions does not match', () => { + const pluginLoader = new PluginLoader(provider, logger); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.load(notSupportedVersionPlugins); + // The hook is only called the first time the module is loaded. + require('notsupported-module'); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.unload(); + }); + // @TODO: simplify this test once we can load module with custom path + it('should load a plugin and patch the target modules when supported versions match', () => { + const pluginLoader = new PluginLoader(provider, logger); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.load(supportedVersionPlugins); + // The hook is only called the first time the module is loaded. + const simpleModule = require('supported-module'); + assert.strictEqual(pluginLoader['plugins'].length, 1); + assert.strictEqual(simpleModule.value(), 1); + assert.strictEqual(simpleModule.name(), 'patched-supported-module'); + pluginLoader.unload(); + }); + + it('should not load a plugin when value is false', () => { + const pluginLoader = new PluginLoader(provider, logger); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.load(disablePlugins); + const simpleModule = require('simple-module'); + assert.strictEqual(pluginLoader['plugins'].length, 0); + assert.strictEqual(simpleModule.value(), 0); + assert.strictEqual(simpleModule.name(), 'simple-module'); + pluginLoader.unload(); + }); + + it('should not load a plugin when value is true but path is missing', () => { + const pluginLoader = new PluginLoader(provider, logger); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.load(missingPathPlugins); + const simpleModule = require('simple-module'); + assert.strictEqual(pluginLoader['plugins'].length, 0); + assert.strictEqual(simpleModule.value(), 0); + assert.strictEqual(simpleModule.name(), 'simple-module'); + pluginLoader.unload(); + }); + + it('should not load a non existing plugin', () => { + const pluginLoader = new PluginLoader(provider, logger); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.load(nonexistentPlugins); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.unload(); + }); + + it("doesn't patch modules for which plugins aren't specified", () => { + const pluginLoader = new PluginLoader(provider, logger); + pluginLoader.load({}); + assert.strictEqual(require('simple-module').value(), 0); + pluginLoader.unload(); + }); + + it('should warn when module was already loaded', callback => { + const verifyWarnLogger = { + error: logger.error, + info: logger.info, + debug: logger.debug, + warn: (message: string, ...args: unknown[]) => { + assert(message.match(/were already required when/)); + assert(message.match(/(already-require-module)/)); + return callback(); + }, + }; + require('already-require-module'); + const pluginLoader = new PluginLoader(provider, verifyWarnLogger); + pluginLoader.load(alreadyRequiredPlugins); + pluginLoader.unload(); + }); + + it('should not load a plugin that patches a different module that the one configured', () => { + const pluginLoader = new PluginLoader(provider, logger); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.load(differentNamePlugins); + require('random-module'); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.unload(); + }); + }); + + describe('.unload()', () => { + it('should unload the plugins and unpatch the target module when unloads', () => { + const pluginLoader = new PluginLoader(provider, logger); + assert.strictEqual(pluginLoader['plugins'].length, 0); + pluginLoader.load(simplePlugins); + // The hook is only called the first time the module is loaded. + const simpleModule = require('simple-module'); + assert.strictEqual(pluginLoader['plugins'].length, 1); + assert.strictEqual(simpleModule.value(), 1); + assert.strictEqual(simpleModule.name(), 'patched-simple-module'); + pluginLoader.unload(); + assert.strictEqual(pluginLoader['plugins'].length, 0); + assert.strictEqual(simpleModule.name(), 'simple-module'); + assert.strictEqual(simpleModule.value(), 0); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation/test/node/autoLoader.test.ts b/packages/opentelemetry-instrumentation/test/node/autoLoader.test.ts new file mode 100644 index 0000000000..5e14c2424e --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/autoLoader.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { NOOP_METER_PROVIDER, NOOP_TRACER_PROVIDER } from '@opentelemetry/api'; +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; + +import { registerInstrumentations } from '../../src'; +import { + Plugins, + searchPathForTest, +} from '../../src/platform/node/old/PluginLoader'; + +const INSTALLED_PLUGINS_PATH = path.join(__dirname, 'node_modules'); + +const httpPlugin: Plugins = { + http: { + enabled: true, + path: '@opentelemetry/plugin-http-module', + ignoreMethods: [], + ignoreUrls: [], + }, +}; + +describe('autoLoader', () => { + let sandbox: sinon.SinonSandbox; + let unload: Function | undefined; + before(() => { + module.paths.push(INSTALLED_PLUGINS_PATH); + searchPathForTest(INSTALLED_PLUGINS_PATH); + }); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + afterEach(() => { + sandbox.restore(); + Object.keys(require.cache).forEach(key => delete require.cache[key]); + if (typeof unload === 'function') { + unload(); + unload = undefined; + } + }); + + describe('registerInstrumentations', () => { + describe('Old Plugins', () => { + let enableSpy: sinon.SinonSpy; + const tracerProvider = NOOP_TRACER_PROVIDER; + const meterProvider = NOOP_METER_PROVIDER; + beforeEach(() => { + // eslint-disable-next-line node/no-extraneous-require + const simpleModule = require('@opentelemetry/plugin-simple-module') + .plugin; + enableSpy = sandbox.spy(simpleModule, 'enable'); + unload = registerInstrumentations({ + instrumentations: [ + { + plugins: { + ...httpPlugin, + 'simple-module': { enabled: true, plugin: simpleModule }, + }, + }, + ], + tracerProvider, + meterProvider, + }); + }); + afterEach(() => { + Object.keys(require.cache).forEach(key => delete require.cache[key]); + if (typeof unload === 'function') { + unload(); + unload = undefined; + } + }); + + it('should enable a required plugin', () => { + // eslint-disable-next-line node/no-extraneous-require + const simpleModule = require('simple-module'); + assert.ok(simpleModule); + assert.strictEqual(enableSpy.callCount, 1); + }); + + it('should set TracerProvider', () => { + // eslint-disable-next-line node/no-extraneous-require + const simpleModule = require('simple-module'); + assert.ok(simpleModule); + assert.ok(enableSpy.lastCall.args[1] === tracerProvider); + }); + }); + }); +}); diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-http-module/http-module.js b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-http-module/http-module.js new file mode 100644 index 0000000000..99c16a8af2 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-http-module/http-module.js @@ -0,0 +1,22 @@ +Object.defineProperty(exports, "__esModule", { value: true }); +const { BasePlugin } = require('../../../BasePlugin'); +const shimmer = require("shimmer"); + +class HttpModulePlugin extends BasePlugin { + constructor() { + super(); + this.moduleName = 'http'; + } + + patch() { + shimmer.wrap(this._moduleExports, 'get', orig => () => 'patched'); + return this._moduleExports; + } + + unpatch() { + shimmer.unwrap(this._moduleExports, 'get'); + } +} +exports.HttpModulePlugin = HttpModulePlugin; +const plugin = new HttpModulePlugin(); +exports.plugin = plugin; diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-http-module/index.js b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-http-module/index.js new file mode 100644 index 0000000000..4847af1405 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-http-module/index.js @@ -0,0 +1,5 @@ +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +Object.defineProperty(exports, "__esModule", { value: true }); +__export(require("./http-module")); diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-http-module/package.json b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-http-module/package.json new file mode 100644 index 0000000000..bb40eab67d --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-http-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "@opentelemetry/plugin-http-module", + "version": "0.0.1" +} diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-notsupported-module/index.js b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-notsupported-module/index.js new file mode 100644 index 0000000000..1b22b5ce90 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-notsupported-module/index.js @@ -0,0 +1,5 @@ +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +Object.defineProperty(exports, "__esModule", { value: true }); +__export(require("./simple-module")); diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-notsupported-module/package.json b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-notsupported-module/package.json new file mode 100644 index 0000000000..4db9e49b1d --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-notsupported-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "@opentelemetry/plugin-notsupported-module", + "version": "1.0.0" +} diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-notsupported-module/simple-module.js b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-notsupported-module/simple-module.js new file mode 100644 index 0000000000..3001ad4c61 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-notsupported-module/simple-module.js @@ -0,0 +1,25 @@ +Object.defineProperty(exports, "__esModule", { value: true }); +const { BasePlugin } = require('../../../BasePlugin'); +const shimmer = require("shimmer"); + +class SimpleModulePlugin extends BasePlugin { + constructor() { + super(); + this.moduleName = 'notsupported-module'; + } + + patch() { + shimmer.wrap(this._moduleExports, 'name', orig => () => 'patched-' + orig.apply()); + shimmer.wrap(this._moduleExports, 'value', orig => () => orig.apply() + 1); + return this._moduleExports; + } + + unpatch() { + shimmer.unwrap(this._moduleExports, 'name'); + shimmer.unwrap(this._moduleExports, 'value'); + } +} +exports.SimpleModulePlugin = SimpleModulePlugin; +const plugin = new SimpleModulePlugin(); +plugin.supportedVersions = ['1.0.0']; +exports.plugin = plugin; diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-simple-module/index.js b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-simple-module/index.js new file mode 100644 index 0000000000..1b22b5ce90 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-simple-module/index.js @@ -0,0 +1,5 @@ +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +Object.defineProperty(exports, "__esModule", { value: true }); +__export(require("./simple-module")); diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-simple-module/package.json b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-simple-module/package.json new file mode 100644 index 0000000000..59d87df350 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-simple-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "@opentelemetry/plugin-simple-module", + "version": "0.0.1" +} diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-simple-module/simple-module.js b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-simple-module/simple-module.js new file mode 100644 index 0000000000..3cfacba5fa --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-simple-module/simple-module.js @@ -0,0 +1,24 @@ +Object.defineProperty(exports, "__esModule", { value: true }); +const { BasePlugin } = require('../../../BasePlugin'); +const shimmer = require("shimmer"); + +class SimpleModulePlugin extends BasePlugin { + constructor() { + super(); + this.moduleName = 'simple-module'; + } + + patch() { + shimmer.wrap(this._moduleExports, 'name', orig => () => 'patched-' + orig.apply()); + shimmer.wrap(this._moduleExports, 'value', orig => () => orig.apply() + 1); + return this._moduleExports; + } + + unpatch() { + shimmer.unwrap(this._moduleExports, 'name'); + shimmer.unwrap(this._moduleExports, 'value'); + } +} +exports.SimpleModulePlugin = SimpleModulePlugin; +const plugin = new SimpleModulePlugin(); +exports.plugin = plugin; diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-supported-module/index.js b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-supported-module/index.js new file mode 100644 index 0000000000..1b22b5ce90 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-supported-module/index.js @@ -0,0 +1,5 @@ +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +Object.defineProperty(exports, "__esModule", { value: true }); +__export(require("./simple-module")); diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-supported-module/package.json b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-supported-module/package.json new file mode 100644 index 0000000000..ca18bafa63 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-supported-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "@opentelemetry/plugin-supported-module", + "version": "0.0.1" +} diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-supported-module/simple-module.js b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-supported-module/simple-module.js new file mode 100644 index 0000000000..3a77642f79 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/@opentelemetry/plugin-supported-module/simple-module.js @@ -0,0 +1,25 @@ +Object.defineProperty(exports, "__esModule", { value: true }); +const { BasePlugin } = require('../../../BasePlugin'); +const shimmer = require("shimmer"); + +class SimpleModulePlugin extends BasePlugin { + constructor() { + super(); + this.moduleName = 'supported-module'; + } + + patch() { + shimmer.wrap(this._moduleExports, 'name', orig => () => 'patched-' + orig.apply()); + shimmer.wrap(this._moduleExports, 'value', orig => () => orig.apply() + 1); + return this._moduleExports; + } + + unpatch() { + shimmer.unwrap(this._moduleExports, 'name'); + shimmer.unwrap(this._moduleExports, 'value'); + } +} +exports.SimpleModulePlugin = SimpleModulePlugin; +const plugin = new SimpleModulePlugin(); +plugin.supportedVersions = ['^0.0.1']; +exports.plugin = plugin; diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/already-require-module/index.js b/packages/opentelemetry-instrumentation/test/node/node_modules/already-require-module/index.js new file mode 100644 index 0000000000..18c0f69a3b --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/already-require-module/index.js @@ -0,0 +1,4 @@ +module.exports = { + name: () => 'already-module', + value: () => 0, +}; diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/already-require-module/package.json b/packages/opentelemetry-instrumentation/test/node/node_modules/already-require-module/package.json new file mode 100644 index 0000000000..7ae0ab8f09 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/already-require-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "already-module", + "version": "0.1.0" +} diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/notsupported-module/index.js b/packages/opentelemetry-instrumentation/test/node/node_modules/notsupported-module/index.js new file mode 100644 index 0000000000..4fe98dae33 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/notsupported-module/index.js @@ -0,0 +1,4 @@ +module.exports = { + name: () => 'notsupported-module', + value: () => 0, +}; diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/notsupported-module/package.json b/packages/opentelemetry-instrumentation/test/node/node_modules/notsupported-module/package.json new file mode 100644 index 0000000000..9494b2866e --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/notsupported-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "notsupported-module", + "version": "0.0.1" +} diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/random-module/index.js b/packages/opentelemetry-instrumentation/test/node/node_modules/random-module/index.js new file mode 100644 index 0000000000..35a4110c28 --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/random-module/index.js @@ -0,0 +1,4 @@ +module.exports = { + name: () => 'random-module', + value: () => 0, +}; diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/random-module/package.json b/packages/opentelemetry-instrumentation/test/node/node_modules/random-module/package.json new file mode 100644 index 0000000000..a5c840081b --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/random-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "random-module", + "version": "0.1.0" +} diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/simple-module/index.js b/packages/opentelemetry-instrumentation/test/node/node_modules/simple-module/index.js new file mode 100644 index 0000000000..8ec2e77ffd --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/simple-module/index.js @@ -0,0 +1,4 @@ +module.exports = { + name: () => 'simple-module', + value: () => 0, +}; diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/simple-module/package.json b/packages/opentelemetry-instrumentation/test/node/node_modules/simple-module/package.json new file mode 100644 index 0000000000..2eba36a5bd --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/simple-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "simple-module", + "version": "0.1.0" +} diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/supported-module/index.js b/packages/opentelemetry-instrumentation/test/node/node_modules/supported-module/index.js new file mode 100644 index 0000000000..090d0db5fb --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/supported-module/index.js @@ -0,0 +1,4 @@ +module.exports = { + name: () => 'supported-module', + value: () => 0, +}; diff --git a/packages/opentelemetry-instrumentation/test/node/node_modules/supported-module/package.json b/packages/opentelemetry-instrumentation/test/node/node_modules/supported-module/package.json new file mode 100644 index 0000000000..ffd520afda --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/node_modules/supported-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "supported-module", + "version": "0.0.1" +} diff --git a/packages/opentelemetry-instrumentation/test/node/utils.test.ts b/packages/opentelemetry-instrumentation/test/node/utils.test.ts new file mode 100644 index 0000000000..273c98e2dc --- /dev/null +++ b/packages/opentelemetry-instrumentation/test/node/utils.test.ts @@ -0,0 +1,95 @@ +/* + * 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 { NoopLogger } from '@opentelemetry/api'; +import * as assert from 'assert'; +import * as path from 'path'; +import * as utils from '../../src/platform/node/old/utils'; + +const INSTALLED_PLUGINS_PATH = path.join(__dirname, 'node_modules'); +const TEST_MODULES: Array<{ name: string; version: string | null }> = [ + { + name: 'simple-module', + version: '0.1.0', + }, + { + name: 'nonexistent-module', + version: null, + }, + { + name: 'http', + version: null, + }, +]; + +describe('Instrumentation#utils', () => { + const logger = new NoopLogger(); + + before(() => { + utils.searchPathForTest(INSTALLED_PLUGINS_PATH); + }); + + describe('getPackageVersion', () => { + TEST_MODULES.forEach(testCase => { + it(`should return ${testCase.version} for ${testCase.name}`, () => { + assert.strictEqual( + utils.getPackageVersion(logger, testCase.name), + testCase.version + ); + }); + }); + }); + describe('isSupportedVersion', () => { + const version = '1.0.1'; + + it('should return true when supportedVersions is not defined', () => { + assert.strictEqual(utils.isSupportedVersion('1.0.0', undefined), true); + }); + + [ + ['1.X'], + [version], + ['1.X.X', '3.X.X'], + ['^1.0.0'], + ['~1.0.0', '^0.1.0'], + ['*'], + ['>1.0.0'], + [], + ].forEach(supportedVersion => { + it(`should return true when version is equal to ${version} and supportedVersions is equal to ${supportedVersion}`, () => { + assert.strictEqual( + utils.isSupportedVersion(version, supportedVersion), + true + ); + }); + }); + + [['0.X'], ['0.1.0'], ['0.X.X'], ['^0.1.0'], ['1.0.0'], ['<1.0.0']].forEach( + supportedVersion => { + it(`should return false when version is equal to ${version} and supportedVersions is equal to ${supportedVersion}`, () => { + assert.strictEqual( + utils.isSupportedVersion(version, supportedVersion), + false + ); + }); + } + ); + + it("should return false when version is equal to null and supportedVersions is equal to '*'", () => { + assert.strictEqual(utils.isSupportedVersion(null as any, ['*']), false); + }); + }); +});