@redthreadlabs/tracelog 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +126 -0
  3. package/index.d.ts +464 -0
  4. package/index.js +11 -0
  5. package/lib/InflightEventSet.js +53 -0
  6. package/lib/activation-method.js +97 -0
  7. package/lib/agent.js +1226 -0
  8. package/lib/apm-client/apm-client.js +107 -0
  9. package/lib/apm-client/channel-writer.js +334 -0
  10. package/lib/apm-client/jsonl-file-client.js +241 -0
  11. package/lib/apm-client/ndjson.js +20 -0
  12. package/lib/apm-client/noop-apm-client.js +79 -0
  13. package/lib/apm-client/s3-uploader.js +308 -0
  14. package/lib/apm-client/truncate.js +507 -0
  15. package/lib/async-hooks-polyfill.js +58 -0
  16. package/lib/cloud-metadata/aws.js +175 -0
  17. package/lib/cloud-metadata/azure.js +123 -0
  18. package/lib/cloud-metadata/callback-coordination.js +159 -0
  19. package/lib/cloud-metadata/gcp.js +133 -0
  20. package/lib/cloud-metadata/index.js +175 -0
  21. package/lib/config/config.js +431 -0
  22. package/lib/config/normalizers.js +649 -0
  23. package/lib/config/schema.js +946 -0
  24. package/lib/constants.js +35 -0
  25. package/lib/errors.js +303 -0
  26. package/lib/filters/sanitize-field-names.js +69 -0
  27. package/lib/http-request.js +249 -0
  28. package/lib/instrumentation/context.js +56 -0
  29. package/lib/instrumentation/dropped-spans-stats.js +112 -0
  30. package/lib/instrumentation/elasticsearch-shared.js +63 -0
  31. package/lib/instrumentation/express-utils.js +91 -0
  32. package/lib/instrumentation/generic-span.js +322 -0
  33. package/lib/instrumentation/http-shared.js +424 -0
  34. package/lib/instrumentation/ids.js +39 -0
  35. package/lib/instrumentation/index.js +1078 -0
  36. package/lib/instrumentation/modules/@apollo/server.js +39 -0
  37. package/lib/instrumentation/modules/@aws-sdk/client-dynamodb.js +143 -0
  38. package/lib/instrumentation/modules/@aws-sdk/client-s3.js +230 -0
  39. package/lib/instrumentation/modules/@aws-sdk/client-sns.js +197 -0
  40. package/lib/instrumentation/modules/@aws-sdk/client-sqs.js +336 -0
  41. package/lib/instrumentation/modules/@elastic/elasticsearch.js +343 -0
  42. package/lib/instrumentation/modules/@hapi/hapi.js +221 -0
  43. package/lib/instrumentation/modules/@redis/client/dist/lib/client/commands-queue.js +178 -0
  44. package/lib/instrumentation/modules/@redis/client/dist/lib/client/index.js +49 -0
  45. package/lib/instrumentation/modules/@smithy/smithy-client.js +198 -0
  46. package/lib/instrumentation/modules/apollo-server-core.js +49 -0
  47. package/lib/instrumentation/modules/aws-sdk/dynamodb.js +155 -0
  48. package/lib/instrumentation/modules/aws-sdk/s3.js +184 -0
  49. package/lib/instrumentation/modules/aws-sdk/sns.js +232 -0
  50. package/lib/instrumentation/modules/aws-sdk/sqs.js +361 -0
  51. package/lib/instrumentation/modules/aws-sdk.js +76 -0
  52. package/lib/instrumentation/modules/bluebird.js +93 -0
  53. package/lib/instrumentation/modules/cassandra-driver.js +280 -0
  54. package/lib/instrumentation/modules/elasticsearch.js +200 -0
  55. package/lib/instrumentation/modules/express-graphql.js +66 -0
  56. package/lib/instrumentation/modules/express-queue.js +28 -0
  57. package/lib/instrumentation/modules/express.js +162 -0
  58. package/lib/instrumentation/modules/fastify.js +179 -0
  59. package/lib/instrumentation/modules/finalhandler.js +41 -0
  60. package/lib/instrumentation/modules/generic-pool.js +85 -0
  61. package/lib/instrumentation/modules/graphql.js +256 -0
  62. package/lib/instrumentation/modules/handlebars.js +33 -0
  63. package/lib/instrumentation/modules/http.js +112 -0
  64. package/lib/instrumentation/modules/http2.js +320 -0
  65. package/lib/instrumentation/modules/https.js +68 -0
  66. package/lib/instrumentation/modules/ioredis.js +94 -0
  67. package/lib/instrumentation/modules/jade.js +29 -0
  68. package/lib/instrumentation/modules/kafkajs.js +476 -0
  69. package/lib/instrumentation/modules/knex.js +91 -0
  70. package/lib/instrumentation/modules/koa-router.js +74 -0
  71. package/lib/instrumentation/modules/koa.js +15 -0
  72. package/lib/instrumentation/modules/memcached.js +100 -0
  73. package/lib/instrumentation/modules/mimic-response.js +45 -0
  74. package/lib/instrumentation/modules/mongodb/lib/cmap/connection_pool.js +40 -0
  75. package/lib/instrumentation/modules/mongodb-core.js +206 -0
  76. package/lib/instrumentation/modules/mongodb.js +259 -0
  77. package/lib/instrumentation/modules/mysql.js +200 -0
  78. package/lib/instrumentation/modules/mysql2.js +140 -0
  79. package/lib/instrumentation/modules/pg.js +148 -0
  80. package/lib/instrumentation/modules/pug.js +29 -0
  81. package/lib/instrumentation/modules/redis.js +176 -0
  82. package/lib/instrumentation/modules/restify.js +52 -0
  83. package/lib/instrumentation/modules/tedious.js +159 -0
  84. package/lib/instrumentation/modules/undici.js +270 -0
  85. package/lib/instrumentation/modules/ws.js +59 -0
  86. package/lib/instrumentation/noop-transaction.js +81 -0
  87. package/lib/instrumentation/run-context/AbstractRunContextManager.js +215 -0
  88. package/lib/instrumentation/run-context/AsyncHooksRunContextManager.js +106 -0
  89. package/lib/instrumentation/run-context/AsyncLocalStorageRunContextManager.js +73 -0
  90. package/lib/instrumentation/run-context/BasicRunContextManager.js +82 -0
  91. package/lib/instrumentation/run-context/RunContext.js +151 -0
  92. package/lib/instrumentation/run-context/index.js +23 -0
  93. package/lib/instrumentation/shimmer.js +123 -0
  94. package/lib/instrumentation/span-compression.js +239 -0
  95. package/lib/instrumentation/span.js +621 -0
  96. package/lib/instrumentation/template-shared.js +43 -0
  97. package/lib/instrumentation/timer.js +84 -0
  98. package/lib/instrumentation/transaction.js +571 -0
  99. package/lib/load-source-map.js +100 -0
  100. package/lib/logging.js +212 -0
  101. package/lib/metrics/index.js +92 -0
  102. package/lib/metrics/platforms/generic/index.js +40 -0
  103. package/lib/metrics/platforms/generic/process-cpu.js +22 -0
  104. package/lib/metrics/platforms/generic/process-top.js +157 -0
  105. package/lib/metrics/platforms/generic/stats.js +34 -0
  106. package/lib/metrics/platforms/generic/system-cpu.js +51 -0
  107. package/lib/metrics/platforms/linux/index.js +19 -0
  108. package/lib/metrics/platforms/linux/stats.js +213 -0
  109. package/lib/metrics/queue.js +90 -0
  110. package/lib/metrics/registry.js +52 -0
  111. package/lib/metrics/reporter.js +119 -0
  112. package/lib/metrics/runtime.js +77 -0
  113. package/lib/middleware/connect.js +16 -0
  114. package/lib/parsers.js +225 -0
  115. package/lib/propwrap.js +147 -0
  116. package/lib/stacktraces.js +537 -0
  117. package/lib/symbols.js +15 -0
  118. package/lib/tracecontext/index.js +115 -0
  119. package/lib/tracecontext/traceparent.js +185 -0
  120. package/lib/tracecontext/tracestate.js +388 -0
  121. package/lib/wildcard-matcher.js +52 -0
  122. package/loader.mjs +7 -0
  123. package/package.json +98 -0
  124. package/start.d.ts +8 -0
  125. package/start.js +29 -0
  126. package/types/aws-lambda.d.ts +98 -0
  127. package/types/connect.d.ts +23 -0
