diff --git a/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts b/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts index 70dac85332f..81455fde66d 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts @@ -29,7 +29,7 @@ export abstract class InstrumentationBase extends InstrumentationAbstract implements types.Instrumentation { private _modules: InstrumentationModuleDefinition[]; - private _hooks: RequireInTheMiddle.Hooked[] = []; + private _hooked = false; private _enabled = false; constructor( @@ -142,7 +142,7 @@ export abstract class InstrumentationBase this._enabled = true; // already hooked, just call patch again - if (this._hooks.length > 0) { + if (this._hooked) { for (const module of this._modules) { if (typeof module.patch === 'function' && module.moduleExports) { module.patch(module.moduleExports, module.moduleVersion); @@ -158,22 +158,20 @@ export abstract class InstrumentationBase this._warnOnPreloadedModules(); for (const module of this._modules) { - this._hooks.push( - RequireInTheMiddle( - [module.name], - { internals: true }, - (exports, name, baseDir) => { - return this._onRequire( - (module as unknown) as InstrumentationModuleDefinition< - typeof exports + requireInTheMiddleSingleton.register( + module.name, + (exports, name, baseDir) => { + return this._onRequire( + (module as unknown) as InstrumentationModuleDefinition< + typeof exports >, - exports, - name, - baseDir - ); - } - ) + exports, + name, + baseDir + ); + } ); + this._hooked = true; } } @@ -210,3 +208,52 @@ function isSupported(supportedVersions: string[], version?: string, includePrere return satisfies(version, supportedVersion, { includePrerelease }); }); } + +/** + * Singleton class for `require-in-the-middle` + * Allows instrumentation plugins to patch modules with only a single `require` patch + * TODO: Move this to a higher level to ensure it is a singleton across the Node.js process + */ +class RequireInTheMiddleSingleton { + private _modulesToHook: Array<{ moduleName: string, onRequire: RequireInTheMiddle.OnRequireFn }> = []; + + constructor() { + this._initialize(); + } + + private _initialize() { + RequireInTheMiddle( + // Intercept all `require` calls; we will filter the matching ones below + null, + { internals: true }, + (exports, name, baseDir) => { + const matches = this._modulesToHook.filter(({ moduleName: hookedModuleName }) => { + return RequireInTheMiddleSingleton._shouldHook(hookedModuleName, name, baseDir); + }); + + for (const { onRequire } of matches) { + exports = onRequire(exports, name, baseDir); + } + + return exports; + } + ); + } + + private static _shouldHook(hookedModuleName: string, requiredModuleName: string, requiredModuleBaseDir: string | undefined) { + if (path.isAbsolute(hookedModuleName)) { + if (requiredModuleBaseDir === undefined) { + return false; + } + requiredModuleName = path.resolve(requiredModuleBaseDir, requiredModuleName); + } + + return requiredModuleName === hookedModuleName || requiredModuleName.startsWith(hookedModuleName + path.sep); + } + + register(moduleName: string, onRequire: RequireInTheMiddle.OnRequireFn) { + this._modulesToHook.push({ moduleName, onRequire }); + } +} + +const requireInTheMiddleSingleton = new RequireInTheMiddleSingleton();