@opencensus/nodejs-base
Version:
OpenCensus is a toolkit for collecting application performance and behavior data.
165 lines (164 loc) • 6.33 kB
JavaScript
;
/**
* Copyright 2018, OpenCensus 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
*
* http://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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginLoader = void 0;
const fs = require("fs");
const path = require("path");
const hook = require("require-in-the-middle");
const constants_1 = require("../constants");
var HookState;
(function (HookState) {
HookState[HookState["UNINITIALIZED"] = 0] = "UNINITIALIZED";
HookState[HookState["ENABLED"] = 1] = "ENABLED";
HookState[HookState["DISABLED"] = 2] = "DISABLED";
})(HookState || (HookState = {}));
/**
* The PluginLoader class can load instrumentation plugins that
* use a patch mechanism to enable automatic tracing for
* specific target modules.
*/
class PluginLoader {
/**
* Constructs a new PluginLoader instance.
* @param tracer The tracer.
*/
constructor(logger, tracer, stats) {
/** A list of loaded plugins. */
this.plugins = [];
/**
* A field that tracks whether the r-i-t-m hook has been loaded for the
* first time, as well as whether the hook body is enabled or not.
*/
this.hookState = HookState.UNINITIALIZED;
this.tracer = tracer;
this.logger = logger;
this.stats = stats;
}
/**
* 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 that package.
*/
static defaultPackageName(moduleName) {
return `${constants_1.Constants.OPENCENSUS_SCOPE}/${constants_1.Constants.DEFAULT_PLUGIN_PACKAGE_NAME_PREFIX}-${moduleName}`;
}
/**
* Returns a PluginNames object, build from a string array of target modules
* names, using the defaultPackageName.
* @param modulesToPatch A list of modules to patch.
* @returns Plugin names.
*/
static defaultPluginsFromArray(modulesToPatch) {
const plugins = modulesToPatch.reduce((plugins, moduleName) => {
plugins[moduleName] = PluginLoader.defaultPackageName(moduleName);
return plugins;
}, {});
return plugins;
}
/**
* Gets the package version.
* @param name Name.
* @param basedir The base directory.
*/
getPackageVersion(name, basedir) {
let version = null;
if (basedir) {
const pkgJson = path.join(basedir, 'package.json');
try {
version = JSON.parse(fs.readFileSync(pkgJson).toString()).version;
}
catch (e) {
this.logger.error('could not get version of %s module: %s', name, e.message);
}
}
else {
version = process.versions.node;
}
return version;
}
/**
* Loads a list of plugins (using a map of the target module name
* and its instrumentation plugin package name). Each plugin module
* should implement the core 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 pluginList A list of plugins.
*/
loadPlugins(pluginList) {
if (this.hookState === HookState.UNINITIALIZED) {
const modulesToHook = Object.keys(pluginList);
// 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;
}
hook(modulesToHook, (exports, name, basedir) => {
if (this.hookState !== HookState.ENABLED) {
return exports;
}
const plugin = pluginList[name];
const version = this.getPackageVersion(name, basedir);
this.logger.info('trying loading %s.%s', name, version);
if (!version) {
return exports;
}
this.logger.debug('applying patch to %s@%s module', name, version);
let moduleName;
let moduleConfig = {};
if (typeof plugin === 'string') {
moduleName = plugin;
}
else {
moduleConfig = plugin.config;
moduleName = plugin.module;
}
this.logger.debug('using package %s to patch %s', moduleName, name);
// Expecting a plugin from module;
try {
const plugin = require(moduleName).plugin;
this.plugins.push(plugin);
return plugin.enable(exports, this.tracer, version, moduleConfig, basedir, this.stats);
}
catch (e) {
this.logger.error('could not load plugin %s of module %s. Error: %s', moduleName, name, e.message);
return exports;
}
});
}
this.hookState = HookState.ENABLED;
}
/** Unloads plugins. */
unloadPlugins() {
for (const plugin of this.plugins) {
plugin.disable();
}
this.plugins = [];
this.hookState = HookState.DISABLED;
}
/**
* Adds a search path for plugin modules. Intended for testing purposes only.
* @param searchPath The path to add.
*/
static set searchPathForTest(searchPath) {
module.paths.push(searchPath);
}
}
exports.PluginLoader = PluginLoader;