-
Notifications
You must be signed in to change notification settings - Fork 294
/
instrument.js
130 lines (109 loc) · 3.76 KB
/
instrument.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
'use strict'
const dc = require('diagnostics_channel')
const path = require('path')
const semver = require('semver')
const iitm = require('../../../dd-trace/src/iitm')
const ritm = require('../../../dd-trace/src/ritm')
const parse = require('module-details-from-path')
const requirePackageJson = require('../../../dd-trace/src/require-package-json')
const { AsyncResource } = require('async_hooks')
const pathSepExpr = new RegExp(`\\${path.sep}`, 'g')
const channelMap = {}
exports.channel = function channel (name) {
const maybe = channelMap[name]
if (maybe) return maybe
const ch = dc.channel(name)
channelMap[name] = ch
return ch
}
exports.addHook = function addHook ({ name, versions, file }, hook) {
const fullFilename = filename(name, file)
const loaderHook = (moduleExports, moduleName, moduleBaseDir) => {
moduleName = moduleName.replace(pathSepExpr, '/')
if (moduleName !== fullFilename || !matchVersion(getVersion(moduleBaseDir), versions)) {
return moduleExports
}
return hook(moduleExports)
}
ritm([name], loaderHook)
cjsPostLoad({ name, versions, file }, hook)
iitm([name], loaderHook)
}
function matchVersion (version, ranges) {
return !version || (ranges && ranges.some(range => semver.satisfies(semver.coerce(version), range)))
}
function getVersion (moduleBaseDir) {
if (moduleBaseDir) {
return requirePackageJson(moduleBaseDir, module).version
}
}
function filename (name, file) {
return [name, file].filter(val => val).join('/')
}
// TODO this is basically Loader#_getModules + running the hook. DRY up.
function cjsPostLoad (instrumentation, hook) {
const ids = Object.keys(require.cache)
let pkg
for (let i = 0, l = ids.length; i < l; i++) {
if (ids[i] === instrumentation.name) {
hook(require.cache[ids[i]].exports)
continue
}
const id = ids[i].replace(pathSepExpr, '/')
if (!id.includes(`/node_modules/${instrumentation.name}/`)) continue
if (instrumentation.file) {
if (!id.endsWith(`/node_modules/${filename(instrumentation.name, instrumentation.file)}`)) continue
const basedir = getBasedir(ids[i])
pkg = requirePackageJson(basedir, module)
} else {
const basedir = getBasedir(ids[i])
pkg = requirePackageJson(basedir, module)
const mainFile = path.posix.normalize(pkg.main || 'index.js')
if (!id.endsWith(`/node_modules/${instrumentation.name}/${mainFile}`)) continue
}
if (!matchVersion(pkg.version, instrumentation.versions)) continue
hook(require.cache[ids[i]].exports)
}
}
function getBasedir (id) {
return parse(id).basedir.replace(pathSepExpr, '/')
}
// AsyncResource.bind exists and binds `this` properly only from 17.8.0 and up.
// https://nodejs.org/api/async_context.html#asyncresourcebindfn-thisarg
if (semver.satisfies(process.versions.node, '>=17.8.0')) {
exports.AsyncResource = AsyncResource
} else {
exports.AsyncResource = class extends AsyncResource {
static bind (fn, type, thisArg) {
type = type || fn.name
return (new exports.AsyncResource(type || 'bound-anonymous-fn')).bind(fn, thisArg)
}
bind (fn, thisArg) {
let bound
if (thisArg === undefined) {
const resource = this
bound = function (...args) {
args.unshift(fn, this)
return Reflect.apply(resource.runInAsyncScope, resource, args)
}
} else {
bound = this.runInAsyncScope.bind(this, fn, thisArg)
}
Object.defineProperties(bound, {
'length': {
configurable: true,
enumerable: false,
value: fn.length,
writable: false
},
'asyncResource': {
configurable: true,
enumerable: true,
value: this,
writable: true
}
})
return bound
}
}
}