Skip to content

Commit b5e2330

Browse files
committed
chore: initial commit of hooks
0 parents  commit b5e2330

File tree

7 files changed

+252
-0
lines changed

7 files changed

+252
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

.npmrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
package-lock=false
2+

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Tracing Hooks
2+
This repository contains a ESM loader for injecting tracing channel hooks into Node.js modules. It also has a patch ofr Module to be used to patch CJS modules.
3+
4+
5+
## Usage
6+
7+
To load esm loader:
8+
9+
```js
10+
import { register } from 'node:module';
11+
const packages = new Set('pkg1', 'pkg2');
12+
const instrumentations = [
13+
{
14+
channelName: 'channel1',
15+
module: { name: 'pkg1', verisonRange: '>=1.0.0', filePath: 'index.js' },
16+
functionQuery: {
17+
className: 'Class1',
18+
methodName: 'method1',
19+
kind: 'Async'
20+
}
21+
},
22+
{
23+
channelName: 'channel2',
24+
module: { name: 'pkg2', verisonRange: '>=1.0.0', filePath: 'index.js' },
25+
functionQuery: {
26+
className: 'Class2,
27+
methodName: 'method2',
28+
kind: 'Sync'
29+
}
30+
}
31+
]
32+
33+
register('@apm-js-collab/tracing-hooks/hook.mjs', import.meta.url, {
34+
data: { instrumentations, packages }
35+
});
36+
```
37+
38+
To load CJS patch:
39+
40+
```js
41+
const ModulePatch = require('@apm-js-collab/tracing-hooks')
42+
const packages = new Set('pkg1', 'pkg2');
43+
const instrumentations = [
44+
{
45+
channelName: 'channel1',
46+
module: { name: 'pkg1', verisonRange: '>=1.0.0', filePath: 'index.js' },
47+
functionQuery: {
48+
className: 'Class1',
49+
methodName: 'method1',
50+
kind: 'Async'
51+
}
52+
},
53+
{
54+
channelName: 'channel2',
55+
module: { name: 'pkg2', verisonRange: '>=1.0.0', filePath: 'index.js' },
56+
functionQuery: {
57+
className: 'Class2,
58+
methodName: 'method2',
59+
kind: 'Sync'
60+
}
61+
}
62+
]
63+
64+
65+
const modulePatch = new ModulePatch({ instrumentations, packages });
66+
modulePatch.patch()
67+
```
68+

hook.mjs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict'
2+
import createDebug from 'debug';
3+
import { readFile } from 'node:fs/promises'
4+
import { create } from '@apm-js-collab/code-transformer'
5+
import parse from 'module-details-from-path'
6+
import { fileURLToPath } from 'node:url'
7+
import getPackageVersion from './lib/get-package-version.js'
8+
const debug = createDebug('@apm-js-collab/tracing-hooks:esm-hook')
9+
const transformers = new Map()
10+
let instrumentator = null
11+
let packages
12+
13+
export async function initialize(data) {
14+
instrumentator = create(data?.instrumentations || [])
15+
packages = data?.packages || new Set()
16+
}
17+
18+
export async function resolve(specifier, context, nextResolve) {
19+
const url = await nextResolve(specifier, context)
20+
const resolvedModule = parse(url.url)
21+
if (resolvedModule && packages.has(resolvedModule.name)) {
22+
const path = fileURLToPath(resolvedModule.basedir)
23+
const version = getPackageVersion(path)
24+
const transformer = instrumentator.getTransformer(resolvedModule.name, version, resolvedModule.path)
25+
if (transformer) {
26+
transformers.set(url.url, transformer)
27+
}
28+
}
29+
return url
30+
}
31+
32+
export async function load(url, context, nextLoad) {
33+
const result = await nextLoad(url, context)
34+
if (transformers.has(url) === false) {
35+
return result
36+
}
37+
38+
if (result.format === 'commonjs') {
39+
const parsedUrl = new URL(result.responseURL ?? url)
40+
result.source ??= await readFile(parsedUrl)
41+
}
42+
43+
const code = result.source
44+
if (code) {
45+
const transformer = transformers.get(url)
46+
try {
47+
const transformedCode = transformer.transform(code.toString('utf8'), 'unknown')
48+
result.source = transformedCode
49+
result.shortCircuit = true
50+
} catch(err) {
51+
debug('Error transforming module %s: %o', filename, error)
52+
} finally {
53+
transformer.free()
54+
}
55+
}
56+
57+
return result
58+
}
59+

