@senzops/apm-node 1.2.7 → 1.3.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.
- package/CHANGELOG.md +9 -0
- package/README.md +479 -398
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/register.js +1 -1
- package/dist/register.js.map +1 -1
- package/dist/register.mjs +1 -1
- package/dist/register.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/client.ts +57 -0
- package/src/core/context.ts +71 -9
- package/src/core/transport.ts +20 -3
- package/src/core/types.ts +5 -1
- package/src/index.ts +4 -0
- package/src/instrumentation/amqplib.ts +371 -0
- package/src/instrumentation/anthropic.ts +245 -0
- package/src/instrumentation/aws-sdk.ts +403 -0
- package/src/instrumentation/azure-openai.ts +177 -0
- package/src/instrumentation/bunyan.ts +93 -0
- package/src/instrumentation/cassandra.ts +367 -0
- package/src/instrumentation/cohere.ts +227 -0
- package/src/instrumentation/connect.ts +200 -0
- package/src/instrumentation/dataloader.ts +291 -0
- package/src/instrumentation/dns.ts +220 -0
- package/src/instrumentation/firebase.ts +445 -0
- package/src/instrumentation/fs.ts +260 -0
- package/src/instrumentation/generic-pool.ts +317 -0
- package/src/instrumentation/google-genai.ts +426 -0
- package/src/instrumentation/graphql.ts +434 -0
- package/src/instrumentation/grpc.ts +666 -0
- package/src/instrumentation/hapi.ts +257 -0
- package/src/instrumentation/kafka.ts +360 -0
- package/src/instrumentation/knex.ts +249 -0
- package/src/instrumentation/lru-memoizer.ts +175 -0
- package/src/instrumentation/memcached.ts +190 -0
- package/src/instrumentation/mistral.ts +254 -0
- package/src/instrumentation/nestjs.ts +243 -0
- package/src/instrumentation/net.ts +171 -0
- package/src/instrumentation/openai.ts +281 -0
- package/src/instrumentation/pino.ts +170 -0
- package/src/instrumentation/restify.ts +213 -0
- package/src/instrumentation/runtime.ts +352 -0
- package/src/instrumentation/socketio.ts +272 -0
- package/src/instrumentation/tedious.ts +509 -0
- package/src/instrumentation/winston.ts +149 -0
- package/src/register.ts +22 -3
- package/src/wrappers/lambda.ts +417 -0
- package/tsup.config.ts +3 -3
- package/wiki.md +1547 -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
|
+
};
|