@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.
- package/CHANGELOG.md +13 -0
- package/README.md +527 -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/lambda-handler.d.mts +13 -0
- package/dist/lambda-handler.d.ts +13 -0
- package/dist/lambda-handler.js +2 -0
- package/dist/lambda-handler.js.map +1 -0
- package/dist/lambda-handler.mjs +2 -0
- package/dist/lambda-handler.mjs.map +1 -0
- 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 +6 -1
- package/src/core/client.ts +57 -0
- 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/lambda-handler.ts +262 -0
- package/src/register.ts +22 -3
- package/src/wrappers/lambda.ts +417 -0
- package/tsup.config.ts +4 -4
- package/wiki.md +1693 -852
|
@@ -0,0 +1,434 @@
|
|
|
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
|
+
// GraphQL Instrumentation
|
|
8
|
+
//
|
|
9
|
+
// Instruments the `graphql` package at the execution layer, covering:
|
|
10
|
+
// - graphql() — top-level convenience function
|
|
11
|
+
// - execute() — execution engine
|
|
12
|
+
// - parse() — query parsing
|
|
13
|
+
// - validate() — schema validation
|
|
14
|
+
//
|
|
15
|
+
// Follows OTel semantic conventions: graphql.operation.name,
|
|
16
|
+
// graphql.operation.type, graphql.document, graphql.source
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/** Maximum length of captured GraphQL document to avoid oversized spans. */
|
|
20
|
+
const MAX_DOCUMENT_LENGTH = 4096;
|
|
21
|
+
|
|
22
|
+
/** Safely truncate a GraphQL source document. */
|
|
23
|
+
const truncateDocument = (source: string): string => {
|
|
24
|
+
if (!source || typeof source !== 'string') return '';
|
|
25
|
+
if (source.length <= MAX_DOCUMENT_LENGTH) return source;
|
|
26
|
+
return source.slice(0, MAX_DOCUMENT_LENGTH) + '...[truncated]';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract operation metadata (name, type) from a parsed DocumentNode.
|
|
31
|
+
*
|
|
32
|
+
* Handles queries with multiple operation definitions by finding the one
|
|
33
|
+
* matching `operationName`, or falling back to the first definition.
|
|
34
|
+
*/
|
|
35
|
+
const extractOperationInfo = (
|
|
36
|
+
document: any,
|
|
37
|
+
operationName?: string | null
|
|
38
|
+
): { operationType: string; name: string } => {
|
|
39
|
+
const defaultResult = { operationType: 'query', name: operationName || 'anonymous' };
|
|
40
|
+
|
|
41
|
+
if (!document || !document.definitions || !Array.isArray(document.definitions)) {
|
|
42
|
+
return defaultResult;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Filter to operation definitions
|
|
46
|
+
const operationDefs = document.definitions.filter(
|
|
47
|
+
(def: any) => def.kind === 'OperationDefinition'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (operationDefs.length === 0) return defaultResult;
|
|
51
|
+
|
|
52
|
+
// Find the matching operation
|
|
53
|
+
let target = operationDefs[0];
|
|
54
|
+
if (operationName) {
|
|
55
|
+
const named = operationDefs.find(
|
|
56
|
+
(def: any) => def.name?.value === operationName
|
|
57
|
+
);
|
|
58
|
+
if (named) target = named;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
operationType: target.operation || 'query',
|
|
63
|
+
name: target.name?.value || operationName || 'anonymous',
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract the source text from a DocumentNode or Source object.
|
|
69
|
+
*/
|
|
70
|
+
const getSourceText = (document: any): string => {
|
|
71
|
+
if (!document) return '';
|
|
72
|
+
if (typeof document === 'string') return document;
|
|
73
|
+
if (document.loc?.source?.body) return document.loc.source.body;
|
|
74
|
+
if (document.source?.body) return document.source.body;
|
|
75
|
+
return '';
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if GraphQL result contains errors.
|
|
80
|
+
*/
|
|
81
|
+
const hasErrors = (result: any): boolean => {
|
|
82
|
+
return result && Array.isArray(result.errors) && result.errors.length > 0;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extract error summary from GraphQL errors array.
|
|
87
|
+
*/
|
|
88
|
+
const extractErrorSummary = (errors: any[]): string => {
|
|
89
|
+
if (!errors || errors.length === 0) return '';
|
|
90
|
+
const messages = errors
|
|
91
|
+
.slice(0, 5) // Limit to first 5 errors
|
|
92
|
+
.map((e: any) => e.message || String(e))
|
|
93
|
+
.join('; ');
|
|
94
|
+
return messages.length > 1024 ? messages.slice(0, 1024) + '...' : messages;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Patching
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
const patchGraphQL = (graphql: any, options?: SenzorOptions) => {
|
|
102
|
+
if (!graphql) return;
|
|
103
|
+
|
|
104
|
+
// Patch the top-level graphql() convenience function
|
|
105
|
+
patchMethod(
|
|
106
|
+
graphql,
|
|
107
|
+
'graphql',
|
|
108
|
+
'senzor.graphql.graphql',
|
|
109
|
+
(original) =>
|
|
110
|
+
function patchedGraphqlFn(this: any, ...args: any[]) {
|
|
111
|
+
// graphql(schema, source, rootValue, contextValue, variableValues, operationName)
|
|
112
|
+
// or graphql({ schema, source, rootValue, ... })
|
|
113
|
+
let source: string | undefined;
|
|
114
|
+
let operationName: string | undefined;
|
|
115
|
+
let document: any;
|
|
116
|
+
|
|
117
|
+
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
|
118
|
+
// Object form
|
|
119
|
+
source = args[0].source;
|
|
120
|
+
operationName = args[0].operationName;
|
|
121
|
+
} else {
|
|
122
|
+
source = args[1];
|
|
123
|
+
operationName = args[5];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const span = startCapturedSpan(
|
|
127
|
+
`GraphQL ${operationName || 'query'}`,
|
|
128
|
+
'custom',
|
|
129
|
+
{
|
|
130
|
+
'graphql.operation.name': operationName || 'anonymous',
|
|
131
|
+
'graphql.source': source ? truncateDocument(source) : undefined,
|
|
132
|
+
},
|
|
133
|
+
options
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!span) return original.apply(this, args);
|
|
137
|
+
|
|
138
|
+
return runWithCapturedSpan(span, () => {
|
|
139
|
+
try {
|
|
140
|
+
const result = original.apply(this, args);
|
|
141
|
+
|
|
142
|
+
if (result && typeof result.then === 'function') {
|
|
143
|
+
return result.then(
|
|
144
|
+
(res: any) => {
|
|
145
|
+
if (hasErrors(res)) {
|
|
146
|
+
span.end(500, {
|
|
147
|
+
'graphql.error_count': res.errors.length,
|
|
148
|
+
'error.message': extractErrorSummary(res.errors),
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
span.end(0);
|
|
152
|
+
}
|
|
153
|
+
return res;
|
|
154
|
+
},
|
|
155
|
+
(error: any) => {
|
|
156
|
+
span.end(500, {
|
|
157
|
+
'error.message': error?.message,
|
|
158
|
+
'error.type': error?.name || 'GraphQLError',
|
|
159
|
+
});
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (hasErrors(result)) {
|
|
166
|
+
span.end(500, {
|
|
167
|
+
'graphql.error_count': result.errors.length,
|
|
168
|
+
'error.message': extractErrorSummary(result.errors),
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
span.end(0);
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
} catch (error: any) {
|
|
175
|
+
span.end(500, {
|
|
176
|
+
'error.message': error?.message,
|
|
177
|
+
'error.type': error?.name || 'Error',
|
|
178
|
+
});
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Patch execute()
|
|
186
|
+
patchMethod(
|
|
187
|
+
graphql,
|
|
188
|
+
'execute',
|
|
189
|
+
'senzor.graphql.execute',
|
|
190
|
+
(original) =>
|
|
191
|
+
function patchedExecute(this: any, ...args: any[]) {
|
|
192
|
+
// execute(schema, document, rootValue, contextValue, variableValues, operationName)
|
|
193
|
+
// or execute({ schema, document, ... })
|
|
194
|
+
let document: any;
|
|
195
|
+
let operationName: string | undefined;
|
|
196
|
+
|
|
197
|
+
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && !Array.isArray(args[0])) {
|
|
198
|
+
document = args[0].document;
|
|
199
|
+
operationName = args[0].operationName;
|
|
200
|
+
} else {
|
|
201
|
+
document = args[1];
|
|
202
|
+
operationName = args[5];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const opInfo = extractOperationInfo(document, operationName);
|
|
206
|
+
const sourceText = getSourceText(document);
|
|
207
|
+
|
|
208
|
+
const span = startCapturedSpan(
|
|
209
|
+
`GraphQL execute ${opInfo.operationType} ${opInfo.name}`,
|
|
210
|
+
'custom',
|
|
211
|
+
{
|
|
212
|
+
'graphql.operation.name': opInfo.name,
|
|
213
|
+
'graphql.operation.type': opInfo.operationType,
|
|
214
|
+
'graphql.document': truncateDocument(sourceText),
|
|
215
|
+
},
|
|
216
|
+
options
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (!span) return original.apply(this, args);
|
|
220
|
+
|
|
221
|
+
return runWithCapturedSpan(span, () => {
|
|
222
|
+
try {
|
|
223
|
+
const result = original.apply(this, args);
|
|
224
|
+
|
|
225
|
+
if (result && typeof result.then === 'function') {
|
|
226
|
+
return result.then(
|
|
227
|
+
(res: any) => {
|
|
228
|
+
if (hasErrors(res)) {
|
|
229
|
+
span.end(500, {
|
|
230
|
+
'graphql.error_count': res.errors.length,
|
|
231
|
+
'error.message': extractErrorSummary(res.errors),
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
span.end(0);
|
|
235
|
+
}
|
|
236
|
+
return res;
|
|
237
|
+
},
|
|
238
|
+
(error: any) => {
|
|
239
|
+
span.end(500, {
|
|
240
|
+
'error.message': error?.message,
|
|
241
|
+
'error.type': error?.name || 'GraphQLError',
|
|
242
|
+
});
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (hasErrors(result)) {
|
|
249
|
+
span.end(500, {
|
|
250
|
+
'graphql.error_count': result.errors.length,
|
|
251
|
+
'error.message': extractErrorSummary(result.errors),
|
|
252
|
+
});
|
|
253
|
+
} else {
|
|
254
|
+
span.end(0);
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
} catch (error: any) {
|
|
258
|
+
span.end(500, {
|
|
259
|
+
'error.message': error?.message,
|
|
260
|
+
'error.type': error?.name || 'Error',
|
|
261
|
+
});
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Patch parse()
|
|
269
|
+
patchMethod(
|
|
270
|
+
graphql,
|
|
271
|
+
'parse',
|
|
272
|
+
'senzor.graphql.parse',
|
|
273
|
+
(original) =>
|
|
274
|
+
function patchedParse(this: any, source: any, ...args: any[]) {
|
|
275
|
+
const sourceText = typeof source === 'string'
|
|
276
|
+
? source
|
|
277
|
+
: source?.body || '';
|
|
278
|
+
|
|
279
|
+
const span = startCapturedSpan(
|
|
280
|
+
'GraphQL parse',
|
|
281
|
+
'custom',
|
|
282
|
+
{
|
|
283
|
+
'graphql.operation': 'parse',
|
|
284
|
+
'graphql.source': truncateDocument(sourceText),
|
|
285
|
+
},
|
|
286
|
+
options
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (!span) return original.call(this, source, ...args);
|
|
290
|
+
|
|
291
|
+
return runWithCapturedSpan(span, () => {
|
|
292
|
+
try {
|
|
293
|
+
const result = original.call(this, source, ...args);
|
|
294
|
+
span.end(0);
|
|
295
|
+
return result;
|
|
296
|
+
} catch (error: any) {
|
|
297
|
+
span.end(500, {
|
|
298
|
+
'error.message': error?.message,
|
|
299
|
+
'error.type': error?.name || 'GraphQLError',
|
|
300
|
+
'graphql.error_count': 1,
|
|
301
|
+
});
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Patch validate()
|
|
309
|
+
patchMethod(
|
|
310
|
+
graphql,
|
|
311
|
+
'validate',
|
|
312
|
+
'senzor.graphql.validate',
|
|
313
|
+
(original) =>
|
|
314
|
+
function patchedValidate(this: any, schema: any, documentAST: any, ...args: any[]) {
|
|
315
|
+
const span = startCapturedSpan(
|
|
316
|
+
'GraphQL validate',
|
|
317
|
+
'custom',
|
|
318
|
+
{
|
|
319
|
+
'graphql.operation': 'validate',
|
|
320
|
+
},
|
|
321
|
+
options
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (!span) return original.call(this, schema, documentAST, ...args);
|
|
325
|
+
|
|
326
|
+
return runWithCapturedSpan(span, () => {
|
|
327
|
+
try {
|
|
328
|
+
const errors = original.call(this, schema, documentAST, ...args);
|
|
329
|
+
|
|
330
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
331
|
+
span.end(500, {
|
|
332
|
+
'graphql.error_count': errors.length,
|
|
333
|
+
'error.message': extractErrorSummary(errors),
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
span.end(0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return errors;
|
|
340
|
+
} catch (error: any) {
|
|
341
|
+
span.end(500, {
|
|
342
|
+
'error.message': error?.message,
|
|
343
|
+
'error.type': error?.name || 'Error',
|
|
344
|
+
});
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Public API
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
export const instrumentGraphQL = (options?: SenzorOptions) => {
|
|
357
|
+
hookRequire('graphql', (exports: any) => patchGraphQL(exports, options));
|
|
358
|
+
|
|
359
|
+
// Also try graphql/execution for cases where execute is imported directly
|
|
360
|
+
hookRequire('graphql/execution', (exports: any) => {
|
|
361
|
+
if (exports?.execute) {
|
|
362
|
+
patchMethod(
|
|
363
|
+
exports,
|
|
364
|
+
'execute',
|
|
365
|
+
'senzor.graphql.execution.execute',
|
|
366
|
+
(original) => {
|
|
367
|
+
// Reuse the same logic
|
|
368
|
+
return function patchedDirectExecute(this: any, ...args: any[]) {
|
|
369
|
+
let document: any;
|
|
370
|
+
let operationName: string | undefined;
|
|
371
|
+
|
|
372
|
+
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
|
373
|
+
document = args[0].document;
|
|
374
|
+
operationName = args[0].operationName;
|
|
375
|
+
} else {
|
|
376
|
+
document = args[1];
|
|
377
|
+
operationName = args[5];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const opInfo = extractOperationInfo(document, operationName);
|
|
381
|
+
const sourceText = getSourceText(document);
|
|
382
|
+
|
|
383
|
+
const span = startCapturedSpan(
|
|
384
|
+
`GraphQL execute ${opInfo.operationType} ${opInfo.name}`,
|
|
385
|
+
'custom',
|
|
386
|
+
{
|
|
387
|
+
'graphql.operation.name': opInfo.name,
|
|
388
|
+
'graphql.operation.type': opInfo.operationType,
|
|
389
|
+
'graphql.document': truncateDocument(sourceText),
|
|
390
|
+
},
|
|
391
|
+
options
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
if (!span) return original.apply(this, args);
|
|
395
|
+
|
|
396
|
+
return runWithCapturedSpan(span, () => {
|
|
397
|
+
try {
|
|
398
|
+
const result = original.apply(this, args);
|
|
399
|
+
if (result && typeof result.then === 'function') {
|
|
400
|
+
return result.then(
|
|
401
|
+
(res: any) => {
|
|
402
|
+
if (hasErrors(res)) {
|
|
403
|
+
span.end(500, {
|
|
404
|
+
'graphql.error_count': res.errors.length,
|
|
405
|
+
'error.message': extractErrorSummary(res.errors),
|
|
406
|
+
});
|
|
407
|
+
} else {
|
|
408
|
+
span.end(0);
|
|
409
|
+
}
|
|
410
|
+
return res;
|
|
411
|
+
},
|
|
412
|
+
(error: any) => {
|
|
413
|
+
span.end(500, { 'error.message': error?.message });
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (hasErrors(result)) {
|
|
419
|
+
span.end(500, { 'graphql.error_count': result.errors.length });
|
|
420
|
+
} else {
|
|
421
|
+
span.end(0);
|
|
422
|
+
}
|
|
423
|
+
return result;
|
|
424
|
+
} catch (error: any) {
|
|
425
|
+
span.end(500, { 'error.message': error?.message });
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
};
|