Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add OTEL_NO_PATCH_MODULES #1153

Merged
merged 13 commits into from
Jun 12, 2020
8 changes: 8 additions & 0 deletions packages/opentelemetry-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ const provider = new NodeTracerProvider({
});
```

### Disable Plugins with Environment Variables

Plugins can be disabled without modifying and redeploying code.
`OPENTELEMETRY_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, `OPENTELEMETRY_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.
Expand Down
39 changes: 39 additions & 0 deletions packages/opentelemetry-node/src/instrumentation/PluginLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ export enum HookState {
DISABLED,
}

/**
* Environment variable which will contain list of modules to not load corresponding plugins for
* e.g.OPENTELEMETRY_NO_PATCH_MODULES=pg,https,mongodb
*/
export const ENV_PLUGIN_DISABLED_LIST = 'OPENTELEMETRY_NO_PATCH_MODULES';

/**
* Wildcard symbol. If ignore list is set to this, disable all plugins
*/
const DISABLE_ALL_PLUGINS = '*';
mayurkale22 marked this conversation as resolved.
Show resolved Hide resolved

export interface Plugins {
[pluginName: string]: PluginConfig;
}
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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);
Expand Down