forked from open-telemetry/opentelemetry-js
-
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.
feat(instrumentation): implement
require-in-the-middle
singleton (o…
…pen-telemetry#3161) Co-authored-by: Daniel Dyla <[email protected]> Co-authored-by: Rauno Viskus <[email protected]>
- Loading branch information
1 parent
0d4c71f
commit bbc1811
Showing
6 changed files
with
397 additions
and
5 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
86 changes: 86 additions & 0 deletions
86
experimental/packages/opentelemetry-instrumentation/src/platform/node/ModuleNameTrie.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,86 @@ | ||
/* | ||
* Copyright The 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 type { Hooked } from './RequireInTheMiddleSingleton'; | ||
|
||
export const ModuleNameSeparator = '/'; | ||
|
||
/** | ||
* Node in a `ModuleNameTrie` | ||
*/ | ||
class ModuleNameTrieNode { | ||
hooks: Array<{ hook: Hooked, insertedId: number }> = []; | ||
children: Map<string, ModuleNameTrieNode> = new Map(); | ||
} | ||
|
||
/** | ||
* Trie containing nodes that represent a part of a module name (i.e. the parts separated by forward slash) | ||
*/ | ||
export class ModuleNameTrie { | ||
private _trie: ModuleNameTrieNode = new ModuleNameTrieNode(); | ||
private _counter: number = 0; | ||
|
||
/** | ||
* Insert a module hook into the trie | ||
* | ||
* @param {Hooked} hook Hook | ||
*/ | ||
insert(hook: Hooked) { | ||
let trieNode = this._trie; | ||
|
||
for (const moduleNamePart of hook.moduleName.split(ModuleNameSeparator)) { | ||
let nextNode = trieNode.children.get(moduleNamePart); | ||
if (!nextNode) { | ||
nextNode = new ModuleNameTrieNode(); | ||
trieNode.children.set(moduleNamePart, nextNode); | ||
} | ||
trieNode = nextNode; | ||
} | ||
trieNode.hooks.push({ hook, insertedId: this._counter++ }); | ||
} | ||
|
||
/** | ||
* Search for matching hooks in the trie | ||
* | ||
* @param {string} moduleName Module name | ||
* @param {boolean} maintainInsertionOrder Whether to return the results in insertion order | ||
* @returns {Hooked[]} Matching hooks | ||
*/ | ||
search(moduleName: string, { maintainInsertionOrder }: { maintainInsertionOrder?: boolean } = {}): Hooked[] { | ||
let trieNode = this._trie; | ||
const results: ModuleNameTrieNode['hooks'] = []; | ||
|
||
for (const moduleNamePart of moduleName.split(ModuleNameSeparator)) { | ||
const nextNode = trieNode.children.get(moduleNamePart); | ||
if (!nextNode) { | ||
break; | ||
} | ||
results.push(...nextNode.hooks); | ||
trieNode = nextNode; | ||
} | ||
|
||
if (results.length === 0) { | ||
return []; | ||
} | ||
if (results.length === 1) { | ||
return [results[0].hook]; | ||
} | ||
if (maintainInsertionOrder) { | ||
results.sort((a, b) => a.insertedId - b.insertedId); | ||
} | ||
return results.map(({ hook }) => hook); | ||
} | ||
} |
111 changes: 111 additions & 0 deletions
111
...l/packages/opentelemetry-instrumentation/src/platform/node/RequireInTheMiddleSingleton.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,111 @@ | ||
/* | ||
* Copyright The 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 * as RequireInTheMiddle from 'require-in-the-middle'; | ||
import * as path from 'path'; | ||
import { ModuleNameTrie, ModuleNameSeparator } from './ModuleNameTrie'; | ||
|
||
export type Hooked = { | ||
moduleName: string | ||
onRequire: RequireInTheMiddle.OnRequireFn | ||
}; | ||
|
||
/** | ||
* Whether Mocha is running in this process | ||
* Inspired by https://github.com/AndreasPizsa/detect-mocha | ||
* | ||
* @type {boolean} | ||
*/ | ||
const isMocha = ['afterEach','after','beforeEach','before','describe','it'].every(fn => { | ||
// @ts-expect-error TS7053: Element implicitly has an 'any' type | ||
return typeof global[fn] === 'function'; | ||
}); | ||
|
||
/** | ||
* Singleton class for `require-in-the-middle` | ||
* Allows instrumentation plugins to patch modules with only a single `require` patch | ||
* WARNING: Because this class will create its own `require-in-the-middle` (RITM) instance, | ||
* we should minimize the number of new instances of this class. | ||
* Multiple instances of `@opentelemetry/instrumentation` (e.g. multiple versions) in a single process | ||
* will result in multiple instances of RITM, which will have an impact | ||
* on the performance of instrumentation hooks being applied. | ||
*/ | ||
export class RequireInTheMiddleSingleton { | ||
private _moduleNameTrie: ModuleNameTrie = new ModuleNameTrie(); | ||
private static _instance?: RequireInTheMiddleSingleton; | ||
|
||
private constructor() { | ||
this._initialize(); | ||
} | ||
|
||
private _initialize() { | ||
RequireInTheMiddle( | ||
// Intercept all `require` calls; we will filter the matching ones below | ||
null, | ||
{ internals: true }, | ||
(exports, name, basedir) => { | ||
// For internal files on Windows, `name` will use backslash as the path separator | ||
const normalizedModuleName = normalizePathSeparators(name); | ||
|
||
const matches = this._moduleNameTrie.search(normalizedModuleName, { maintainInsertionOrder: true }); | ||
|
||
for (const { onRequire } of matches) { | ||
exports = onRequire(exports, name, basedir); | ||
} | ||
|
||
return exports; | ||
} | ||
); | ||
} | ||
|
||
/** | ||
* Register a hook with `require-in-the-middle` | ||
* | ||
* @param {string} moduleName Module name | ||
* @param {RequireInTheMiddle.OnRequireFn} onRequire Hook function | ||
* @returns {Hooked} Registered hook | ||
*/ | ||
register(moduleName: string, onRequire: RequireInTheMiddle.OnRequireFn): Hooked { | ||
const hooked = { moduleName, onRequire }; | ||
this._moduleNameTrie.insert(hooked); | ||
return hooked; | ||
} | ||
|
||
/** | ||
* Get the `RequireInTheMiddleSingleton` singleton | ||
* | ||
* @returns {RequireInTheMiddleSingleton} Singleton of `RequireInTheMiddleSingleton` | ||
*/ | ||
static getInstance(): RequireInTheMiddleSingleton { | ||
// Mocha runs all test suites in the same process | ||
// This prevents test suites from sharing a singleton | ||
if (isMocha) return new RequireInTheMiddleSingleton(); | ||
|
||
return this._instance = this._instance ?? new RequireInTheMiddleSingleton(); | ||
} | ||
} | ||
|
||
/** | ||
* Normalize the path separators to forward slash in a module name or path | ||
* | ||
* @param {string} moduleNameOrPath Module name or path | ||
* @returns {string} Normalized module name or path | ||
*/ | ||
function normalizePathSeparators(moduleNameOrPath: string): string { | ||
return path.sep !== ModuleNameSeparator | ||
? moduleNameOrPath.split(path.sep).join(ModuleNameSeparator) | ||
: moduleNameOrPath; | ||
} |
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
68 changes: 68 additions & 0 deletions
68
experimental/packages/opentelemetry-instrumentation/test/node/ModuleNameTrie.test.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,68 @@ | ||
/* | ||
* Copyright The 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 * as assert from 'assert'; | ||
import { Hooked } from '../../src/platform/node/RequireInTheMiddleSingleton'; | ||
import { ModuleNameTrie } from '../../src/platform/node/ModuleNameTrie'; | ||
|
||
describe('ModuleNameTrie', () => { | ||
describe('search', () => { | ||
const trie = new ModuleNameTrie(); | ||
const inserts = [ | ||
{ moduleName: 'a', onRequire: () => {} }, | ||
{ moduleName: 'a/b', onRequire: () => {} }, | ||
{ moduleName: 'a', onRequire: () => {} }, | ||
{ moduleName: 'a/c', onRequire: () => {} }, | ||
{ moduleName: 'd', onRequire: () => {} } | ||
] as Hooked[]; | ||
inserts.forEach(trie.insert.bind(trie)); | ||
|
||
it('should return a list of exact matches (no results)', () => { | ||
assert.deepEqual(trie.search('e'), []); | ||
}); | ||
|
||
it('should return a list of exact matches (one result)', () => { | ||
assert.deepEqual(trie.search('d'), [inserts[4]]); | ||
}); | ||
|
||
it('should return a list of exact matches (more than one result)', () => { | ||
assert.deepEqual(trie.search('a'), [ | ||
inserts[0], | ||
inserts[2] | ||
]); | ||
}); | ||
|
||
describe('maintainInsertionOrder = false', () => { | ||
it('should return a list of matches in prefix order', () => { | ||
assert.deepEqual(trie.search('a/b'), [ | ||
inserts[0], | ||
inserts[2], | ||
inserts[1] | ||
]); | ||
}); | ||
}); | ||
|
||
describe('maintainInsertionOrder = true', () => { | ||
it('should return a list of matches in insertion order', () => { | ||
assert.deepEqual(trie.search('a/b', { maintainInsertionOrder: true }), [ | ||
inserts[0], | ||
inserts[1], | ||
inserts[2] | ||
]); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.