@senzops/apm-node 1.2.8 → 1.3.1

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 (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +527 -398
  3. package/dist/index.d.mts +5 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.global.js +1 -1
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/lambda-handler.d.mts +13 -0
  12. package/dist/lambda-handler.d.ts +13 -0
  13. package/dist/lambda-handler.js +2 -0
  14. package/dist/lambda-handler.js.map +1 -0
  15. package/dist/lambda-handler.mjs +2 -0
  16. package/dist/lambda-handler.mjs.map +1 -0
  17. package/dist/register.js +1 -1
  18. package/dist/register.js.map +1 -1
  19. package/dist/register.mjs +1 -1
  20. package/dist/register.mjs.map +1 -1
  21. package/package.json +6 -1
  22. package/src/core/client.ts +57 -0
  23. package/src/core/transport.ts +20 -3
  24. package/src/core/types.ts +5 -1
  25. package/src/index.ts +4 -0
  26. package/src/instrumentation/amqplib.ts +371 -0
  27. package/src/instrumentation/anthropic.ts +245 -0
  28. package/src/instrumentation/aws-sdk.ts +403 -0
  29. package/src/instrumentation/azure-openai.ts +177 -0
  30. package/src/instrumentation/bunyan.ts +93 -0
  31. package/src/instrumentation/cassandra.ts +367 -0
  32. package/src/instrumentation/cohere.ts +227 -0
  33. package/src/instrumentation/connect.ts +200 -0
  34. package/src/instrumentation/dataloader.ts +291 -0
  35. package/src/instrumentation/dns.ts +220 -0
  36. package/src/instrumentation/firebase.ts +445 -0
  37. package/src/instrumentation/fs.ts +260 -0
  38. package/src/instrumentation/generic-pool.ts +317 -0
  39. package/src/instrumentation/google-genai.ts +426 -0
  40. package/src/instrumentation/graphql.ts +434 -0
  41. package/src/instrumentation/grpc.ts +666 -0
  42. package/src/instrumentation/hapi.ts +257 -0
  43. package/src/instrumentation/kafka.ts +360 -0
  44. package/src/instrumentation/knex.ts +249 -0
  45. package/src/instrumentation/lru-memoizer.ts +175 -0
  46. package/src/instrumentation/memcached.ts +190 -0
  47. package/src/instrumentation/mistral.ts +254 -0
  48. package/src/instrumentation/nestjs.ts +243 -0
  49. package/src/instrumentation/net.ts +171 -0
  50. package/src/instrumentation/openai.ts +281 -0
  51. package/src/instrumentation/pino.ts +170 -0
  52. package/src/instrumentation/restify.ts +213 -0
  53. package/src/instrumentation/runtime.ts +352 -0
  54. package/src/instrumentation/socketio.ts +272 -0
  55. package/src/instrumentation/tedious.ts +509 -0
  56. package/src/instrumentation/winston.ts +149 -0
  57. package/src/lambda-handler.ts +262 -0
  58. package/src/register.ts +22 -3
  59. package/src/wrappers/lambda.ts +417 -0
  60. package/tsup.config.ts +4 -4
  61. package/wiki.md +1693 -852
@@ -0,0 +1,291 @@
1
+ import { SenzorOptions } from '../core/types';
2
+ import { hookRequire } from './hook';
3
+ import { patchMethod } from './patch';
4
+ import { runWithCapturedSpan, startCapturedSpan } from './span';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // DataLoader Instrumentation
8
+ //
9
+ // Instruments the `dataloader` package — the standard batching/caching
10
+ // utility for solving N+1 queries in GraphQL resolvers, REST APIs,
11
+ // and any data-fetching layer.
12
+ //
13
+ // Patches DataLoader.prototype methods:
14
+ // - load(key) — single key lookup (batched)
15
+ // - loadMany(keys) — multi-key lookup (batched)
16
+ // - prime(key, val) — cache priming
17
+ // - clear(key) — single cache eviction
18
+ // - clearAll() — full cache clear
19
+ //
20
+ // The key insight: load() calls are batched by DataLoader and dispatched
21
+ // together via the batch function. We instrument both the individual
22
+ // load() calls AND the batch dispatch to show:
23
+ // 1. Per-key latency (includes batching wait time)
24
+ // 2. Batch execution performance
25
+ //
26
+ // Captured attributes:
27
+ // - dataloader.operation: load, loadMany, prime, clear, clearAll, batch
28
+ // - dataloader.key_count: number of keys in the batch
29
+ // - dataloader.name: DataLoader instance name (if available)
30
+ // - dataloader.cache_hit: whether result came from cache
31
+ // ---------------------------------------------------------------------------
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // DataLoader prototype patching
35
+ // ---------------------------------------------------------------------------
36
+
37
+ const patchDataLoader = (DataLoader: any, options?: SenzorOptions) => {
38
+ const proto = DataLoader?.prototype;
39
+ if (!proto) return;
40
+
41
+ // --- load(key) → Promise<value> ---
42
+ patchMethod(
43
+ proto,
44
+ 'load',
45
+ 'senzor.dataloader.load',
46
+ (original) =>
47
+ function patchedLoad(this: any, key: any) {
48
+ const loaderName = this.name || this._name || 'DataLoader';
49
+
50
+ const span = startCapturedSpan(
51
+ `${loaderName} load`,
52
+ 'function',
53
+ {
54
+ 'dataloader.operation': 'load',
55
+ 'dataloader.name': loaderName,
56
+ library: 'dataloader',
57
+ },
58
+ options
59
+ );
60
+
61
+ if (!span) return original.call(this, key);
62
+
63
+ return runWithCapturedSpan(span, () => {
64
+ try {
65
+ const result = original.call(this, key);
66
+
67
+ if (result && typeof result.then === 'function') {
68
+ return result.then(
69
+ (value: any) => {
70
+ span.end(0);
71
+ return value;
72
+ },
73
+ (error: any) => {
74
+ span.end(500, {
75
+ 'error.message': error?.message,
76
+ 'error.type': error?.name || 'Error',
77
+ });
78
+ throw error;
79
+ }
80
+ );
81
+ }
82
+
83
+ span.end(0);
84
+ return result;
85
+ } catch (error: any) {
86
+ span.end(500, { 'error.message': error?.message });
87
+ throw error;
88
+ }
89
+ });
90
+ }
91
+ );
92
+
93
+ // --- loadMany(keys) → Promise<value[]> ---
94
+ patchMethod(
95
+ proto,
96
+ 'loadMany',
97
+ 'senzor.dataloader.loadMany',
98
+ (original) =>
99
+ function patchedLoadMany(this: any, keys: any[]) {
100
+ const loaderName = this.name || this._name || 'DataLoader';
101
+ const keyCount = Array.isArray(keys) ? keys.length : 0;
102
+
103
+ const span = startCapturedSpan(
104
+ `${loaderName} loadMany`,
105
+ 'function',
106
+ {
107
+ 'dataloader.operation': 'loadMany',
108
+ 'dataloader.name': loaderName,
109
+ 'dataloader.key_count': keyCount,
110
+ library: 'dataloader',
111
+ },
112
+ options
113
+ );
114
+
115
+ if (!span) return original.call(this, keys);
116
+
117
+ return runWithCapturedSpan(span, () => {
118
+ try {
119
+ const result = original.call(this, keys);
120
+
121
+ if (result && typeof result.then === 'function') {
122
+ return result.then(
123
+ (values: any) => {
124
+ // Count errors in results
125
+ const errorCount = Array.isArray(values)
126
+ ? values.filter((v: any) => v instanceof Error).length
127
+ : 0;
128
+
129
+ span.end(errorCount > 0 ? 207 : 0, {
130
+ 'dataloader.key_count': keyCount,
131
+ 'dataloader.error_count': errorCount,
132
+ });
133
+ return values;
134
+ },
135
+ (error: any) => {
136
+ span.end(500, { 'error.message': error?.message });
137
+ throw error;
138
+ }
139
+ );
140
+ }
141
+
142
+ span.end(0);
143
+ return result;
144
+ } catch (error: any) {
145
+ span.end(500, { 'error.message': error?.message });
146
+ throw error;
147
+ }
148
+ });
149
+ }
150
+ );
151
+
152
+ // --- Wrap the batch function dispatch ---
153
+ // DataLoader calls the batch function internally. We intercept it
154
+ // by wrapping the constructor or the internal _batchLoadFn.
155
+ // Since we can't easily wrap the constructor via patchMethod,
156
+ // we patch _batchScheduleFn or override the dispatch mechanism.
157
+
158
+ // Patch the internal _batch method if it exists
159
+ const batchMethodName = '_dispatchBatch' in proto
160
+ ? '_dispatchBatch'
161
+ : '_dispatch' in proto
162
+ ? '_dispatch'
163
+ : null;
164
+
165
+ if (batchMethodName && typeof proto[batchMethodName] === 'function') {
166
+ patchMethod(
167
+ proto,
168
+ batchMethodName,
169
+ `senzor.dataloader.${batchMethodName}`,
170
+ (original) =>
171
+ function patchedDispatch(this: any, ...args: any[]) {
172
+ const loaderName = this.name || this._name || 'DataLoader';
173
+ // The batch queue holds pending keys
174
+ const batchSize = this._queue?.length || this._batch?.length || 0;
175
+
176
+ const span = startCapturedSpan(
177
+ `${loaderName} batch dispatch`,
178
+ 'function',
179
+ {
180
+ 'dataloader.operation': 'batch',
181
+ 'dataloader.name': loaderName,
182
+ 'dataloader.batch_size': batchSize,
183
+ library: 'dataloader',
184
+ },
185
+ options
186
+ );
187
+
188
+ if (!span) return original.apply(this, args);
189
+
190
+ return runWithCapturedSpan(span, () => {
191
+ try {
192
+ const result = original.apply(this, args);
193
+ // Batch dispatch is sync; the batch function's promise
194
+ // is resolved internally. End span after dispatch.
195
+ span.end(0, { 'dataloader.batch_size': batchSize });
196
+ return result;
197
+ } catch (error: any) {
198
+ span.end(500, { 'error.message': error?.message });
199
+ throw error;
200
+ }
201
+ });
202
+ }
203
+ );
204
+ }
205
+
206
+ // --- prime(key, value) ---
207
+ if (typeof proto.prime === 'function') {
208
+ patchMethod(
209
+ proto,
210
+ 'prime',
211
+ 'senzor.dataloader.prime',
212
+ (original) =>
213
+ function patchedPrime(this: any, key: any, value: any) {
214
+ const loaderName = this.name || this._name || 'DataLoader';
215
+
216
+ const span = startCapturedSpan(
217
+ `${loaderName} prime`,
218
+ 'function',
219
+ {
220
+ 'dataloader.operation': 'prime',
221
+ 'dataloader.name': loaderName,
222
+ library: 'dataloader',
223
+ },
224
+ options
225
+ );
226
+
227
+ if (!span) return original.call(this, key, value);
228
+
229
+ try {
230
+ const result = original.call(this, key, value);
231
+ span.end(0);
232
+ return result;
233
+ } catch (error: any) {
234
+ span.end(500, { 'error.message': error?.message });
235
+ throw error;
236
+ }
237
+ }
238
+ );
239
+ }
240
+
241
+ // --- clearAll() ---
242
+ if (typeof proto.clearAll === 'function') {
243
+ patchMethod(
244
+ proto,
245
+ 'clearAll',
246
+ 'senzor.dataloader.clearAll',
247
+ (original) =>
248
+ function patchedClearAll(this: any) {
249
+ const loaderName = this.name || this._name || 'DataLoader';
250
+
251
+ const span = startCapturedSpan(
252
+ `${loaderName} clearAll`,
253
+ 'function',
254
+ {
255
+ 'dataloader.operation': 'clearAll',
256
+ 'dataloader.name': loaderName,
257
+ library: 'dataloader',
258
+ },
259
+ options
260
+ );
261
+
262
+ if (!span) return original.call(this);
263
+
264
+ try {
265
+ const result = original.call(this);
266
+ span.end(0);
267
+ return result;
268
+ } catch (error: any) {
269
+ span.end(500, { 'error.message': error?.message });
270
+ throw error;
271
+ }
272
+ }
273
+ );
274
+ }
275
+ };
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Public API
279
+ // ---------------------------------------------------------------------------
280
+
281
+ export const instrumentDataloader = (options?: SenzorOptions) => {
282
+ hookRequire('dataloader', (exports: any) => {
283
+ // dataloader exports the constructor directly
284
+ patchDataLoader(exports, options);
285
+
286
+ // Handle default export (ESM interop)
287
+ if (exports?.default?.prototype) {
288
+ patchDataLoader(exports.default, options);
289
+ }
290
+ });
291
+ };
@@ -0,0 +1,220 @@
1
+ import { SenzorOptions } from '../core/types';
2
+ import { patchMethod } from './patch';
3
+ import { runWithCapturedSpan, startCapturedSpan } from './span';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // DNS Instrumentation
7
+ //
8
+ // Instruments Node.js core `dns` module:
9
+ // - dns.lookup() — resolves a hostname to an address (uses OS resolver)
10
+ // - dns.resolve() — resolves using DNS protocol (network call)
11
+ // - dns.resolve4() — A records
12
+ // - dns.resolve6() — AAAA records
13
+ // - dns.resolveMx() — MX records
14
+ // - dns.resolveTxt() — TXT records
15
+ // - dns.resolveSrv() — SRV records
16
+ // - dns.resolveCname() — CNAME records
17
+ // - dns.resolveNs() — NS records
18
+ // - dns.reverse() — reverse DNS lookup
19
+ //
20
+ // Follows OTel semantic conventions: dns.hostname, network.peer.address
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Patch a single DNS method that accepts (hostname, [options], callback).
25
+ */
26
+ const patchDnsMethod = (
27
+ dnsModule: any,
28
+ methodName: string,
29
+ options?: SenzorOptions
30
+ ) => {
31
+ patchMethod(
32
+ dnsModule,
33
+ methodName,
34
+ `senzor.dns.${methodName}`,
35
+ (original) =>
36
+ function patchedDnsMethod(this: any, ...args: any[]) {
37
+ // Extract hostname (always the first argument)
38
+ const hostname = typeof args[0] === 'string' ? args[0] : 'unknown';
39
+
40
+ const span = startCapturedSpan(
41
+ `DNS ${methodName}`,
42
+ 'custom',
43
+ {
44
+ 'dns.hostname': hostname,
45
+ 'dns.operation': methodName,
46
+ },
47
+ options
48
+ );
49
+
50
+ if (!span) return original.apply(this, args);
51
+
52
+ // Find and wrap the callback (always the last argument)
53
+ const callbackIdx = args.findIndex(
54
+ (arg: any, idx: number) => typeof arg === 'function' && idx === args.length - 1
55
+ );
56
+
57
+ if (callbackIdx >= 0) {
58
+ const originalCallback = args[callbackIdx];
59
+ args[callbackIdx] = function wrappedDnsCallback(
60
+ err: NodeJS.ErrnoException | null,
61
+ ...results: any[]
62
+ ) {
63
+ if (err) {
64
+ span.end(500, {
65
+ 'error.message': err.message,
66
+ 'error.type': err.code || err.name || 'DnsError',
67
+ 'dns.error_code': err.code,
68
+ });
69
+ } else {
70
+ // For lookup, first result is address, second is family
71
+ const meta: Record<string, any> = {};
72
+ if (methodName === 'lookup' && results[0]) {
73
+ meta['network.peer.address'] = results[0];
74
+ meta['dns.address_family'] = results[1] === 6 ? 'IPv6' : 'IPv4';
75
+ } else if (methodName === 'resolve4' || methodName === 'resolve6') {
76
+ meta['dns.result_count'] = Array.isArray(results[0]) ? results[0].length : 0;
77
+ } else if (methodName === 'resolve') {
78
+ meta['dns.result_count'] = Array.isArray(results[0]) ? results[0].length : 0;
79
+ }
80
+ span.end(0, meta);
81
+ }
82
+ return originalCallback.call(this, err, ...results);
83
+ };
84
+ }
85
+
86
+ return runWithCapturedSpan(span, () => {
87
+ try {
88
+ return original.apply(this, args);
89
+ } catch (error: any) {
90
+ span.end(500, {
91
+ 'error.message': error?.message,
92
+ 'error.type': error?.name || 'Error',
93
+ });
94
+ throw error;
95
+ }
96
+ });
97
+ }
98
+ );
99
+ };
100
+
101
+ /**
102
+ * Patch dns.promises methods (returns promises instead of using callbacks).
103
+ */
104
+ const patchDnsPromisesMethod = (
105
+ promises: any,
106
+ methodName: string,
107
+ options?: SenzorOptions
108
+ ) => {
109
+ patchMethod(
110
+ promises,
111
+ methodName,
112
+ `senzor.dns.promises.${methodName}`,
113
+ (original) =>
114
+ function patchedDnsPromiseMethod(this: any, ...args: any[]) {
115
+ const hostname = typeof args[0] === 'string' ? args[0] : 'unknown';
116
+
117
+ const span = startCapturedSpan(
118
+ `DNS ${methodName}`,
119
+ 'custom',
120
+ {
121
+ 'dns.hostname': hostname,
122
+ 'dns.operation': methodName,
123
+ },
124
+ options
125
+ );
126
+
127
+ if (!span) return original.apply(this, args);
128
+
129
+ return runWithCapturedSpan(span, async () => {
130
+ try {
131
+ const result = await original.apply(this, args);
132
+
133
+ const meta: Record<string, any> = {};
134
+ if (methodName === 'lookup' && result) {
135
+ meta['network.peer.address'] = result.address;
136
+ meta['dns.address_family'] = result.family === 6 ? 'IPv6' : 'IPv4';
137
+ } else if (Array.isArray(result)) {
138
+ meta['dns.result_count'] = result.length;
139
+ }
140
+
141
+ span.end(0, meta);
142
+ return result;
143
+ } catch (error: any) {
144
+ span.end(500, {
145
+ 'error.message': error?.message,
146
+ 'error.type': error?.code || error?.name || 'DnsError',
147
+ 'dns.error_code': error?.code,
148
+ });
149
+ throw error;
150
+ }
151
+ });
152
+ }
153
+ );
154
+ };
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Public API
158
+ // ---------------------------------------------------------------------------
159
+
160
+ export const instrumentDns = (options?: SenzorOptions) => {
161
+ let dns: any;
162
+ try {
163
+ dns = require('dns');
164
+ } catch {
165
+ return;
166
+ }
167
+
168
+ if (!dns) return;
169
+
170
+ // Callback-based methods
171
+ const callbackMethods = [
172
+ 'lookup',
173
+ 'resolve',
174
+ 'resolve4',
175
+ 'resolve6',
176
+ 'resolveMx',
177
+ 'resolveTxt',
178
+ 'resolveSrv',
179
+ 'resolveCname',
180
+ 'resolveNs',
181
+ 'resolvePtr',
182
+ 'resolveSoa',
183
+ 'resolveNaptr',
184
+ 'resolveCaa',
185
+ 'reverse',
186
+ ];
187
+
188
+ for (const method of callbackMethods) {
189
+ if (typeof dns[method] === 'function') {
190
+ patchDnsMethod(dns, method, options);
191
+ }
192
+ }
193
+
194
+ // Promise-based methods (dns.promises / dns/promises)
195
+ const promises = dns.promises;
196
+ if (promises) {
197
+ const promiseMethods = [
198
+ 'lookup',
199
+ 'resolve',
200
+ 'resolve4',
201
+ 'resolve6',
202
+ 'resolveMx',
203
+ 'resolveTxt',
204
+ 'resolveSrv',
205
+ 'resolveCname',
206
+ 'resolveNs',
207
+ 'resolvePtr',
208
+ 'resolveSoa',
209
+ 'resolveNaptr',
210
+ 'resolveCaa',
211
+ 'reverse',
212
+ ];
213
+
214
+ for (const method of promiseMethods) {
215
+ if (typeof promises[method] === 'function') {
216
+ patchDnsPromisesMethod(promises, method, options);
217
+ }
218
+ }
219
+ }
220
+ };