-
Notifications
You must be signed in to change notification settings - Fork 950
feat(instrumentation): implement require-in-the-middle singleton
#3161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
ecd1b30
feat(instrumentation): implement `require-in-the-middle` singleton
mhassan1 98412c5
Merge branch 'main' into ritm-singleton
dyladan 9487923
Merge branch 'main' into ritm-singleton
rauno56 7bb8c5d
feat(instrumentation): create ritm singleton on first instrumentation
mhassan1 8ed3f9a
feat(instrumentation): remove process-global ritm singleton
mhassan1 6280629
feat(instrumentation): do not re-use ritm singleton in mocha
mhassan1 5f96daa
Merge remote-tracking branch 'upstream/main' into ritm-singleton
mhassan1 3c593a8
fix(instrumentation): make ritm singleton constructor private
mhassan1 8d275a5
fix(instrumentation): use module name trie for ritm singleton perform…
mhassan1 cfb2f01
fix(instrumentation): fix filename casing
mhassan1 f497108
fix(instrumentation): increase timeout for non-core module
mhassan1 302d4f4
Merge branch 'main' into ritm-singleton
dyladan 78d4a99
Merge remote-tracking branch 'upstream/main' into ritm-singleton
mhassan1 840e80b
chore(instrumentation): move ritm singleton changelog entry
mhassan1 4f9df38
Merge branch 'main' into ritm-singleton
dyladan 701f624
Merge branch 'main' into ritm-singleton
dyladan ed6e3ef
Merge branch 'main' into ritm-singleton
dyladan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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[] { | ||
mhassan1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 hidden or 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 hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.