From 8c56442e297776bc4164d1079108b872d741c846 Mon Sep 17 00:00:00 2001 From: Mark Wolff Date: Fri, 12 Jun 2020 09:58:38 -0400 Subject: [PATCH] feat: add OPENTELEMETRY_NO_PATCH_MODULES (#1153) Co-authored-by: Daniel Dyla --- packages/opentelemetry-node/README.md | 8 ++ .../src/instrumentation/PluginLoader.ts | 39 +++++++++ .../test/instrumentation/PluginLoader.test.ts | 85 +++++++++++++++++++ 3 files changed, 132 insertions(+) diff --git a/packages/opentelemetry-node/README.md b/packages/opentelemetry-node/README.md index df932e2ed16..258e95ff80b 100644 --- a/packages/opentelemetry-node/README.md +++ b/packages/opentelemetry-node/README.md @@ -102,6 +102,14 @@ const provider = new NodeTracerProvider({ }); ``` +### Disable Plugins with Environment Variables + +Plugins can be disabled without modifying and redeploying code. +`OTEL_NO_PATCH_MODULES` accepts a +comma separated list of module names to disabled specific plugins. +The names should match what you use to `require` the module into your application. +For example, `OTEL_NO_PATCH_MODULES=pg,https` will disable the postgres plugin and the https plugin. To disable **all** plugins, set the environment variable to `*`. + ## Examples See how to automatically instrument [http](/~https://github.com/open-telemetry/opentelemetry-js/tree/master/examples/http) and [gRPC](/~https://github.com/open-telemetry/opentelemetry-js/tree/master/examples/grpc) using node-sdk. diff --git a/packages/opentelemetry-node/src/instrumentation/PluginLoader.ts b/packages/opentelemetry-node/src/instrumentation/PluginLoader.ts index 8ed3ebf4ce9..1c3a2b3d118 100644 --- a/packages/opentelemetry-node/src/instrumentation/PluginLoader.ts +++ b/packages/opentelemetry-node/src/instrumentation/PluginLoader.ts @@ -30,6 +30,17 @@ export enum HookState { 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]: PluginConfig; } @@ -46,6 +57,18 @@ function filterPlugins(plugins: Plugins): Plugins { }, {}); } +/** + * 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. @@ -74,6 +97,7 @@ export class 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 @@ -119,6 +143,21 @@ export class PluginLoader { 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}` ); diff --git a/packages/opentelemetry-node/test/instrumentation/PluginLoader.test.ts b/packages/opentelemetry-node/test/instrumentation/PluginLoader.test.ts index 2214e7165d7..954cf7f8ed4 100644 --- a/packages/opentelemetry-node/test/instrumentation/PluginLoader.test.ts +++ b/packages/opentelemetry-node/test/instrumentation/PluginLoader.test.ts @@ -23,6 +23,7 @@ import { PluginLoader, Plugins, searchPathForTest, + ENV_PLUGIN_DISABLED_LIST, } from '../../src/instrumentation/PluginLoader'; const INSTALLED_PLUGINS_PATH = path.join(__dirname, 'node_modules'); @@ -135,6 +136,10 @@ describe('PluginLoader', () => { }); 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'); @@ -152,6 +157,86 @@ describe('PluginLoader', () => { 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);