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

Add Plugin loader #126

Merged
merged 15 commits into from
Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/opentelemetry-node-tracer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Dependency directories
!test/instrumentation/node_modules
7 changes: 6 additions & 1 deletion packages/opentelemetry-node-tracer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@
"devDependencies": {
"@types/mocha": "^5.2.5",
"@types/node": "^12.6.8",
"@types/shimmer": "^1.0.1",
"@types/semver": "^6.0.1",
"codecov": "^3.1.0",
"gts": "^1.0.0",
"mocha": "^6.1.0",
"nyc": "^14.1.1",
"shimmer": "^1.2.0",
"ts-mocha": "^6.0.0",
"ts-node": "^8.0.0",
"typescript": "^3.4.5"
Expand All @@ -53,6 +56,8 @@
"@opentelemetry/core": "^0.0.1",
"@opentelemetry/scope-async-hooks": "^0.0.1",
"@opentelemetry/scope-base": "^0.0.1",
"@opentelemetry/types": "^0.0.1"
"@opentelemetry/types": "^0.0.1",
"require-in-the-middle": "^4.0.0",
"semver": "^6.2.0"
}
}
144 changes: 144 additions & 0 deletions packages/opentelemetry-node-tracer/src/instrumentation/PluginLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Copyright 2019, 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, Plugin, Tracer } from '@opentelemetry/types';
import * as hook from 'require-in-the-middle';
import * as utils from './utils';

// States for the Plugin Loader
export enum HookState {
UNINITIALIZED,
ENABLED,
DISABLED,
}

interface PluginNames {
[pluginName: string]: string;
}

interface PluginConfig {
// TODO: Consider to add configuration options
[pluginName: string]: boolean;
}

/**
* 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. */
private _plugins: Plugin[] = [];
/**
* 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 tracer: Tracer, 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 pluginConfig an object whose keys are plugin names and whose
* boolean values indicate whether to enable the plugin.
*/
load(pluginConfig: PluginConfig): PluginLoader {
if (this._hookState === HookState.UNINITIALIZED) {
const plugins = Object.keys(pluginConfig).reduce(
(plugins: PluginNames, moduleName: string) => {
if (pluginConfig[moduleName]) {
plugins[moduleName] = utils.defaultPackageName(moduleName);
}
return plugins;
},
{} as PluginNames
);
const modulesToHook = Object.keys(plugins);
// 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) {
mayurkale22 marked this conversation as resolved.
Show resolved Hide resolved
this._hookState = HookState.DISABLED;
return this;
}

// Enable the require hook.
hook(modulesToHook, (exports, name, baseDir) => {
if (this._hookState !== HookState.ENABLED) return exports;

const moduleName = plugins[name];
// Get the module version.
const version = utils.getPackageVersion(this.logger, baseDir as string);
this.logger.info(
`PluginLoader#load: trying loading ${name}.${version}`
);

// @todo (issues/132): Check if version and supportedVersions are
// satisfied
if (!version) return exports;
mayurkale22 marked this conversation as resolved.
Show resolved Hide resolved

this.logger.debug(
`PluginLoader#load: applying patch to ${name}@${version} using ${moduleName} module`
);

// Expecting a plugin from module;
try {
const plugin: Plugin = require(moduleName).plugin;
this._plugins.push(plugin);
// Enable each supported plugin.
return plugin.enable(exports, this.tracer);
mayurkale22 marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
this.logger.error(
`PluginLoader#load: could not load plugin ${moduleName} 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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright 2019, 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.
*/

/** opentelemetry scope */
export const OPENTELEMETRY_SCOPE = '@opentelemetry';

/** Default prefix for instrumentation modules */
export const DEFAULT_PLUGIN_PACKAGE_NAME_PREFIX = 'plugin';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright 2019, 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.
*/

declare module 'require-in-the-middle' {
namespace hook {
type Options = {
internals?: boolean;
};
type OnRequireFn = <T>(exports: T, name: string, basedir?: string) => T;
}
function hook(modules: string[]|null, options: hook.Options|null, onRequire: hook.OnRequireFn): void;
function hook(modules: string[]|null, onRequire: hook.OnRequireFn): void;
function hook(onRequire: hook.OnRequireFn): void;
export = hook;
}
69 changes: 69 additions & 0 deletions packages/opentelemetry-node-tracer/src/instrumentation/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright 2019, 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 } from '@opentelemetry/types';
import * as path from 'path';
import * as semver from 'semver';
import * as constants from './constants';

/**
* Gets the default package name for a target module. The default package
* name uses the default scope and a default prefix.
* @param moduleName The module name.
* @returns The default name for the package.
*/
export function defaultPackageName(moduleName: string): string {
return `${constants.OPENTELEMETRY_SCOPE}/${constants.DEFAULT_PLUGIN_PACKAGE_NAME_PREFIX}-${moduleName}`;
}

/**
* Gets the package version.
* @param logger The logger to use.
* @param [basedir] The base directory.
*/
export function getPackageVersion(
logger: Logger,
basedir?: string
): string | null {
if (!basedir) return 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;
}
}

/**
* 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);
}
Loading