index.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict'
2+
const { create } = require('@apm-js-collab/code-transformer')
3+
const Module = require('node:module')
4+
const parse = require('module-details-from-path')
5+
const getPackageVersion = require('./lib/get-package-version')
6+
const debug = require('debug')('@apm-js-collab/tracing-hooks:module-patch')
7+
8+
class ModulePatch {
9+
constructor({ packages = new Set(), instrumentations = [] } = {}) {
10+
this.packages = packages
11+
this.instrumentator = create(instrumentations)
12+
this.transformers = new Map()
13+
this.resolve = Module._resolveFilename
14+
this.compile = Module.prototype._compile
15+
}
16+
17+
/**
18+
* Patches the Node.js module class methods that are responsible for resolving filePaths and compiling code.
19+
* If a module is found that has an instrumentator, it will transform the code before compiling it
20+
* with tracing channel methods.
21+
*/
22+
patch() {
23+
const self = this
24+
Module._resolveFilename = function wrappedResolveFileName() {
25+
const resolvedName = self.resolve.apply(this, arguments)
26+
const resolvedModule = parse(resolvedName)
27+
if (resolvedModule && self.packages.has(resolvedModule.name)) {
28+
const version = getPackageVersion(resolvedModule.basedir, resolvedModule.name)
29+
const transformer = self.instrumentator.getTransformer(resolvedModule.name, version, resolvedModule.path)
30+
if (transformer) {
31+
self.transformers.set(resolvedName, transformer)
32+
}
33+
}
34+
return resolvedName
35+
}
36+
37+
Module.prototype._compile = function wrappedCompile(...args) {
38+
const [content, filename] = args
39+
if (self.transformers.has(filename)) {
40+
const transformer = self.transformers.get(filename)
41+
try {
42+
const transformedCode = transformer.transform(content, 'unknown')
43+
args[0] = transformedCode
44+
} catch (error) {
45+
debug('Error transforming module %s: %o', filename, error)
46+
} finally {
47+
transformer.free()
48+
}
49+
}
50+
51+
return self.compile.apply(this, args)
52+
}
53+
}
54+
55+
/**
56+
* Clears all the transformers and restores the original Module methods that were wrapped.
57+
* **Note**: This is intended to be used in testing only.
58+
*/
59+
unpatch() {
60+
this.transformers.clear()
61+
Module._resolveFilename = this.resolve
62+
Module.prototype._compile = this.compile
63+
}
64+
}
65+
66+
module.exports = ModulePatch
67+

lib/get-package-version.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict'
2+
const { readFileSync } = require('node:fs')
3+
const { join } = require('node:path')
4+
5+
const packageVersions = new Map()
6+
7+
/**
8+
* Retrieves the version of a package from its package.json file.
9+
* If the package.json file cannot be read, it defaults to the Node.js version.
10+
* @param {string} baseDir - The base directory where the package.json file is located.
11+
* @returns {string} The version of the package or the Node.js version if the package.json cannot be read.
12+
*/
13+
function getPackageVersion(baseDir) {
14+
if (packageVersions.has(baseDir)) {
15+
return packageVersions.get(baseDir)
16+
}
17+
18+
try {
19+
const packageJsonPath = join(baseDir, 'package.json')
20+
const jsonFile = readFileSync(packageJsonPath)
21+
const { version } = JSON.parse(jsonFile)
22+
packageVersions.set(baseDir, version)
23+
return version
24+
} catch {
25+
return process.version.slice(1)
26+
}
27+
}
28+
29+
module.exports = getPackageVersion
30+

package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@apm-js-collab/tracing-hooks",
3+
"version": "0.0.1",
4+
"description": "CJS and ESM hooks for orchestrion",
5+
"license": "Apache-2.0",
6+
"type": "commonjs",
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/apm-js-collab/tracing-hooks.git"
10+
},
11+
"main": "index.js",
12+
"scripts": {
13+
"test": "echo \"Error: no test specified\" && exit 1"
14+
},
15+
"files": [
16+
"index.js",
17+
"hook.mjs",
18+
"lib"
19+
],
20+
"dependencies": {
21+
"@apm-js-collab/code-transformer": "^0.6.0",
22+
"debug": "^4.4.1",
23+
"module-details-from-path": "^1.0.4"
24+
}
25+
}

0 commit comments

Comments
 (0)