@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +479 -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/register.js +1 -1
  12. package/dist/register.js.map +1 -1
  13. package/dist/register.mjs +1 -1
  14. package/dist/register.mjs.map +1 -1
  15. package/package.json +1 -1
  16. package/src/core/client.ts +57 -0
  17. package/src/core/context.ts +71 -9
  18. package/src/core/transport.ts +20 -3
  19. package/src/core/types.ts +5 -1
  20. package/src/index.ts +4 -0
  21. package/src/instrumentation/amqplib.ts +371 -0
  22. package/src/instrumentation/anthropic.ts +245 -0
  23. package/src/instrumentation/aws-sdk.ts +403 -0
  24. package/src/instrumentation/azure-openai.ts +177 -0
  25. package/src/instrumentation/bunyan.ts +93 -0
  26. package/src/instrumentation/cassandra.ts +367 -0
  27. package/src/instrumentation/cohere.ts +227 -0
  28. package/src/instrumentation/connect.ts +200 -0
  29. package/src/instrumentation/dataloader.ts +291 -0
  30. package/src/instrumentation/dns.ts +220 -0
  31. package/src/instrumentation/firebase.ts +445 -0
  32. package/src/instrumentation/fs.ts +260 -0
  33. package/src/instrumentation/generic-pool.ts +317 -0
  34. package/src/instrumentation/google-genai.ts +426 -0
  35. package/src/instrumentation/graphql.ts +434 -0
  36. package/src/instrumentation/grpc.ts +666 -0
  37. package/src/instrumentation/hapi.ts +257 -0
  38. package/src/instrumentation/kafka.ts +360 -0
  39. package/src/instrumentation/knex.ts +249 -0
  40. package/src/instrumentation/lru-memoizer.ts +175 -0
  41. package/src/instrumentation/memcached.ts +190 -0
  42. package/src/instrumentation/mistral.ts +254 -0
  43. package/src/instrumentation/nestjs.ts +243 -0
  44. package/src/instrumentation/net.ts +171 -0
  45. package/src/instrumentation/openai.ts +281 -0
  46. package/src/instrumentation/pino.ts +170 -0
  47. package/src/instrumentation/restify.ts +213 -0
  48. package/src/instrumentation/runtime.ts +352 -0
  49. package/src/instrumentation/socketio.ts +272 -0
  50. package/src/instrumentation/tedious.ts +509 -0
  51. package/src/instrumentation/winston.ts +149 -0
  52. package/src/register.ts +22 -3
  53. package/src/wrappers/lambda.ts +417 -0
  54. package/tsup.config.ts +3 -3
  55. package/wiki.md +1547 -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
+ };