Skip to content

Commit

Permalink
feat(instrumentation): implement require-in-the-middle singleton
Browse files Browse the repository at this point in the history
  • Loading branch information
mhassan1 committed Sep 6, 2022
1 parent 597ea98 commit 8f261d8
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export abstract class InstrumentationBase<T = any>
extends InstrumentationAbstract
implements types.Instrumentation {
private _modules: InstrumentationModuleDefinition<T>[];
private _hooks: RequireInTheMiddle.Hooked[] = [];
private _hooked = false;
private _enabled = false;

constructor(
Expand Down Expand Up @@ -142,7 +142,7 @@ export abstract class InstrumentationBase<T = any>
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);
Expand All @@ -158,22 +158,20 @@ export abstract class InstrumentationBase<T = any>

this._warnOnPreloadedModules();
for (const module of this._modules) {
this._hooks.push(
RequireInTheMiddle(
[module.name],
{ internals: true },
(exports, name, baseDir) => {
return this._onRequire<typeof exports>(
(module as unknown) as InstrumentationModuleDefinition<
typeof exports
requireInTheMiddleSingleton.register(
module.name,
(exports, name, baseDir) => {
return this._onRequire<typeof exports>(
(module as unknown) as InstrumentationModuleDefinition<
typeof exports
>,
exports,
name,
baseDir
);
}
)
exports,
name,
baseDir
);
}
);
this._hooked = true;
}
}

Expand Down Expand Up @@ -210,3 +208,67 @@ 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
*/
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 _shouldHook(hookedModuleName, name, baseDir);
});

for (const { onRequire } of matches) {
exports = onRequire(exports, name, baseDir);
}

return exports;
}
);
}

register(moduleName: string, onRequire: RequireInTheMiddle.OnRequireFn) {
this._modulesToHook.push({ moduleName, onRequire });
}

static getGlobalInstance(): RequireInTheMiddleSingleton {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (global as any)[RITM_SINGLETON_SYM] = (global as any)[RITM_SINGLETON_SYM] ?? new RequireInTheMiddleSingleton();
}
}

/**
* Determine whether a `require`d module should be hooked
*
* @param {string} hookedModuleName Hooked module name
* @param {string} requiredModuleName Required module name
* @param {string|undefined} requiredModuleBaseDir Required module base directory
* @returns {boolean} Whether to hook the required module
* @private
*/
export function _shouldHook(hookedModuleName: string, requiredModuleName: string, requiredModuleBaseDir: string | undefined): boolean {
if (path.isAbsolute(hookedModuleName)) {
if (requiredModuleBaseDir === undefined) {
return false;
}
requiredModuleName = path.resolve(requiredModuleBaseDir, requiredModuleName);
}

return requiredModuleName === hookedModuleName || requiredModuleName.startsWith(hookedModuleName + path.sep);
}

const RITM_SINGLETON_SYM = Symbol.for('OpenTelemetry.js.sdk.require-in-the-middle');

const requireInTheMiddleSingleton = RequireInTheMiddleSingleton.getGlobalInstance();
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
*/

import * as assert from 'assert';
import * as path from 'path';
import * as sinon from 'sinon';
import { InstrumentationBase, InstrumentationModuleDefinition } from '../../src';
import { InstrumentationBase, InstrumentationModuleDefinition, _shouldHook } from '../../src';

const MODULE_NAME = 'test-module';
const MODULE_FILE_NAME = 'test-module-file';
Expand Down Expand Up @@ -254,4 +255,21 @@ describe('InstrumentationBase', () => {
});
});
});

describe('_shouldHook', () => {
it('should determine whether to hook a module that has an absolute path', () => {
assert.equal(_shouldHook(path.join(path.sep, 'a', 'b', 'c'), 'c', path.join(path.sep, 'a', 'b')), true);
assert.equal(_shouldHook(path.join(path.sep, 'a', 'b', 'c'), path.join('c', 'd'), path.join(path.sep, 'a', 'b')), true);

assert.equal(_shouldHook(path.join(path.sep, 'a', 'b', 'c'), 'c', undefined), false);
assert.equal(_shouldHook(path.join(path.sep, 'a', 'b', 'c'), 'e', path.join(path.sep, 'a', 'b')), false);
});

it('should determine whether to hook a module that does not have an absolute path', () => {
assert.equal(_shouldHook('c', 'c', undefined), true);
assert.equal(_shouldHook('c', path.join('c', 'd'), undefined), true);

assert.equal(_shouldHook('c', 'e', undefined), false);
});
});
});

0 comments on commit 8f261d8

Please sign in to comment.