forked from open-telemetry/opentelemetry-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Plugin loader (open-telemetry#126)
* Add Plugin Loader * Load simple-module * gts fix * Use type variable T instead of any * Log instead of throwing and remove redundant Plugins suffix * Add package specific .gitignore * Add @todo * Change DEFAULT_PLUGIN_PACKAGE_NAME_PREFIX to plugin * fix: build pipeline * fix: remove test functions out of the class * fix: use private members in test without exposing it * feat: add PluginConfig during load * fix: make comment more descriptive * fix: move the any to the Plugin type * fix: remove constants container
- Loading branch information
1 parent
e32796a
commit 20c9a49
Showing
14 changed files
with
510 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Dependency directories | ||
!test/instrumentation/node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
packages/opentelemetry-node-tracer/src/instrumentation/PluginLoader.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
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; | ||
|
||
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); | ||
} 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); | ||
} |
21 changes: 21 additions & 0 deletions
21
packages/opentelemetry-node-tracer/src/instrumentation/constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
28 changes: 28 additions & 0 deletions
28
packages/opentelemetry-node-tracer/src/instrumentation/ext-types.d.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
69
packages/opentelemetry-node-tracer/src/instrumentation/utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.