@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,445 @@
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
+ // Firebase Admin SDK Instrumentation
8
+ //
9
+ // Instruments `firebase-admin` services:
10
+ // 1. Firestore — document/collection CRUD and queries
11
+ // 2. Auth — user management and token verification
12
+ // 3. Storage — file upload/download operations
13
+ // 4. Messaging (FCM) — push notification delivery
14
+ //
15
+ // Also instruments `firebase-functions` for Cloud Functions triggers.
16
+ //
17
+ // Strategy: Hook each firebase-admin sub-module separately since they're
18
+ // lazy-loaded. Firestore is the most critical — we patch DocumentReference,
19
+ // CollectionReference, Query, and Transaction prototypes.
20
+ //
21
+ // Captured attributes:
22
+ // - db.system.name: 'firestore' (for Firestore ops)
23
+ // - db.operation.name: GET, SET, ADD, UPDATE, DELETE, QUERY
24
+ // - db.collection.name: collection path
25
+ // - firebase.service: 'firestore' | 'auth' | 'storage' | 'messaging'
26
+ // - firebase.operation: specific operation name
27
+ // ---------------------------------------------------------------------------
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Firestore patching
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /** Wrap an async Firestore method that returns a promise. */
34
+ const wrapFirestoreMethod = (
35
+ proto: any,
36
+ methodName: string,
37
+ getSpanInfo: (instance: any, args: any[]) => { name: string; meta: Record<string, any> },
38
+ patchKey: string,
39
+ options?: SenzorOptions
40
+ ) => {
41
+ if (!proto || typeof proto[methodName] !== 'function') return;
42
+
43
+ patchMethod(
44
+ proto,
45
+ methodName,
46
+ patchKey,
47
+ (original) =>
48
+ function patchedFirestoreMethod(this: any, ...args: any[]) {
49
+ const { name, meta } = getSpanInfo(this, args);
50
+
51
+ const span = startCapturedSpan(name, 'db', meta, options);
52
+
53
+ if (!span) return original.apply(this, args);
54
+
55
+ return runWithCapturedSpan(span, () => {
56
+ try {
57
+ const result = original.apply(this, args);
58
+
59
+ if (result && typeof result.then === 'function') {
60
+ return result.then(
61
+ (value: any) => {
62
+ const endMeta: Record<string, any> = {};
63
+ // DocumentSnapshot
64
+ if (value?.exists !== undefined) {
65
+ endMeta['firestore.exists'] = value.exists;
66
+ }
67
+ // QuerySnapshot
68
+ if (value?.size !== undefined) {
69
+ endMeta['db.response.row_count'] = value.size;
70
+ }
71
+ span.end(0, endMeta);
72
+ return value;
73
+ },
74
+ (error: any) => {
75
+ span.end(error?.code || 500, {
76
+ 'error.message': error?.message,
77
+ 'error.type': error?.name || 'FirestoreError',
78
+ 'db.error.code': error?.code,
79
+ });
80
+ throw error;
81
+ }
82
+ );
83
+ }
84
+
85
+ // WriteResult (for set, update, delete — they return a WriteResult promise)
86
+ span.end(0);
87
+ return result;
88
+ } catch (error: any) {
89
+ span.end(500, { 'error.message': error?.message });
90
+ throw error;
91
+ }
92
+ });
93
+ }
94
+ );
95
+ };
96
+
97
+ /** Get the collection path from a DocumentReference or CollectionReference. */
98
+ const getPath = (ref: any): string => {
99
+ try {
100
+ return ref?.path || ref?._path?.toString() || 'unknown';
101
+ } catch {
102
+ return 'unknown';
103
+ }
104
+ };
105
+
106
+ const getCollectionName = (ref: any): string => {
107
+ try {
108
+ // For DocumentReference, parent is the collection
109
+ if (ref?.parent?.id) return ref.parent.id;
110
+ // For CollectionReference, id is the collection name
111
+ if (ref?.id) return ref.id;
112
+ // For Query, try _query or _path
113
+ if (ref?._query?._path) return ref._query._path.toString().split('/').pop() || 'unknown';
114
+ return getPath(ref).split('/').filter(Boolean).slice(-2, -1)[0] || 'unknown';
115
+ } catch {
116
+ return 'unknown';
117
+ }
118
+ };
119
+
120
+ const patchFirestore = (firestoreModule: any, options?: SenzorOptions) => {
121
+ // --- DocumentReference ---
122
+ const DocRef = firestoreModule?.DocumentReference;
123
+ if (DocRef?.prototype) {
124
+ const docProto = DocRef.prototype;
125
+
126
+ // get()
127
+ wrapFirestoreMethod(docProto, 'get', (instance) => ({
128
+ name: `Firestore GET ${getPath(instance)}`,
129
+ meta: {
130
+ 'db.system.name': 'firestore',
131
+ 'db.operation.name': 'GET',
132
+ 'db.collection.name': getCollectionName(instance),
133
+ 'firestore.path': getPath(instance),
134
+ 'firebase.service': 'firestore',
135
+ library: 'firebase-admin',
136
+ },
137
+ }), 'senzor.firebase.docRef.get', options);
138
+
139
+ // set()
140
+ wrapFirestoreMethod(docProto, 'set', (instance) => ({
141
+ name: `Firestore SET ${getPath(instance)}`,
142
+ meta: {
143
+ 'db.system.name': 'firestore',
144
+ 'db.operation.name': 'SET',
145
+ 'db.collection.name': getCollectionName(instance),
146
+ 'firestore.path': getPath(instance),
147
+ 'firebase.service': 'firestore',
148
+ library: 'firebase-admin',
149
+ },
150
+ }), 'senzor.firebase.docRef.set', options);
151
+
152
+ // update()
153
+ wrapFirestoreMethod(docProto, 'update', (instance) => ({
154
+ name: `Firestore UPDATE ${getPath(instance)}`,
155
+ meta: {
156
+ 'db.system.name': 'firestore',
157
+ 'db.operation.name': 'UPDATE',
158
+ 'db.collection.name': getCollectionName(instance),
159
+ 'firestore.path': getPath(instance),
160
+ 'firebase.service': 'firestore',
161
+ library: 'firebase-admin',
162
+ },
163
+ }), 'senzor.firebase.docRef.update', options);
164
+
165
+ // delete()
166
+ wrapFirestoreMethod(docProto, 'delete', (instance) => ({
167
+ name: `Firestore DELETE ${getPath(instance)}`,
168
+ meta: {
169
+ 'db.system.name': 'firestore',
170
+ 'db.operation.name': 'DELETE',
171
+ 'db.collection.name': getCollectionName(instance),
172
+ 'firestore.path': getPath(instance),
173
+ 'firebase.service': 'firestore',
174
+ library: 'firebase-admin',
175
+ },
176
+ }), 'senzor.firebase.docRef.delete', options);
177
+
178
+ // create()
179
+ if (typeof docProto.create === 'function') {
180
+ wrapFirestoreMethod(docProto, 'create', (instance) => ({
181
+ name: `Firestore CREATE ${getPath(instance)}`,
182
+ meta: {
183
+ 'db.system.name': 'firestore',
184
+ 'db.operation.name': 'CREATE',
185
+ 'db.collection.name': getCollectionName(instance),
186
+ 'firebase.service': 'firestore',
187
+ library: 'firebase-admin',
188
+ },
189
+ }), 'senzor.firebase.docRef.create', options);
190
+ }
191
+ }
192
+
193
+ // --- CollectionReference ---
194
+ const ColRef = firestoreModule?.CollectionReference;
195
+ if (ColRef?.prototype) {
196
+ // add()
197
+ wrapFirestoreMethod(ColRef.prototype, 'add', (instance) => ({
198
+ name: `Firestore ADD ${getPath(instance)}`,
199
+ meta: {
200
+ 'db.system.name': 'firestore',
201
+ 'db.operation.name': 'ADD',
202
+ 'db.collection.name': getCollectionName(instance),
203
+ 'firebase.service': 'firestore',
204
+ library: 'firebase-admin',
205
+ },
206
+ }), 'senzor.firebase.colRef.add', options);
207
+
208
+ // listDocuments()
209
+ if (typeof ColRef.prototype.listDocuments === 'function') {
210
+ wrapFirestoreMethod(ColRef.prototype, 'listDocuments', (instance) => ({
211
+ name: `Firestore LIST ${getPath(instance)}`,
212
+ meta: {
213
+ 'db.system.name': 'firestore',
214
+ 'db.operation.name': 'LIST',
215
+ 'db.collection.name': getCollectionName(instance),
216
+ 'firebase.service': 'firestore',
217
+ library: 'firebase-admin',
218
+ },
219
+ }), 'senzor.firebase.colRef.listDocuments', options);
220
+ }
221
+ }
222
+
223
+ // --- Query ---
224
+ const Query = firestoreModule?.Query;
225
+ if (Query?.prototype) {
226
+ wrapFirestoreMethod(Query.prototype, 'get', (instance) => ({
227
+ name: `Firestore QUERY ${getCollectionName(instance)}`,
228
+ meta: {
229
+ 'db.system.name': 'firestore',
230
+ 'db.operation.name': 'QUERY',
231
+ 'db.collection.name': getCollectionName(instance),
232
+ 'firebase.service': 'firestore',
233
+ library: 'firebase-admin',
234
+ },
235
+ }), 'senzor.firebase.query.get', options);
236
+ }
237
+
238
+ // --- Transaction ---
239
+ const Transaction = firestoreModule?.Transaction;
240
+ if (Transaction?.prototype) {
241
+ for (const method of ['get', 'set', 'update', 'delete', 'create'] as const) {
242
+ if (typeof Transaction.prototype[method] !== 'function') continue;
243
+ // Transaction methods are sync (they queue operations) — except get()
244
+ if (method === 'get') {
245
+ wrapFirestoreMethod(Transaction.prototype, 'get', (_instance, args) => ({
246
+ name: `Firestore TX GET ${getPath(args[0])}`,
247
+ meta: {
248
+ 'db.system.name': 'firestore',
249
+ 'db.operation.name': 'TX_GET',
250
+ 'firebase.service': 'firestore',
251
+ library: 'firebase-admin',
252
+ },
253
+ }), 'senzor.firebase.transaction.get', options);
254
+ }
255
+ }
256
+ }
257
+
258
+ // --- WriteBatch ---
259
+ const WriteBatch = firestoreModule?.WriteBatch;
260
+ if (WriteBatch?.prototype && typeof WriteBatch.prototype.commit === 'function') {
261
+ wrapFirestoreMethod(WriteBatch.prototype, 'commit', () => ({
262
+ name: 'Firestore BATCH COMMIT',
263
+ meta: {
264
+ 'db.system.name': 'firestore',
265
+ 'db.operation.name': 'BATCH_COMMIT',
266
+ 'firebase.service': 'firestore',
267
+ library: 'firebase-admin',
268
+ },
269
+ }), 'senzor.firebase.writeBatch.commit', options);
270
+ }
271
+ };
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Auth patching
275
+ // ---------------------------------------------------------------------------
276
+
277
+ const AUTH_METHODS = [
278
+ 'createUser', 'getUser', 'getUserByEmail', 'getUserByPhoneNumber',
279
+ 'listUsers', 'deleteUser', 'deleteUsers', 'updateUser',
280
+ 'verifyIdToken', 'verifySessionCookie', 'createSessionCookie',
281
+ 'revokeRefreshTokens', 'setCustomUserClaims', 'generateEmailVerificationLink',
282
+ 'generatePasswordResetLink', 'generateSignInWithEmailLink',
283
+ ] as const;
284
+
285
+ const patchFirebaseAuth = (authModule: any, options?: SenzorOptions) => {
286
+ // Auth is typically at auth().* or Auth.prototype
287
+ const Auth = authModule?.Auth;
288
+ if (!Auth?.prototype) return;
289
+
290
+ for (const method of AUTH_METHODS) {
291
+ if (typeof Auth.prototype[method] !== 'function') continue;
292
+
293
+ patchMethod(
294
+ Auth.prototype,
295
+ method,
296
+ `senzor.firebase.auth.${method}`,
297
+ (original) =>
298
+ function patchedAuthMethod(this: any, ...args: any[]) {
299
+ const span = startCapturedSpan(
300
+ `Firebase Auth ${method}`,
301
+ 'function',
302
+ {
303
+ 'firebase.service': 'auth',
304
+ 'firebase.operation': method,
305
+ library: 'firebase-admin',
306
+ },
307
+ options
308
+ );
309
+
310
+ if (!span) return original.apply(this, args);
311
+
312
+ return runWithCapturedSpan(span, () => {
313
+ try {
314
+ const result = original.apply(this, args);
315
+ if (result && typeof result.then === 'function') {
316
+ return result.then(
317
+ (value: any) => { span.end(0); return value; },
318
+ (error: any) => {
319
+ span.end(500, {
320
+ 'error.message': error?.message,
321
+ 'error.type': error?.code || error?.name || 'AuthError',
322
+ });
323
+ throw error;
324
+ }
325
+ );
326
+ }
327
+ span.end(0);
328
+ return result;
329
+ } catch (error: any) {
330
+ span.end(500, { 'error.message': error?.message });
331
+ throw error;
332
+ }
333
+ });
334
+ }
335
+ );
336
+ }
337
+ };
338
+
339
+ // ---------------------------------------------------------------------------
340
+ // Messaging (FCM) patching
341
+ // ---------------------------------------------------------------------------
342
+
343
+ const MESSAGING_METHODS = [
344
+ 'send', 'sendEach', 'sendEachForMulticast', 'sendMulticast',
345
+ 'sendToDevice', 'sendToTopic', 'sendToCondition',
346
+ 'subscribeToTopic', 'unsubscribeFromTopic',
347
+ ] as const;
348
+
349
+ const patchFirebaseMessaging = (messagingModule: any, options?: SenzorOptions) => {
350
+ const Messaging = messagingModule?.Messaging;
351
+ if (!Messaging?.prototype) return;
352
+
353
+ for (const method of MESSAGING_METHODS) {
354
+ if (typeof Messaging.prototype[method] !== 'function') continue;
355
+
356
+ patchMethod(
357
+ Messaging.prototype,
358
+ method,
359
+ `senzor.firebase.messaging.${method}`,
360
+ (original) =>
361
+ function patchedMessagingMethod(this: any, ...args: any[]) {
362
+ const span = startCapturedSpan(
363
+ `Firebase FCM ${method}`,
364
+ 'messaging',
365
+ {
366
+ 'firebase.service': 'messaging',
367
+ 'firebase.operation': method,
368
+ 'messaging.system': 'fcm',
369
+ library: 'firebase-admin',
370
+ },
371
+ options
372
+ );
373
+
374
+ if (!span) return original.apply(this, args);
375
+
376
+ return runWithCapturedSpan(span, () => {
377
+ try {
378
+ const result = original.apply(this, args);
379
+ if (result && typeof result.then === 'function') {
380
+ return result.then(
381
+ (value: any) => {
382
+ const endMeta: Record<string, any> = {};
383
+ // sendEach/sendMulticast returns BatchResponse
384
+ if (value?.successCount !== undefined) {
385
+ endMeta['firebase.fcm.success_count'] = value.successCount;
386
+ endMeta['firebase.fcm.failure_count'] = value.failureCount;
387
+ }
388
+ span.end(0, endMeta);
389
+ return value;
390
+ },
391
+ (error: any) => {
392
+ span.end(500, { 'error.message': error?.message });
393
+ throw error;
394
+ }
395
+ );
396
+ }
397
+ span.end(0);
398
+ return result;
399
+ } catch (error: any) {
400
+ span.end(500, { 'error.message': error?.message });
401
+ throw error;
402
+ }
403
+ });
404
+ }
405
+ );
406
+ }
407
+ };
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // Public API
411
+ // ---------------------------------------------------------------------------
412
+
413
+ export const instrumentFirebase = (options?: SenzorOptions) => {
414
+ // Firestore
415
+ hookRequire('firebase-admin/firestore', (exports: any) => {
416
+ patchFirestore(exports, options);
417
+ });
418
+ hookRequire('@google-cloud/firestore', (exports: any) => {
419
+ patchFirestore(exports, options);
420
+ });
421
+
422
+ // Auth
423
+ hookRequire('firebase-admin/auth', (exports: any) => {
424
+ patchFirebaseAuth(exports, options);
425
+ });
426
+
427
+ // Messaging
428
+ hookRequire('firebase-admin/messaging', (exports: any) => {
429
+ patchFirebaseMessaging(exports, options);
430
+ });
431
+
432
+ // firebase-admin main module — try to access services from it
433
+ hookRequire('firebase-admin', (exports: any) => {
434
+ // In newer firebase-admin, services are at exports.firestore, exports.auth, etc.
435
+ // They're getter-based, so accessing them triggers sub-module loading
436
+ // which our hooks above will catch.
437
+ // Just ensure the main module export is patched if it exposes prototypes
438
+ try {
439
+ const firestore = exports?.firestore;
440
+ if (firestore) {
441
+ // This forces the lazy-load, which triggers our hookRequire above
442
+ }
443
+ } catch { }
444
+ });
445
+ };
@@ -0,0 +1,260 @@
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
+ // File System (fs) Instrumentation
8
+ //
9
+ // Instruments Node.js core `fs` module to capture file I/O operations.
10
+ // File system calls are a common source of latency in Node.js applications
11
+ // (template rendering, config loading, log writing, file uploads, etc.).
12
+ //
13
+ // Only instruments ASYNC methods (callback + promises) — never sync methods,
14
+ // as those block the event loop and adding span overhead would be wasteful.
15
+ //
16
+ // Patches:
17
+ // - Callback-based: fs.readFile, fs.writeFile, fs.stat, fs.access,
18
+ // fs.readdir, fs.mkdir, fs.rmdir, fs.unlink, fs.rename, fs.copyFile,
19
+ // fs.appendFile, fs.chmod, fs.chown, fs.link, fs.symlink, fs.realpath,
20
+ // fs.mkdtemp, fs.open, fs.close
21
+ // - Promise-based: fs.promises.* (same set)
22
+ //
23
+ // Captured attributes:
24
+ // - fs.operation: readFile, writeFile, stat, etc.
25
+ // - fs.path: file path (sanitized — no secrets in paths)
26
+ // - fs.flags: open flags (r, w, a, etc.)
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /** Operations to instrument. Grouped by argument patterns. */
30
+ const PATH_OPERATIONS = [
31
+ 'readFile', 'writeFile', 'appendFile', 'stat', 'lstat',
32
+ 'access', 'readdir', 'mkdir', 'rmdir', 'unlink',
33
+ 'chmod', 'chown', 'realpath', 'mkdtemp', 'truncate',
34
+ 'readlink', 'exists',
35
+ ] as const;
36
+
37
+ const TWO_PATH_OPERATIONS = [
38
+ 'rename', 'copyFile', 'link', 'symlink',
39
+ ] as const;
40
+
41
+ /** Sanitize a file path — strip home directory prefix for privacy. */
42
+ const sanitizePath = (filePath: any): string | undefined => {
43
+ if (typeof filePath !== 'string' && !(filePath instanceof Buffer) && !(filePath instanceof URL)) {
44
+ return undefined;
45
+ }
46
+ const pathStr = String(filePath);
47
+ // Truncate very long paths
48
+ if (pathStr.length > 200) return pathStr.slice(0, 200) + '...';
49
+ return pathStr;
50
+ };
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Callback-based fs method patching
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const patchFsCallbackMethod = (
57
+ fsModule: any,
58
+ methodName: string,
59
+ pathArgCount: number,
60
+ options?: SenzorOptions
61
+ ) => {
62
+ if (typeof fsModule[methodName] !== 'function') return;
63
+
64
+ patchMethod(
65
+ fsModule,
66
+ methodName,
67
+ `senzor.fs.${methodName}`,
68
+ (original) =>
69
+ function patchedFsMethod(this: any, ...args: any[]) {
70
+ const operation = methodName.toUpperCase();
71
+ const filePath = sanitizePath(args[0]);
72
+
73
+ const spanMeta: Record<string, any> = {
74
+ 'fs.operation': methodName,
75
+ 'fs.path': filePath,
76
+ library: 'fs',
77
+ };
78
+
79
+ if (pathArgCount === 2 && args[1]) {
80
+ spanMeta['fs.destination'] = sanitizePath(args[1]);
81
+ }
82
+
83
+ const span = startCapturedSpan(
84
+ `FS ${operation}`,
85
+ 'custom',
86
+ spanMeta,
87
+ options
88
+ );
89
+
90
+ if (!span) return original.apply(this, args);
91
+
92
+ // Find and wrap the callback (last function argument)
93
+ const callbackIndex = args.findIndex(
94
+ (arg, idx) => idx >= pathArgCount && typeof arg === 'function'
95
+ );
96
+
97
+ if (callbackIndex >= 0) {
98
+ const originalCb = args[callbackIndex];
99
+ args[callbackIndex] = function (err: any, ...results: any[]) {
100
+ if (err) {
101
+ span.end(500, {
102
+ 'error.message': err.message,
103
+ 'error.type': err.name || 'Error',
104
+ 'error.code': err.code,
105
+ });
106
+ } else {
107
+ span.end(0);
108
+ }
109
+ return originalCb.call(this, err, ...results);
110
+ };
111
+ }
112
+
113
+ return runWithCapturedSpan(span, () => {
114
+ try {
115
+ const result = original.apply(this, args);
116
+
117
+ // If no callback found, end span after sync return
118
+ if (callbackIndex < 0) {
119
+ span.end(0);
120
+ }
121
+
122
+ return result;
123
+ } catch (error: any) {
124
+ span.end(500, {
125
+ 'error.message': error?.message,
126
+ 'error.code': error?.code,
127
+ });
128
+ throw error;
129
+ }
130
+ });
131
+ }
132
+ );
133
+ };
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Promise-based fs.promises method patching
137
+ // ---------------------------------------------------------------------------
138
+
139
+ const patchFsPromiseMethod = (
140
+ fsPromises: any,
141
+ methodName: string,
142
+ pathArgCount: number,
143
+ options?: SenzorOptions
144
+ ) => {
145
+ if (typeof fsPromises[methodName] !== 'function') return;
146
+
147
+ patchMethod(
148
+ fsPromises,
149
+ methodName,
150
+ `senzor.fs.promises.${methodName}`,
151
+ (original) =>
152
+ function patchedFsPromiseMethod(this: any, ...args: any[]) {
153
+ const operation = methodName.toUpperCase();
154
+ const filePath = sanitizePath(args[0]);
155
+
156
+ const spanMeta: Record<string, any> = {
157
+ 'fs.operation': methodName,
158
+ 'fs.path': filePath,
159
+ 'fs.api': 'promises',
160
+ library: 'fs',
161
+ };
162
+
163
+ if (pathArgCount === 2 && args[1]) {
164
+ spanMeta['fs.destination'] = sanitizePath(args[1]);
165
+ }
166
+
167
+ const span = startCapturedSpan(
168
+ `FS ${operation}`,
169
+ 'custom',
170
+ spanMeta,
171
+ options
172
+ );
173
+
174
+ if (!span) return original.apply(this, args);
175
+
176
+ return runWithCapturedSpan(span, () => {
177
+ try {
178
+ const result = original.apply(this, args);
179
+
180
+ if (result && typeof result.then === 'function') {
181
+ return result.then(
182
+ (value: any) => { span.end(0); return value; },
183
+ (error: any) => {
184
+ span.end(500, {
185
+ 'error.message': error?.message,
186
+ 'error.code': error?.code,
187
+ });
188
+ throw error;
189
+ }
190
+ );
191
+ }
192
+
193
+ span.end(0);
194
+ return result;
195
+ } catch (error: any) {
196
+ span.end(500, { 'error.message': error?.message });
197
+ throw error;
198
+ }
199
+ });
200
+ }
201
+ );
202
+ };
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Core fs module patching
206
+ // ---------------------------------------------------------------------------
207
+
208
+ const patchFs = (fsModule: any, options?: SenzorOptions) => {
209
+ if (!fsModule) return;
210
+
211
+ // Patch callback-based methods (single path argument)
212
+ for (const method of PATH_OPERATIONS) {
213
+ patchFsCallbackMethod(fsModule, method, 1, options);
214
+ }
215
+
216
+ // Patch callback-based methods (two path arguments)
217
+ for (const method of TWO_PATH_OPERATIONS) {
218
+ patchFsCallbackMethod(fsModule, method, 2, options);
219
+ }
220
+
221
+ // Patch open/close separately (they have different signatures)
222
+ patchFsCallbackMethod(fsModule, 'open', 1, options);
223
+ patchFsCallbackMethod(fsModule, 'close', 1, options);
224
+
225
+ // Patch fs.promises
226
+ const promises = fsModule.promises;
227
+ if (promises) {
228
+ for (const method of PATH_OPERATIONS) {
229
+ if (method === 'exists') continue; // fs.promises.exists doesn't exist
230
+ patchFsPromiseMethod(promises, method, 1, options);
231
+ }
232
+
233
+ for (const method of TWO_PATH_OPERATIONS) {
234
+ patchFsPromiseMethod(promises, method, 2, options);
235
+ }
236
+
237
+ patchFsPromiseMethod(promises, 'open', 1, options);
238
+ }
239
+ };
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Public API
243
+ // ---------------------------------------------------------------------------
244
+
245
+ export const instrumentFs = (options?: SenzorOptions) => {
246
+ // fs is a Node.js built-in — require it directly
247
+ try {
248
+ const fs = require('fs');
249
+ patchFs(fs, options);
250
+ } catch { }
251
+
252
+ // Also hook for any dynamic requires
253
+ hookRequire('fs', (exports: any) => {
254
+ patchFs(exports, options);
255
+ });
256
+
257
+ hookRequire('node:fs', (exports: any) => {
258
+ patchFs(exports, options);
259
+ });
260
+ };