@@ -0,0 +1,1078 @@
1
+ /*
2
+ * Copyright Elasticsearch B.V. and other contributors where applicable.
3
+ * Licensed under the BSD 2-Clause License; you may not use this file except in
4
+ * compliance with the BSD 2-Clause License.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ var fs = require('fs');
10
+ var path = require('path');
11
+
12
+ const { Hook: RitmHook } = require('require-in-the-middle');
13
+ const IitmHook = require('import-in-the-middle');
14
+ const semver = require('semver');
15
+
16
+ const {
17
+ CONTEXT_MANAGER_ASYNCHOOKS,
18
+ CONTEXT_MANAGER_ASYNCLOCALSTORAGE,
19
+ } = require('../constants');
20
+ var { Ids } = require('./ids');
21
+ var Transaction = require('./transaction');
22
+ var { NoopTransaction } = require('./noop-transaction');
23
+ const {
24
+ AsyncHooksRunContextManager,
25
+ AsyncLocalStorageRunContextManager,
26
+ } = require('./run-context');
27
+ const undiciInstr = require('./modules/undici');
28
+
29
+ const nodeSupportsAsyncLocalStorage = semver.satisfies(
30
+ process.versions.node,
31
+ '>=14.5 || ^12.19.0',
32
+ );
33
+ // Node v16.5.0 added fetch support (behind `--experimental-fetch` until
34
+ // v18.0.0) based on undici@5.0.0. We can instrument undici >=v4.7.1.
35
+ const nodeHasInstrumentableFetch = typeof global.fetch === 'function';
36
+
37
+ var MODULE_PATCHERS = [
38
+ { modPath: '@apollo/server' },
39
+ { modPath: '@smithy/smithy-client' }, // Instrument the base client which all AWS-SDK v3 clients extend.
40
+ {
41
+ modPath: '@aws-sdk/smithy-client',
42
+ patcher: './modules/@smithy/smithy-client.js',
43
+ },
44
+ { modPath: '@elastic/elasticsearch' },
45
+ {
46
+ modPath: '@elastic/elasticsearch-canary',
47
+ patcher: './modules/@elastic/elasticsearch.js',
48
+ },
49
+ { modPath: '@redis/client/dist/lib/client/index.js', diKey: 'redis' },
50
+ {
51
+ modPath: '@redis/client/dist/lib/client/commands-queue.js',
52
+ diKey: 'redis',
53
+ },
54
+ {
55
+ modPath: '@node-redis/client/dist/lib/client/index.js',
56
+ patcher: './modules/@redis/client/dist/lib/client/index.js',
57
+ diKey: 'redis',
58
+ },
59
+ {
60
+ modPath: '@node-redis/client/dist/lib/client/commands-queue.js',
61
+ patcher: './modules/@redis/client/dist/lib/client/commands-queue.js',
62
+ diKey: 'redis',
63
+ },
64
+ { modPath: 'apollo-server-core' },
65
+ { modPath: 'aws-sdk' },
66
+ { modPath: 'bluebird' },
67
+ { modPath: 'cassandra-driver' },
68
+ { modPath: 'elasticsearch' },
69
+ { modPath: 'express' },
70
+ { modPath: 'express-graphql' },
71
+ { modPath: 'express-queue' },
72
+ { modPath: 'fastify' },
73
+ { modPath: 'finalhandler' },
74
+ { modPath: 'generic-pool' },
75
+ { modPath: 'graphql' },
76
+ { modPath: 'handlebars' },
77
+ { modPath: '@hapi/hapi' },
78
+ { modPath: 'http' },
79
+ { modPath: 'https' },
80
+ { modPath: 'http2' },
81
+ { modPath: 'ioredis' },
82
+ { modPath: 'jade' },
83
+ { modPath: 'kafkajs' },
84
+ { modPath: 'knex' },
85
+ { modPath: 'koa' },
86
+ { modPath: 'koa-router' },
87
+ { modPath: '@koa/router', patcher: './modules/koa-router.js' },
88
+ { modPath: 'memcached' },
89
+ { modPath: 'mimic-response' },
90
+ { modPath: 'mongodb-core' },
91
+ { modPath: 'mongodb' },
92
+ {
93
+ modPath: 'mongodb/lib/cmap/connection_pool.js',
94
+ patcher: './modules/mongodb/lib/cmap/connection_pool.js',
95
+ },
96
+ { modPath: 'mysql' },
97
+ { modPath: 'mysql2' },
98
+ { modPath: 'pg' },
99
+ { modPath: 'pug' },
100
+ { modPath: 'redis' },
101
+ { modPath: 'restify' },
102
+ { modPath: 'tedious' },
103
+ { modPath: 'undici' },
104
+ { modPath: 'ws' },
105
+ ];
106
+
107
+ /**
108
+ * This is a subset of `MODULES` until ESM support for all is tested.
109
+ *
110
+ * @typedef {Object} IitmModuleInfo
111
+ * @property {boolean} [instrumentImportMod] If false, this indicates that
112
+ * the instrumentation for this module should be passed the
113
+ * `modExports.default` property instead of the `modExports`. For
114
+ * instrumentation of CommonJS modules that do not modify top-level
115
+ * exports, this generally means the instrumentation can remain unchanged.
116
+ * See the handling of the `default` property at
117
+ * https://nodejs.org/api/esm.html#commonjs-namespaces
118
+ *
119
+ * @type {Map<string, IitmModuleInfo>}
120
+ */
121
+ const IITM_MODULES = {
122
+ // This smithy-client entry isn't used for `@aws-sdk/client-*` ESM support
123
+ // because the smithy-client is transitively `require`d by CommonJS aws-sdk
124
+ // code. If a future aws-sdk v3 version switches to ESM imports internally,
125
+ // then this will be relevant.
126
+ // '@aws-sdk/smithy-client': { instrumentImportMod: false },
127
+ 'cassandra-driver': { instrumentImportMod: false },
128
+ express: { instrumentImportMod: false },
129
+ fastify: { instrumentImportMod: true },
130
+ http: { instrumentImportMod: true },
131
+ https: { instrumentImportMod: true },
132
+ ioredis: { instrumentImportMod: false },
133
+ knex: { instrumentImportMod: false },
134
+ pg: { instrumentImportMod: false },
135
+ };
136
+
137
+ /**
138
+ * modPath modName
139
+ * ------- ---------
140
+ * mongodb mongodb
141
+ * mongodb/lib/foo.js mongodb
142
+ * @elastic/elasticsearch @elastic/elasticsearch
143
+ * @redis/client/dist/lib/client.js @redis/client
144
+ * /var/task/index.js /var/task/index.js
145
+ */
146
+ function modNameFromModPath(modPath) {
147
+ if (modPath.startsWith('/')) {
148
+ return modPath;
149
+ } else if (modPath.startsWith('@')) {
150
+ return modPath.split('/', 2).join('/');
151
+ } else {
152
+ return modPath.split('/', 1)[0];
153
+ }
154
+ }
155
+
156
+ function normPathSeps(s) {
157
+ return path.sep !== '/' ? s.split(path.sep).join('/') : s;
158
+ }
159
+
160
+ /**
161
+ * Holds the registered set of "patchers" (functions that monkey patch imported
162
+ * modules) for a module path (`modPath`).
163
+ */
164
+ class PatcherRegistry {
165
+ constructor() {
166
+ this.reset();
167
+ }
168
+
169
+ reset() {
170
+ this._infoFromModPath = {};
171
+ }
172
+
173
+ /**
174
+ * Add a patcher for the given module path.
175
+ *
176
+ * @param {string} modPath - Identifies a module that RITM can hook: a
177
+ * module name (http, @smithy/client), a module-relative path
178
+ * (mongodb/lib/cmap/connection_pool.js), an absolute path
179
+ * (/var/task/index.js; Windows paths are not supported), a sub-module
180
+ * (react-dom/server).
181
+ * @param {import('../..').PatchHandler | string} patcher - A patcher function
182
+ * or a path to a CommonJS module that exports one as the default export.
183
+ * @param {string} [diKey] - An optional key in the `disableInstrumentations`
184
+ * config var that is used to determine if this patcher is
185
+ * disabled. All patchers for the same modPath must share the same `diKey`.
186
+ * This throws if a conflicting `diKey` is given.
187
+ * It defaults to the `modName` (derived from the `modPath`).
188
+ */
189
+ add(modPath, patcher, diKey = null) {
190
+ if (!(modPath in this._infoFromModPath)) {
191
+ this._infoFromModPath[modPath] = {
192
+ patchers: [patcher],
193
+ diKey: diKey || modNameFromModPath(modPath),
194
+ };
195
+ } else {
196
+ const entry = this._infoFromModPath[modPath];
197
+ // The `diKey`, if provided, must be the same for all patchers for a modPath.
198
+ if (diKey && diKey !== entry.diKey) {
199
+ throw new Error(
200
+ `invalid "diKey", ${diKey}, for module "${modPath}" patcher: it conflicts with existing diKey=${entry.diKey}`,
201
+ );
202
+ }
203
+ entry.patchers.push(patcher);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Remove the given patcher for the given module path.
209
+ */
210
+ remove(modPath, patcher) {
211
+ const entry = this._infoFromModPath[modPath];
212
+ if (!entry) {
213
+ return;
214
+ }
215
+ const idx = entry.patchers.indexOf(patcher);
216
+ if (idx !== -1) {
217
+ entry.patchers.splice(idx, 1);
218
+ }
219
+ if (entry.patchers.length === 0) {
220
+ delete this._infoFromModPath[modPath];
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Remove all patchers for the given module path.
226
+ */
227
+ clear(modPath) {
228
+ delete this._infoFromModPath[modPath];
229
+ }
230
+
231
+ has(modPath) {
232
+ return modPath in this._infoFromModPath;
233
+ }
234
+
235
+ getPatchers(modPath) {
236
+ return this._infoFromModPath[modPath]?.patchers;
237
+ }
238
+
239
+ /**
240
+ * Returns the appropriate RITM `modules` argument so that all registered
241
+ * `modPath`s will be hooked. This assumes `{internals: true}` RITM options
242
+ * are used.
243
+ *
244
+ * @returns {Array<string>}
245
+ */
246
+ ritmModulesArg() {
247
+ // RITM hooks:
248
+ // 1. `require('mongodb')` if 'mongodb' is in the modules arg;
249
+ // 2. `require('mongodb/lib/foo.js')`, a module-relative path, if 'mongodb'
250
+ // is in the modules arg and `{internals: true}` option is given;
251
+ // 3. `require('/var/task/index.js')` if the exact resolved absolute path
252
+ // is in the modules arg; and
253
+ // 4. `require('react-dom/server')`, a "sub-module", if 'react-dom/server'
254
+ // is in the modules arg.
255
+ //
256
+ // The wrinkle is that the modPath "mongodb/lib/foo.js" need not be in the
257
+ // `modules` argument to RITM, but the similar-looking "react-dom/server"
258
+ // must be.
259
+ const modules = new Set();
260
+ const hasModExt = /\.(js|cjs|mjs|json)$/;
261
+ Object.keys(this._infoFromModPath).forEach((modPath) => {
262
+ const modName = modNameFromModPath(modPath);
263
+ if (modPath === modName) {
264
+ modules.add(modPath);
265
+ } else {
266
+ if (hasModExt.test(modPath)) {
267
+ modules.add(modName); // case 2
268
+ } else {
269
+ // Beware the RITM bug: passing both 'foo' and 'foo/subpath' results
270
+ // in 'foo/subpath' not being hooked.
271
+ // TODO: link to issue for this
272
+ modules.add(modPath); // case 4
273
+ }
274
+ }
275
+ });
276
+
277
+ return Array.from(modules);
278
+ }
279
+
280
+ /**
281
+ * Get the string on the `disableInstrumentations` config var that indicates
282
+ * if this module path should be disabled.
283
+ *
284
+ * Typically this is the module name -- e.g. "@redis/client" -- but might be
285
+ * a custom value -- e.g. "lambda" for a Lambda handler path.
286
+ *
287
+ * @returns {string | undefined}
288
+ */
289
+ diKey(modPath) {
290
+ return this._infoFromModPath[modPath]?.diKey;
291
+ }
292
+ }
293
+
294
+ function Instrumentation(agent) {
295
+ this._agent = agent;
296
+ this._disableInstrumentationsSet = null;
297
+ this._ritmHook = null;
298
+ this._iitmHook = null;
299
+ this._started = false;
300
+ this._runCtxMgr = null;
301
+ this._log = agent.logger;
302
+ this._patcherReg = new PatcherRegistry();
303
+ this._cachedVerFromModBaseDir = new Map();
304
+ }
305
+
306
+ Instrumentation.prototype.currTransaction = function () {
307
+ if (!this._started) {
308
+ return null;
309
+ }
310
+ return this._runCtxMgr.active().currTransaction();
311
+ };
312
+
313
+ Instrumentation.prototype.currSpan = function () {
314
+ if (!this._started) {
315
+ return null;
316
+ }
317
+ return this._runCtxMgr.active().currSpan();
318
+ };
319
+
320
+ Instrumentation.prototype.ids = function () {
321
+ if (!this._started) {
322
+ return new Ids();
323
+ }
324
+ const runContext = this._runCtxMgr.active();
325
+ const currSpanOrTrans = runContext.currSpan() || runContext.currTransaction();
326
+ if (currSpanOrTrans) {
327
+ return currSpanOrTrans.ids;
328
+ }
329
+ return new Ids();
330
+ };
331
+
332
+ Instrumentation.prototype.addPatch = function (modules, handler) {
333
+ if (!Array.isArray(modules)) {
334
+ modules = [modules];
335
+ }
336
+ for (const modPath of modules) {
337
+ const type = typeof handler;
338
+ if (type !== 'function' && type !== 'string') {
339
+ this._agent.logger.error('Invalid patch handler type: %s', type);
340
+ return;
341
+ }
342
+ this._patcherReg.add(modPath, handler);
343
+ }
344
+ this._restartHooks();
345
+ };
346
+
347
+ Instrumentation.prototype.removePatch = function (modules, handler) {
348
+ if (!Array.isArray(modules)) modules = [modules];
349
+
350
+ for (const modPath of modules) {
351
+ this._patcherReg.remove(modPath, handler);
352
+ }
353
+
354
+ this._restartHooks();
355
+ };
356
+
357
+ Instrumentation.prototype.clearPatches = function (modules) {
358
+ if (!Array.isArray(modules)) modules = [modules];
359
+
360
+ for (const modPath of modules) {
361
+ this._patcherReg.clear(modPath);
362
+ }
363
+
364
+ this._restartHooks();
365
+ };
366
+
367
+ // Start the instrumentation system.
368
+ //
369
+ // @param {RunContext} [runContextClass] - A class to use for the core object
370
+ // that is used to track run context. It defaults to `RunContext`. If given,
371
+ // it must be `RunContext` (the typical case) or a subclass of it. The OTel
372
+ // Bridge uses this to provide a subclass that bridges to OpenTelemetry
373
+ // `Context` usage.
374
+ Instrumentation.prototype.start = function (runContextClass) {
375
+ if (this._started) return;
376
+ this._started = true;
377
+
378
+ // Could have changed in Agent.start().
379
+ this._log = this._agent.logger;
380
+
381
+ // Select the appropriate run-context manager.
382
+ const confContextManager = this._agent._conf.contextManager;
383
+ if (confContextManager === CONTEXT_MANAGER_ASYNCHOOKS) {
384
+ this._runCtxMgr = new AsyncHooksRunContextManager(
385
+ this._log,
386
+ runContextClass,
387
+ );
388
+ } else if (nodeSupportsAsyncLocalStorage) {
389
+ this._runCtxMgr = new AsyncLocalStorageRunContextManager(
390
+ this._log,
391
+ runContextClass,
392
+ );
393
+ } else {
394
+ if (confContextManager === CONTEXT_MANAGER_ASYNCLOCALSTORAGE) {
395
+ this._log.warn(
396
+ `config includes 'contextManager="${confContextManager}"', but node ${process.version} does not support AsyncLocalStorage for run-context management: falling back to using async_hooks`,
397
+ );
398
+ }
399
+ this._runCtxMgr = new AsyncHooksRunContextManager(
400
+ this._log,
401
+ runContextClass,
402
+ );
403
+ }
404
+
405
+ // Load module patchers: from MODULE_PATCHERS, for Lambda, and from
406
+ // config.addPatch.
407
+ for (let info of MODULE_PATCHERS) {
408
+ let patcher;
409
+ if (info.patcher) {
410
+ patcher = path.resolve(__dirname, info.patcher);
411
+ } else {
412
+ // Typically the patcher module for the APM agent's included
413
+ // instrumentations is "./modules/${modPath}[.js]".
414
+ patcher = path.resolve(
415
+ __dirname,
416
+ 'modules',
417
+ info.modPath + (info.modPath.endsWith('.js') ? '' : '.js'),
418
+ );
419
+ }
420
+
421
+ this._patcherReg.add(info.modPath, patcher, info.diKey);
422
+ }
423
+
424
+ const patches = this._agent._conf.addPatch;
425
+ if (Array.isArray(patches)) {
426
+ for (const [modPath, patcher] of patches) {
427
+ this._patcherReg.add(modPath, patcher);
428
+ }
429
+ }
430
+
431
+ this._runCtxMgr.enable();
432
+ this._restartHooks();
433
+
434
+ if (nodeHasInstrumentableFetch && this._isModuleEnabled('undici')) {
435
+ this._log.debug('instrumenting fetch');
436
+ undiciInstr.instrumentUndici(this._agent);
437
+ }
438
+
439
+ };
440
+
441
+ // Stop active instrumentation and reset global state *as much as possible*.
442
+ //
443
+ // Limitations: Removing and re-applying 'require-in-the-middle'-based patches
444
+ // has no way to update existing references to patched or unpatched exports from
445
+ // those modules.
446
+ Instrumentation.prototype.stop = function () {
447
+ this._started = false;
448
+
449
+ // Reset run context tracking.
450
+ if (this._runCtxMgr) {
451
+ this._runCtxMgr.disable();
452
+ this._runCtxMgr = null;
453
+ }
454
+
455
+ // Reset patching.
456
+ if (this._ritmHook) {
457
+ this._ritmHook.unhook();
458
+ this._ritmHook = null;
459
+ }
460
+ if (this._iitmHook) {
461
+ this._iitmHook.unhook();
462
+ this._iitmHook = null;
463
+ }
464
+ this._patcherReg.reset();
465
+ if (nodeHasInstrumentableFetch) {
466
+ undiciInstr.uninstrumentUndici();
467
+ }
468
+ };
469
+
470
+ // Reset internal state for (relatively) clean re-use of this Instrumentation.
471
+ // Used for testing, while `resetAgent()` + "test/_agent.js" usage still exists.
472
+ //
473
+ // This does *not* include redoing monkey patching. It resets context tracking,
474
+ // so a subsequent test case can re-use the Instrumentation in the same process.
475
+ Instrumentation.prototype.testReset = function () {
476
+ if (this._runCtxMgr) {
477
+ this._runCtxMgr.testReset();
478
+ }
479
+ };
480
+
481
+ Instrumentation.prototype._isModuleEnabled = function (modName) {
482
+ if (!this._disableInstrumentationsSet) {
483
+ this._disableInstrumentationsSet = new Set(
484
+ this._agent._conf.disableInstrumentations,
485
+ );
486
+ }
487
+ return (
488
+ this._agent._conf.instrument &&
489
+ !this._disableInstrumentationsSet.has(modName)
490
+ );
491
+ };
492
+
493
+ Instrumentation.prototype._restartHooks = function () {
494
+ if (!this._started) {
495
+ return;
496
+ }
497
+ if (this._ritmHook || this._iitmHook) {
498
+ this._agent.logger.debug('removing hooks to Node.js module loader');
499
+ if (this._ritmHook) {
500
+ this._ritmHook.unhook();
501
+ }
502
+ if (this._iitmHook) {
503
+ this._iitmHook.unhook();
504
+ }
505
+ }
506
+
507
+ var self = this;
508
+
509
+ this._log.debug('adding Node.js module loader hooks');
510
+
511
+ this._ritmHook = new RitmHook(
512
+ this._patcherReg.ritmModulesArg(),
513
+ { internals: true },
514
+ function (exports, modPath, basedir) {
515
+ let version = undefined;
516
+
517
+ // RITM returns `modPath` using native path separators. However,
518
+ // _patcherReg is keyed with '/' separators, so we need to normalize.
519
+ modPath = normPathSeps(modPath);
520
+
521
+ if (!self._patcherReg.has(modPath)) {
522
+ // Skip out if there are no patchers for this hooked module name.
523
+ return exports;
524
+ }
525
+
526
+ // Find an appropriate version for this modPath.
527
+ if (!basedir) {
528
+ // This is a core module.
529
+ version = process.versions.node;
530
+ } else {
531
+ // This is a module (e.g. 'mongodb') or a module internal path
532
+ // ('mongodb/lib/cmap/connection_pool.js').
533
+ version = self._getPackageVersion(modPath, basedir);
534
+ if (version === undefined) {
535
+ self._log.debug('could not patch %s module', modPath);
536
+ return exports;
537
+ }
538
+ }
539
+
540
+ const diKey = self._patcherReg.diKey(modPath);
541
+ const enabled = self._isModuleEnabled(diKey);
542
+ return self._patchModule(exports, modPath, version, enabled, false);
543
+ },
544
+ );
545
+
546
+ this._iitmHook = IitmHook(
547
+ // TODO: Eventually derive this from `_patcherRegistry`.
548
+ Object.keys(IITM_MODULES),
549
+ function (modExports, modName, modBaseDir) {
550
+ const enabled = self._isModuleEnabled(modName);
551
+ const version = modBaseDir
552
+ ? self._getPackageVersion(modName, modBaseDir)
553
+ : process.versions.node;
554
+ if (IITM_MODULES[modName].instrumentImportMod) {
555
+ return self._patchModule(modExports, modName, version, enabled, true);
556
+ } else {
557
+ modExports.default = self._patchModule(
558
+ modExports.default,
559
+ modName,
560
+ version,
561
+ enabled,
562
+ false,
563
+ );
564
+ return modExports;
565
+ }
566
+ },
567
+ );
568
+ };
569
+
570
+ Instrumentation.prototype._getPackageVersion = function (modName, modBaseDir) {
571
+ if (this._cachedVerFromModBaseDir.has(modBaseDir)) {
572
+ return this._cachedVerFromModBaseDir.get(modBaseDir);
573
+ }
574
+
575
+ let ver = undefined;
576
+ try {
577
+ const version = JSON.parse(
578
+ fs.readFileSync(path.join(modBaseDir, 'package.json')),
579
+ ).version;
580
+ if (typeof version === 'string') {
581
+ ver = version;
582
+ }
583
+ } catch (err) {
584
+ this._agent.logger.debug(
585
+ { modName, modBaseDir, err },
586
+ 'could not load package version',
587
+ );
588
+ }
589
+
590
+ this._cachedVerFromModBaseDir.set(modBaseDir, ver);
591
+ return ver;
592
+ };
593
+
594
+ /**
595
+ * Patch/instrument the given module.
596
+ *
597
+ * @param {Module | any} modExports The object made available by the RITM or
598
+ * IITM hook. For a `require` this is the `module.exports` value, which can
599
+ * by any type. For an `import` this is a `Module` object if
600
+ * `isImportMod=true`, or the default export (the equivalent of
601
+ * `module.exports`) if `isImportMod=false`.
602
+ * @param {string} modPath
603
+ * @param {string} version
604
+ * @param {boolean} enabled Whether instrumentation is enabled for this module
605
+ * depending on the `disableInstrumentations` config value. (Currently the
606
+ * http, https, and http2 instrumentations, at least, do *some* work even if
607
+ * enabled=false.)
608
+ * @param {boolean} isImportMod When false, the `modExports` param is the
609
+ * `module.exports` object (typically from a `require`). When true,
610
+ * `modExports` is the `Module` instance from an `import`. This depends on
611
+ * the `instrumentImportMod` flag that is set per module.
612
+ */
613
+ Instrumentation.prototype._patchModule = function (
614
+ modExports,
615
+ modPath,
616
+ version,
617
+ enabled,
618
+ isImportMod,
619
+ ) {
620
+ this._log.debug(
621
+ 'instrumenting %s@%s module (enabled=%s, isImportMod=%s)',
622
+ modPath,
623
+ version,
624
+ enabled,
625
+ isImportMod,
626
+ );
627
+ const patchers = this._patcherReg.getPatchers(modPath);
628
+ if (patchers) {
629
+ for (let patcher of patchers) {
630
+ if (typeof patcher === 'string') {
631
+ if (patcher[0] === '.') {
632
+ patcher = path.resolve(process.cwd(), patcher);
633
+ }
634
+ patcher = require(patcher);
635
+ }
636
+
637
+ const type = typeof patcher;
638
+ if (type !== 'function') {
639
+ this._agent.logger.error(
640
+ 'Invalid patch handler type "%s" for module "%s"',
641
+ type,
642
+ modPath,
643
+ );
644
+ continue;
645
+ }
646
+
647
+ modExports = patcher(modExports, this._agent, {
648
+ name: modPath,
649
+ version,
650
+ enabled,
651
+ isImportMod,
652
+ });
653
+ }
654
+ }
655
+ return modExports;
656
+ };
657
+
658
+ Instrumentation.prototype.addEndedTransaction = function (transaction) {
659
+ var agent = this._agent;
660
+
661
+ if (!this._started) {
662
+ agent.logger.debug('ignoring transaction %o', {
663
+ trans: transaction.id,
664
+ trace: transaction.traceId,
665
+ });
666
+ return;
667
+ }
668
+
669
+ const rc = this._runCtxMgr.active();
670
+ if (rc.currTransaction() === transaction) {
671
+ // Replace the active run context with an empty one. I.e. there is now
672
+ // no active transaction or span (at least in this async task).
673
+ this._runCtxMgr.supersedeRunContext(this._runCtxMgr.root());
674
+ this._log.debug(
675
+ { ctxmgr: this._runCtxMgr.toString() },
676
+ 'addEndedTransaction(%s)',
677
+ transaction.name,
678
+ );
679
+ }
680
+
681
+ // Avoid transaction filtering time if only propagating trace-context.
682
+ if (agent._conf.contextPropagationOnly) {
683
+ // This one log.trace related to contextPropagationOnly is included as a
684
+ // possible log hint to future debugging for why events are not being sent
685
+ // to APM server.
686
+ agent.logger.trace('contextPropagationOnly: skip sendTransaction');
687
+ return;
688
+ }
689
+
690
+ // https://github.com/elastic/apm/blob/main/specs/agents/tracing-sampling.md#non-sampled-transactions
691
+ if (
692
+ !transaction.sampled &&
693
+ !agent._apmClient.supportsKeepingUnsampledTransaction()
694
+ ) {
695
+ agent.logger.debug(
696
+ { trans: transaction.id, trace: transaction.traceId },
697
+ 'dropping unsampled transaction',
698
+ );
699
+ return;
700
+ }
701
+
702
+ // if I have ended and I have something buffered, send that buffered thing
703
+ if (transaction.getBufferedSpan()) {
704
+ this._encodeAndSendSpan(transaction.getBufferedSpan());
705
+ }
706
+
707
+ var payload = agent._transactionFilters.process(transaction._encode());
708
+ if (!payload) {
709
+ agent.logger.debug('transaction ignored by filter %o', {
710
+ trans: transaction.id,
711
+ trace: transaction.traceId,
712
+ });
713
+ return;
714
+ }
715
+
716
+ agent.logger.debug('sending transaction %o', {
717
+ trans: transaction.id,
718
+ trace: transaction.traceId,
719
+ });
720
+ agent._apmClient.sendTransaction(payload);
721
+ };
722
+
723
+ Instrumentation.prototype.addEndedSpan = function (span) {
724
+ var agent = this._agent;
725
+
726
+ if (!this._started) {
727
+ agent.logger.debug('ignoring span %o', {
728
+ span: span.id,
729
+ parent: span.parentId,
730
+ trace: span.traceId,
731
+ name: span.name,
732
+ type: span.type,
733
+ });
734
+ return;
735
+ }
736
+
737
+ // Replace the active run context with this span removed. Typically this
738
+ // span is the top of stack (i.e. is the current span). However, it is
739
+ // possible to have out-of-order span.end(), in which case the ended span
740
+ // might not.
741
+ const newRc = this._runCtxMgr.active().leaveSpan(span);
742
+ if (newRc) {
743
+ this._runCtxMgr.supersedeRunContext(newRc);
744
+ }
745
+ this._log.debug(
746
+ { ctxmgr: this._runCtxMgr.toString() },
747
+ 'addEndedSpan(%s)',
748
+ span.name,
749
+ );
750
+
751
+ // Avoid span encoding time if only propagating trace-context.
752
+ if (agent._conf.contextPropagationOnly) {
753
+ return;
754
+ }
755
+
756
+ if (!span.isRecorded()) {
757
+ span.transaction.captureDroppedSpan(span);
758
+ return;
759
+ }
760
+
761
+ if (!this._agent._conf.spanCompressionEnabled) {
762
+ this._encodeAndSendSpan(span);
763
+ } else {
764
+ // if I have ended and I have something buffered, send that buffered thing
765
+ if (span.getBufferedSpan()) {
766
+ this._encodeAndSendSpan(span.getBufferedSpan());
767
+ span.setBufferedSpan(null);
768
+ }
769
+
770
+ const parentSpan = span.getParentSpan();
771
+ if ((parentSpan && parentSpan.ended) || !span.isCompressionEligible()) {
772
+ const buffered = parentSpan && parentSpan.getBufferedSpan();
773
+ if (buffered) {
774
+ this._encodeAndSendSpan(buffered);
775
+ parentSpan.setBufferedSpan(null);
776
+ }
777
+ this._encodeAndSendSpan(span);
778
+ } else if (!parentSpan.getBufferedSpan()) {
779
+ // span is compressible and there's nothing buffered
780
+ // add to buffer, move on
781
+ parentSpan.setBufferedSpan(span);
782
+ } else if (!parentSpan.getBufferedSpan().tryToCompress(span)) {
783
+ // we could not compress span so SEND bufferend span
784
+ // and buffer the span we could not compress
785
+ this._encodeAndSendSpan(parentSpan.getBufferedSpan());
786
+ parentSpan.setBufferedSpan(span);
787
+ }
788
+ }
789
+ };
790
+
791
+ Instrumentation.prototype._encodeAndSendSpan = function (span) {
792
+ const duration = span.isComposite()
793
+ ? span.getCompositeSum()
794
+ : span.duration();
795
+ if (
796
+ span.discardable &&
797
+ duration / 1000 < this._agent._conf.exitSpanMinDuration
798
+ ) {
799
+ span.transaction.captureDroppedSpan(span);
800
+ return;
801
+ }
802
+
803
+ const agent = this._agent;
804
+ // Note this error as an "inflight" event. See Agent#flush().
805
+ const inflightEvents = agent._inflightEvents;
806
+ inflightEvents.add(span.id);
807
+
808
+ agent.logger.debug('encoding span %o', {
809
+ span: span.id,
810
+ parent: span.parentId,
811
+ trace: span.traceId,
812
+ name: span.name,
813
+ type: span.type,
814
+ });
815
+ span._encode(function (err, payload) {
816
+ if (err) {
817
+ agent.logger.error('error encoding span %o', {
818
+ span: span.id,
819
+ parent: span.parentId,
820
+ trace: span.traceId,
821
+ name: span.name,
822
+ type: span.type,
823
+ error: err.message,
824
+ });
825
+ } else {
826
+ payload = agent._spanFilters.process(payload);
827
+ if (!payload) {
828
+ agent.logger.debug('span ignored by filter %o', {
829
+ span: span.id,
830
+ parent: span.parentId,
831
+ trace: span.traceId,
832
+ name: span.name,
833
+ type: span.type,
834
+ });
835
+ } else {
836
+ agent.logger.debug('sending span %o', {
837
+ span: span.id,
838
+ parent: span.parentId,
839
+ trace: span.traceId,
840
+ name: span.name,
841
+ type: span.type,
842
+ });
843
+ if (agent._apmClient) {
844
+ agent._apmClient.sendSpan(payload);
845
+ }
846
+ }
847
+ }
848
+ inflightEvents.delete(span.id);
849
+ });
850
+ };
851
+
852
+ // Replace the current run context with one where the given transaction is
853
+ // current.
854
+ Instrumentation.prototype.supersedeWithTransRunContext = function (trans) {
855
+ if (this._started) {
856
+ const rc = this._runCtxMgr.root().enterTrans(trans);
857
+ this._runCtxMgr.supersedeRunContext(rc);
858
+ this._log.debug(
859
+ { ctxmgr: this._runCtxMgr.toString() },
860
+ 'supersedeWithTransRunContext(<Trans %s>)',
861
+ trans.id,
862
+ );
863
+ }
864
+ };
865
+
866
+ // Replace the current run context with one where the given span is current.
867
+ Instrumentation.prototype.supersedeWithSpanRunContext = function (span) {
868
+ if (this._started) {
869
+ const rc = this._runCtxMgr.active().enterSpan(span);
870
+ this._runCtxMgr.supersedeRunContext(rc);
871
+ this._log.debug(
872
+ { ctxmgr: this._runCtxMgr.toString() },
873
+ 'supersedeWithSpanRunContext(<Span %s>)',
874
+ span.id,
875
+ );
876
+ }
877
+ };
878
+
879
+ // Set the current run context to have *no* transaction. No spans will be
880
+ // created in this run context until a subsequent `startTransaction()`.
881
+ Instrumentation.prototype.supersedeWithEmptyRunContext = function () {
882
+ if (this._started) {
883
+ this._runCtxMgr.supersedeRunContext(this._runCtxMgr.root());
884
+ this._log.debug(
885
+ { ctxmgr: this._runCtxMgr.toString() },
886
+ 'supersedeWithEmptyRunContext()',
887
+ );
888
+ }
889
+ };
890
+
891
+ // Create a new transaction, but do *not* replace the current run context to
892
+ // make this the "current" transaction. Compare to `startTransaction`.
893
+ Instrumentation.prototype.createTransaction = function (name, ...args) {
894
+ return new Transaction(this._agent, name, ...args);
895
+ };
896
+
897
+ Instrumentation.prototype.startTransaction = function (name, ...args) {
898
+ if (!this._agent.isStarted()) {
899
+ return new NoopTransaction();
900
+ }
901
+ const trans = new Transaction(this._agent, name, ...args);
902
+ this.supersedeWithTransRunContext(trans);
903
+ return trans;
904
+ };
905
+
906
+ Instrumentation.prototype.endTransaction = function (result, endTime) {
907
+ const trans = this.currTransaction();
908
+ if (!trans) {
909
+ this._agent.logger.debug(
910
+ 'cannot end transaction - no active transaction found',
911
+ );
912
+ return;
913
+ }
914
+ trans.end(result, endTime);
915
+ };
916
+
917
+ Instrumentation.prototype.setDefaultTransactionName = function (name) {
918
+ const trans = this.currTransaction();
919
+ if (!trans) {
920
+ this._agent.logger.debug(
921
+ 'no active transaction found - cannot set default transaction name',
922
+ );
923
+ return;
924
+ }
925
+ trans.setDefaultName(name);
926
+ };
927
+
928
+ Instrumentation.prototype.setTransactionName = function (name) {
929
+ const trans = this.currTransaction();
930
+ if (!trans) {
931
+ this._agent.logger.debug(
932
+ 'no active transaction found - cannot set transaction name',
933
+ );
934
+ return;
935
+ }
936
+ trans.name = name;
937
+ };
938
+
939
+ Instrumentation.prototype.setTransactionOutcome = function (outcome) {
940
+ const trans = this.currTransaction();
941
+ if (!trans) {
942
+ this._agent.logger.debug(
943
+ 'no active transaction found - cannot set transaction outcome',
944
+ );
945
+ return;
946
+ }
947
+ trans.setOutcome(outcome);
948
+ };
949
+
950
+ // Create a new span in the current transaction, if any, and make it the
951
+ // current span. The started span is returned. This will return null if a span
952
+ // could not be created -- which could happen for a number of reasons.
953
+ Instrumentation.prototype.startSpan = function (
954
+ name,
955
+ type,
956
+ subtype,
957
+ action,
958
+ opts,
959
+ ) {
960
+ const trans = this.currTransaction();
961
+ if (!trans) {
962
+ this._agent.logger.debug(
963
+ 'no active transaction found - cannot build new span',
964
+ );
965
+ return null;
966
+ }
967
+ return trans.startSpan.apply(trans, arguments);
968
+ };
969
+
970
+ // Create a new span in the current transaction, if any. The created span is
971
+ // returned, or null if the span could not be created.
972
+ //
973
+ // This does *not* replace the current run context to make this span the
974
+ // "current" one. This allows instrumentations to avoid impacting the run
975
+ // context of the calling code. Compare to `startSpan`.
976
+ Instrumentation.prototype.createSpan = function (
977
+ name,
978
+ type,
979
+ subtype,
980
+ action,
981
+ opts,
982
+ ) {
983
+ const trans = this.currTransaction();
984
+ if (!trans) {
985
+ this._agent.logger.debug(
986
+ 'no active transaction found - cannot build new span',
987
+ );
988
+ return null;
989
+ }
990
+ return trans.createSpan.apply(trans, arguments);
991
+ };
992
+
993
+ Instrumentation.prototype.setSpanOutcome = function (outcome) {
994
+ const span = this.currSpan();
995
+ if (!span) {
996
+ this._agent.logger.debug('no active span found - cannot set span outcome');
997
+ return null;
998
+ }
999
+ span.setOutcome(outcome);
1000
+ };
1001
+
1002
+ Instrumentation.prototype.currRunContext = function () {
1003
+ if (!this._started) {
1004
+ return null;
1005
+ }
1006
+ return this._runCtxMgr.active();
1007
+ };
1008
+
1009
+ // Bind the given function to the current run context.
1010
+ Instrumentation.prototype.bindFunction = function (fn) {
1011
+ if (!this._started) {
1012
+ return fn;
1013
+ }
1014
+ return this._runCtxMgr.bindFn(this._runCtxMgr.active(), fn);
1015
+ };
1016
+
1017
+ // Bind the given function to a given run context.
1018
+ Instrumentation.prototype.bindFunctionToRunContext = function (runContext, fn) {
1019
+ if (!this._started) {
1020
+ return fn;
1021
+ }
1022
+ return this._runCtxMgr.bindFn(runContext, fn);
1023
+ };
1024
+
1025
+ // Bind the given function to an *empty* run context.
1026
+ // This can be used to ensure `fn` does *not* run in the context of the current
1027
+ // transaction or span.
1028
+ Instrumentation.prototype.bindFunctionToEmptyRunContext = function (fn) {
1029
+ if (!this._started) {
1030
+ return fn;
1031
+ }
1032
+ return this._runCtxMgr.bindFn(this._runCtxMgr.root(), fn);
1033
+ };
1034
+
1035
+ // Bind the given EventEmitter to the current run context.
1036
+ //
1037
+ // This wraps the emitter so that any added event handler function is bound
1038
+ // as if `bindFunction` had been called on it. Note that `ee` need not
1039
+ // inherit from EventEmitter -- it uses duck typing.
1040
+ Instrumentation.prototype.bindEmitter = function (ee) {
1041
+ if (!this._started) {
1042
+ return ee;
1043
+ }
1044
+ return this._runCtxMgr.bindEE(this._runCtxMgr.active(), ee);
1045
+ };
1046
+
1047
+ // Bind the given EventEmitter to a given run context.
1048
+ Instrumentation.prototype.bindEmitterToRunContext = function (runContext, ee) {
1049
+ if (!this._started) {
1050
+ return ee;
1051
+ }
1052
+ return this._runCtxMgr.bindEE(runContext, ee);
1053
+ };
1054
+
1055
+ // Return true iff the given EventEmitter is bound to a run context.
1056
+ Instrumentation.prototype.isEventEmitterBound = function (ee) {
1057
+ if (!this._started) {
1058
+ return false;
1059
+ }
1060
+ return this._runCtxMgr.isEEBound(ee);
1061
+ };
1062
+
1063
+ // Invoke the given function in the context of `runContext`.
1064
+ Instrumentation.prototype.withRunContext = function (
1065
+ runContext,
1066
+ fn,
1067
+ thisArg,
1068
+ ...args
1069
+ ) {
1070
+ if (!this._started) {
1071
+ return fn.call(thisArg, ...args);
1072
+ }
1073
+ return this._runCtxMgr.with(runContext, fn, thisArg, ...args);
1074
+ };
1075
+
1076
+ module.exports = {
1077
+ Instrumentation,
1078
+ };