feat: creating one auto loader for instrumentation and old plugins (#1731)

* feat: creating one auto loader for instrumentation and old plugins

* feat: adding temporary tests for autoloader

* chore: adding auto loader for instrumentation and plugins

* chore: removing temporary test

* chore: reverting changes done temporary

* chore: linting

* chore: updating submodule for opentelemetry-proto

* chore: updating submodule for exporter-collector-grpc
This commit is contained in:
Bartlomiej Obecny 2021-01-07 17:03:39 +01:00 committed by GitHub
parent b4c9e40fed
commit 89664a7c4e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1940 additions and 2 deletions

View File

@ -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"

View File

@ -2,6 +2,8 @@
"lerna": "3.13.4",
"npmClient": "npm",
"packages": [
"examples/test",
"examples/tracer-web",
"benchmark/*",
"backwards-compatability/*",
"metapackages/*",

View File

@ -0,0 +1,2 @@
# Dependency directories
!test/node/node_modules

View File

@ -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

View File

@ -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);
};
}

View File

@ -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());
}

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
export * from './autoLoader';
export * from './platform/index';
export * from './types';
export * from './utils';

View File

@ -15,3 +15,4 @@
*/
export * from './instrumentation';
export * from './old/autoLoader';

View File

@ -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();
});
};
}

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
export * from './old/autoLoader';
export * from './instrumentation';
export * from './instrumentationNodeModuleDefinition';
export * from './instrumentationNodeModuleFile';

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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<T = any> {
/**
* 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<string | RegExp>;
/**
* 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;
}

View File

@ -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);
});
});
});
});

View File

@ -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);
});
});
});
});

View File

@ -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);
});
});
});

View File

@ -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<T> implements OldClassPlugin<T> {
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;
}

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});
});

View File

@ -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;

View File

@ -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"));

View File

@ -0,0 +1,4 @@
{
"name": "@opentelemetry/plugin-http-module",
"version": "0.0.1"
}

View File

@ -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"));

View File

@ -0,0 +1,4 @@
{
"name": "@opentelemetry/plugin-notsupported-module",
"version": "1.0.0"
}

View File

@ -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;

View File

@ -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"));

View File

@ -0,0 +1,4 @@
{
"name": "@opentelemetry/plugin-simple-module",
"version": "0.0.1"
}

View File

@ -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;

View File

@ -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"));

View File

@ -0,0 +1,4 @@
{
"name": "@opentelemetry/plugin-supported-module",
"version": "0.0.1"
}

View File

@ -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;

View File

@ -0,0 +1,4 @@
module.exports = {
name: () => 'already-module',
value: () => 0,
};

View File

@ -0,0 +1,4 @@
{
"name": "already-module",
"version": "0.1.0"
}

View File

@ -0,0 +1,4 @@
module.exports = {
name: () => 'notsupported-module',
value: () => 0,
};

View File

@ -0,0 +1,4 @@
{
"name": "notsupported-module",
"version": "0.0.1"
}

View File

@ -0,0 +1,4 @@
module.exports = {
name: () => 'random-module',
value: () => 0,
};

View File

@ -0,0 +1,4 @@
{
"name": "random-module",
"version": "0.1.0"
}

View File

@ -0,0 +1,4 @@
module.exports = {
name: () => 'simple-module',
value: () => 0,
};

View File

@ -0,0 +1,4 @@
{
"name": "simple-module",
"version": "0.1.0"
}

View File

@ -0,0 +1,4 @@
module.exports = {
name: () => 'supported-module',
value: () => 0,
};

View File

@ -0,0 +1,4 @@
{
"name": "supported-module",
"version": "0.0.1"
}

View File

@ -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);
});
});
});