@senzops/apm-node 1.2.8 → 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 (54) 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/transport.ts +20 -3
  18. package/src/core/types.ts +5 -1
  19. package/src/index.ts +4 -0
  20. package/src/instrumentation/amqplib.ts +371 -0
  21. package/src/instrumentation/anthropic.ts +245 -0
  22. package/src/instrumentation/aws-sdk.ts +403 -0
  23. package/src/instrumentation/azure-openai.ts +177 -0
  24. package/src/instrumentation/bunyan.ts +93 -0
  25. package/src/instrumentation/cassandra.ts +367 -0
  26. package/src/instrumentation/cohere.ts +227 -0
  27. package/src/instrumentation/connect.ts +200 -0
  28. package/src/instrumentation/dataloader.ts +291 -0
  29. package/src/instrumentation/dns.ts +220 -0
  30. package/src/instrumentation/firebase.ts +445 -0
  31. package/src/instrumentation/fs.ts +260 -0
  32. package/src/instrumentation/generic-pool.ts +317 -0
  33. package/src/instrumentation/google-genai.ts +426 -0
  34. package/src/instrumentation/graphql.ts +434 -0
  35. package/src/instrumentation/grpc.ts +666 -0
  36. package/src/instrumentation/hapi.ts +257 -0
  37. package/src/instrumentation/kafka.ts +360 -0
  38. package/src/instrumentation/knex.ts +249 -0
  39. package/src/instrumentation/lru-memoizer.ts +175 -0
  40. package/src/instrumentation/memcached.ts +190 -0
  41. package/src/instrumentation/mistral.ts +254 -0
  42. package/src/instrumentation/nestjs.ts +243 -0
  43. package/src/instrumentation/net.ts +171 -0
  44. package/src/instrumentation/openai.ts +281 -0
  45. package/src/instrumentation/pino.ts +170 -0
  46. package/src/instrumentation/restify.ts +213 -0
  47. package/src/instrumentation/runtime.ts +352 -0
  48. package/src/instrumentation/socketio.ts +272 -0
  49. package/src/instrumentation/tedious.ts +509 -0
  50. package/src/instrumentation/winston.ts +149 -0
  51. package/src/register.ts +22 -3
  52. package/src/wrappers/lambda.ts +417 -0
  53. package/tsup.config.ts +3 -3
  54. package/wiki.md +1547 -852
@@ -0,0 +1,666 @@
1
+ import { SenzorOptions } from '../core/types';
2
+ import { Context } from '../core/context';
3
+ import { hookRequire } from './hook';
4
+ import { patchMethod } from './patch';
5
+ import { runWithCapturedSpan, startCapturedSpan } from './span';
6
+ import { generateTraceparent, parseTraceparent } from '../utils/traceContext';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // gRPC Instrumentation
10
+ //
11
+ // Instruments @grpc/grpc-js for both client (unary + streaming) and server
12
+ // (unary + streaming) calls. Follows OTel RPC semantic conventions.
13
+ //
14
+ // Client spans: rpc.system=grpc, rpc.service, rpc.method, rpc.grpc.status_code
15
+ // Server spans: Creates a root trace per incoming RPC call
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** gRPC status code to human-readable name (subset for common codes). */
19
+ const GRPC_STATUS_NAMES: Record<number, string> = {
20
+ 0: 'OK',
21
+ 1: 'CANCELLED',
22
+ 2: 'UNKNOWN',
23
+ 3: 'INVALID_ARGUMENT',
24
+ 4: 'DEADLINE_EXCEEDED',
25
+ 5: 'NOT_FOUND',
26
+ 6: 'ALREADY_EXISTS',
27
+ 7: 'PERMISSION_DENIED',
28
+ 8: 'RESOURCE_EXHAUSTED',
29
+ 9: 'FAILED_PRECONDITION',
30
+ 10: 'ABORTED',
31
+ 11: 'OUT_OF_RANGE',
32
+ 12: 'UNIMPLEMENTED',
33
+ 13: 'INTERNAL',
34
+ 14: 'UNAVAILABLE',
35
+ 15: 'DATA_LOSS',
36
+ 16: 'UNAUTHENTICATED',
37
+ };
38
+
39
+ /** Map gRPC status code → HTTP-equivalent status for span.status. */
40
+ const grpcStatusToHttp = (code: number): number => {
41
+ if (code === 0) return 0; // OK
42
+ if (code === 1) return 499; // CANCELLED → Client Closed Request
43
+ if (code === 3 || code === 9 || code === 11) return 400; // INVALID_ARGUMENT, FAILED_PRECONDITION, OUT_OF_RANGE
44
+ if (code === 4) return 504; // DEADLINE_EXCEEDED
45
+ if (code === 5) return 404; // NOT_FOUND
46
+ if (code === 6) return 409; // ALREADY_EXISTS
47
+ if (code === 7) return 403; // PERMISSION_DENIED
48
+ if (code === 8) return 429; // RESOURCE_EXHAUSTED
49
+ if (code === 10) return 409; // ABORTED
50
+ if (code === 12) return 501; // UNIMPLEMENTED
51
+ if (code === 16) return 401; // UNAUTHENTICATED
52
+ return 500; // UNKNOWN, INTERNAL, UNAVAILABLE, DATA_LOSS, etc.
53
+ };
54
+
55
+ /** Parse a gRPC full method path like /package.ServiceName/MethodName */
56
+ const parseGrpcMethod = (fullPath: string): { service: string; method: string } => {
57
+ const parts = fullPath.replace(/^\//, '').split('/');
58
+ return {
59
+ service: parts[0] || 'unknown',
60
+ method: parts[1] || 'unknown',
61
+ };
62
+ };
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Metadata propagation helpers
66
+ // ---------------------------------------------------------------------------
67
+
68
+ const TRACEPARENT_KEY = 'traceparent';
69
+ const SENZOR_TRACE_KEY = 'x-senzor-trace-id';
70
+ const SENZOR_SPAN_KEY = 'x-senzor-parent-span-id';
71
+
72
+ /** Inject trace context into gRPC metadata (client-side). */
73
+ const injectMetadata = (metadata: any, traceId: string, spanId: string) => {
74
+ try {
75
+ if (metadata && typeof metadata.set === 'function') {
76
+ metadata.set(TRACEPARENT_KEY, generateTraceparent(traceId, spanId));
77
+ metadata.set(SENZOR_TRACE_KEY, traceId);
78
+ metadata.set(SENZOR_SPAN_KEY, spanId);
79
+ }
80
+ } catch { /* metadata immutable — skip silently */ }
81
+ };
82
+
83
+ /** Extract trace context from incoming gRPC metadata (server-side). */
84
+ const extractFromMetadata = (
85
+ metadata: any
86
+ ): { traceId?: string; parentSpanId?: string } => {
87
+ try {
88
+ if (!metadata || typeof metadata.get !== 'function') return {};
89
+
90
+ const traceparent = metadata.get(TRACEPARENT_KEY);
91
+ const tp = Array.isArray(traceparent) ? traceparent[0] : traceparent;
92
+ if (tp) {
93
+ const parsed = parseTraceparent(String(tp));
94
+ if (parsed) return parsed;
95
+ }
96
+
97
+ const traceId = metadata.get(SENZOR_TRACE_KEY);
98
+ const parentSpanId = metadata.get(SENZOR_SPAN_KEY);
99
+ return {
100
+ traceId: Array.isArray(traceId) ? traceId[0] : traceId || undefined,
101
+ parentSpanId: Array.isArray(parentSpanId) ? parentSpanId[0] : parentSpanId || undefined,
102
+ };
103
+ } catch {
104
+ return {};
105
+ }
106
+ };
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Client interceptor
110
+ // ---------------------------------------------------------------------------
111
+
112
+ /**
113
+ * Creates a gRPC client interceptor that wraps each outbound RPC call in a
114
+ * child span and propagates trace context via metadata.
115
+ */
116
+ const createClientInterceptor = (options?: SenzorOptions) => {
117
+ return (methodOptions: any, nextCall: any) => {
118
+ const trace = Context.current();
119
+ if (!trace) return nextCall(methodOptions);
120
+
121
+ const { service, method } = parseGrpcMethod(methodOptions.method_definition?.path || '');
122
+ const fullMethod = methodOptions.method_definition?.path || `/${service}/${method}`;
123
+
124
+ const span = startCapturedSpan(
125
+ `gRPC ${service}/${method}`,
126
+ 'http',
127
+ {
128
+ 'rpc.system': 'grpc',
129
+ 'rpc.service': service,
130
+ 'rpc.method': method,
131
+ 'rpc.grpc.full_method': fullMethod,
132
+ 'network.transport': 'tcp',
133
+ },
134
+ options
135
+ );
136
+
137
+ if (!span) return nextCall(methodOptions);
138
+
139
+ // Clone method options to inject metadata
140
+ const newMethodOptions = { ...methodOptions };
141
+
142
+ return runWithCapturedSpan(span, () => {
143
+ const interceptingCall = new (getInterceptingCall())(nextCall(newMethodOptions));
144
+
145
+ // We need a requester that injects metadata on start and captures status on close
146
+ const requester = {
147
+ start: (metadata: any, listener: any, next: (metadata: any, listener: any) => void) => {
148
+ injectMetadata(metadata, trace.id, span.spanId);
149
+
150
+ const wrappedListener = {
151
+ onReceiveMetadata: (metadata: any, next: (metadata: any) => void) => {
152
+ next(metadata);
153
+ },
154
+ onReceiveMessage: (message: any, next: (message: any) => void) => {
155
+ next(message);
156
+ },
157
+ onReceiveStatus: (status: any, next: (status: any) => void) => {
158
+ const grpcCode = status?.code ?? 0;
159
+ span.end(grpcStatusToHttp(grpcCode), {
160
+ 'rpc.grpc.status_code': grpcCode,
161
+ 'rpc.grpc.status_text': GRPC_STATUS_NAMES[grpcCode] || 'UNKNOWN',
162
+ ...(status?.details ? { 'error.message': status.details } : {}),
163
+ });
164
+ next(status);
165
+ },
166
+ };
167
+
168
+ next(metadata, wrappedListener);
169
+ },
170
+ sendMessage: (message: any, next: (message: any) => void) => {
171
+ next(message);
172
+ },
173
+ halfClose: (next: () => void) => {
174
+ next();
175
+ },
176
+ cancel: (message: string, next: () => void) => {
177
+ span.end(grpcStatusToHttp(1), {
178
+ 'rpc.grpc.status_code': 1,
179
+ 'rpc.grpc.status_text': 'CANCELLED',
180
+ 'error.message': message || 'Call cancelled',
181
+ });
182
+ next();
183
+ },
184
+ };
185
+
186
+ return interceptingCall;
187
+ });
188
+ };
189
+ };
190
+
191
+ // Cache the InterceptingCall class
192
+ let _InterceptingCall: any = null;
193
+ const getInterceptingCall = (): any => {
194
+ if (_InterceptingCall) return _InterceptingCall;
195
+ try {
196
+ const grpc = require('@grpc/grpc-js');
197
+ _InterceptingCall = grpc.InterceptingCall;
198
+ } catch { }
199
+ return _InterceptingCall;
200
+ };
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Client-side patching (simpler approach: patch makeUnaryRequest etc.)
204
+ // ---------------------------------------------------------------------------
205
+
206
+ const patchClientMethods = (grpc: any, options?: SenzorOptions) => {
207
+ const clientProto = grpc?.Client?.prototype;
208
+ if (!clientProto) return;
209
+
210
+ // Patch makeUnaryRequest — the core method all unary stubs resolve to
211
+ patchMethod(
212
+ clientProto,
213
+ 'makeUnaryRequest',
214
+ 'senzor.grpc.client.makeUnaryRequest',
215
+ (original) =>
216
+ function patchedMakeUnaryRequest(this: any, method: string, ...args: any[]) {
217
+ const trace = Context.current();
218
+ if (!trace) return original.call(this, method, ...args);
219
+
220
+ const { service, method: rpcMethod } = parseGrpcMethod(method);
221
+
222
+ const span = startCapturedSpan(
223
+ `gRPC ${service}/${rpcMethod}`,
224
+ 'http',
225
+ {
226
+ 'rpc.system': 'grpc',
227
+ 'rpc.service': service,
228
+ 'rpc.method': rpcMethod,
229
+ 'rpc.grpc.full_method': method,
230
+ 'network.transport': 'tcp',
231
+ },
232
+ options
233
+ );
234
+
235
+ if (!span) return original.call(this, method, ...args);
236
+
237
+ // args: [serialize, deserialize, argument, metadata, options, callback]
238
+ // Inject trace context into metadata
239
+ let metadataIdx = -1;
240
+ let callbackIdx = -1;
241
+
242
+ for (let i = 0; i < args.length; i++) {
243
+ if (args[i] && typeof args[i] === 'object' && typeof args[i].set === 'function' && typeof args[i].get === 'function') {
244
+ metadataIdx = i;
245
+ }
246
+ if (typeof args[i] === 'function' && i === args.length - 1) {
247
+ callbackIdx = i;
248
+ }
249
+ }
250
+
251
+ // Inject into metadata if found
252
+ if (metadataIdx >= 0) {
253
+ injectMetadata(args[metadataIdx], trace.id, span.spanId);
254
+ }
255
+
256
+ // Wrap callback to capture status
257
+ if (callbackIdx >= 0) {
258
+ const originalCallback = args[callbackIdx];
259
+ args[callbackIdx] = function wrappedGrpcCallback(err: any, response: any) {
260
+ if (err) {
261
+ const grpcCode = err.code ?? 2;
262
+ span.end(grpcStatusToHttp(grpcCode), {
263
+ 'rpc.grpc.status_code': grpcCode,
264
+ 'rpc.grpc.status_text': GRPC_STATUS_NAMES[grpcCode] || 'UNKNOWN',
265
+ 'error.message': err.details || err.message,
266
+ 'error.type': err.name || 'GrpcError',
267
+ });
268
+ } else {
269
+ span.end(0, {
270
+ 'rpc.grpc.status_code': 0,
271
+ 'rpc.grpc.status_text': 'OK',
272
+ });
273
+ }
274
+ return originalCallback.call(this, err, response);
275
+ };
276
+ }
277
+
278
+ return runWithCapturedSpan(span, () => {
279
+ try {
280
+ const call = original.call(this, method, ...args);
281
+
282
+ // If no callback was provided, the call returns a ClientUnaryCall
283
+ // which emits 'status' and 'error' events
284
+ if (callbackIdx < 0 && call && typeof call.on === 'function') {
285
+ let ended = false;
286
+ call.on('status', (status: any) => {
287
+ if (ended) return;
288
+ ended = true;
289
+ const grpcCode = status?.code ?? 0;
290
+ span.end(grpcStatusToHttp(grpcCode), {
291
+ 'rpc.grpc.status_code': grpcCode,
292
+ 'rpc.grpc.status_text': GRPC_STATUS_NAMES[grpcCode] || 'UNKNOWN',
293
+ });
294
+ });
295
+ call.on('error', (err: any) => {
296
+ if (ended) return;
297
+ ended = true;
298
+ const grpcCode = err?.code ?? 2;
299
+ span.end(grpcStatusToHttp(grpcCode), {
300
+ 'rpc.grpc.status_code': grpcCode,
301
+ 'error.message': err?.details || err?.message,
302
+ 'error.type': err?.name || 'GrpcError',
303
+ });
304
+ });
305
+ }
306
+
307
+ return call;
308
+ } catch (error: any) {
309
+ span.end(500, {
310
+ 'rpc.grpc.status_code': 13,
311
+ 'rpc.grpc.status_text': 'INTERNAL',
312
+ 'error.message': error?.message,
313
+ 'error.type': error?.name || 'Error',
314
+ });
315
+ throw error;
316
+ }
317
+ });
318
+ }
319
+ );
320
+
321
+ // Patch makeClientStreamRequest
322
+ patchMethod(
323
+ clientProto,
324
+ 'makeClientStreamRequest',
325
+ 'senzor.grpc.client.makeClientStreamRequest',
326
+ (original) =>
327
+ function patchedMakeClientStreamRequest(this: any, method: string, ...args: any[]) {
328
+ const trace = Context.current();
329
+ if (!trace) return original.call(this, method, ...args);
330
+
331
+ const { service, method: rpcMethod } = parseGrpcMethod(method);
332
+ const span = startCapturedSpan(
333
+ `gRPC ${service}/${rpcMethod} (client-stream)`,
334
+ 'http',
335
+ {
336
+ 'rpc.system': 'grpc',
337
+ 'rpc.service': service,
338
+ 'rpc.method': rpcMethod,
339
+ 'rpc.grpc.full_method': method,
340
+ 'rpc.grpc.call_type': 'client_stream',
341
+ 'network.transport': 'tcp',
342
+ },
343
+ options
344
+ );
345
+
346
+ if (!span) return original.call(this, method, ...args);
347
+
348
+ // Inject metadata
349
+ for (let i = 0; i < args.length; i++) {
350
+ if (args[i] && typeof args[i] === 'object' && typeof args[i].set === 'function' && typeof args[i].get === 'function') {
351
+ injectMetadata(args[i], trace.id, span.spanId);
352
+ break;
353
+ }
354
+ }
355
+
356
+ // Wrap callback
357
+ for (let i = args.length - 1; i >= 0; i--) {
358
+ if (typeof args[i] === 'function') {
359
+ const originalCallback = args[i];
360
+ args[i] = function wrappedCallback(err: any, response: any) {
361
+ if (err) {
362
+ const grpcCode = err.code ?? 2;
363
+ span.end(grpcStatusToHttp(grpcCode), {
364
+ 'rpc.grpc.status_code': grpcCode,
365
+ 'error.message': err.details || err.message,
366
+ });
367
+ } else {
368
+ span.end(0, { 'rpc.grpc.status_code': 0 });
369
+ }
370
+ return originalCallback.call(this, err, response);
371
+ };
372
+ break;
373
+ }
374
+ }
375
+
376
+ return runWithCapturedSpan(span, () => {
377
+ try {
378
+ return original.call(this, method, ...args);
379
+ } catch (error: any) {
380
+ span.end(500, { 'error.message': error?.message, 'error.type': error?.name });
381
+ throw error;
382
+ }
383
+ });
384
+ }
385
+ );
386
+
387
+ // Patch makeServerStreamRequest
388
+ patchMethod(
389
+ clientProto,
390
+ 'makeServerStreamRequest',
391
+ 'senzor.grpc.client.makeServerStreamRequest',
392
+ (original) =>
393
+ function patchedMakeServerStreamRequest(this: any, method: string, ...args: any[]) {
394
+ const trace = Context.current();
395
+ if (!trace) return original.call(this, method, ...args);
396
+
397
+ const { service, method: rpcMethod } = parseGrpcMethod(method);
398
+ const span = startCapturedSpan(
399
+ `gRPC ${service}/${rpcMethod} (server-stream)`,
400
+ 'http',
401
+ {
402
+ 'rpc.system': 'grpc',
403
+ 'rpc.service': service,
404
+ 'rpc.method': rpcMethod,
405
+ 'rpc.grpc.full_method': method,
406
+ 'rpc.grpc.call_type': 'server_stream',
407
+ 'network.transport': 'tcp',
408
+ },
409
+ options
410
+ );
411
+
412
+ if (!span) return original.call(this, method, ...args);
413
+
414
+ // Inject metadata
415
+ for (let i = 0; i < args.length; i++) {
416
+ if (args[i] && typeof args[i] === 'object' && typeof args[i].set === 'function') {
417
+ injectMetadata(args[i], trace.id, span.spanId);
418
+ break;
419
+ }
420
+ }
421
+
422
+ return runWithCapturedSpan(span, () => {
423
+ try {
424
+ const call = original.call(this, method, ...args);
425
+
426
+ if (call && typeof call.on === 'function') {
427
+ let ended = false;
428
+ call.on('status', (status: any) => {
429
+ if (ended) return;
430
+ ended = true;
431
+ const grpcCode = status?.code ?? 0;
432
+ span.end(grpcStatusToHttp(grpcCode), {
433
+ 'rpc.grpc.status_code': grpcCode,
434
+ 'rpc.grpc.status_text': GRPC_STATUS_NAMES[grpcCode] || 'UNKNOWN',
435
+ });
436
+ });
437
+ call.on('error', (err: any) => {
438
+ if (ended) return;
439
+ ended = true;
440
+ span.end(grpcStatusToHttp(err?.code ?? 2), {
441
+ 'rpc.grpc.status_code': err?.code ?? 2,
442
+ 'error.message': err?.details || err?.message,
443
+ });
444
+ });
445
+ }
446
+
447
+ return call;
448
+ } catch (error: any) {
449
+ span.end(500, { 'error.message': error?.message, 'error.type': error?.name });
450
+ throw error;
451
+ }
452
+ });
453
+ }
454
+ );
455
+
456
+ // Patch makeBidiStreamRequest
457
+ patchMethod(
458
+ clientProto,
459
+ 'makeBidiStreamRequest',
460
+ 'senzor.grpc.client.makeBidiStreamRequest',
461
+ (original) =>
462
+ function patchedMakeBidiStreamRequest(this: any, method: string, ...args: any[]) {
463
+ const trace = Context.current();
464
+ if (!trace) return original.call(this, method, ...args);
465
+
466
+ const { service, method: rpcMethod } = parseGrpcMethod(method);
467
+ const span = startCapturedSpan(
468
+ `gRPC ${service}/${rpcMethod} (bidi-stream)`,
469
+ 'http',
470
+ {
471
+ 'rpc.system': 'grpc',
472
+ 'rpc.service': service,
473
+ 'rpc.method': rpcMethod,
474
+ 'rpc.grpc.full_method': method,
475
+ 'rpc.grpc.call_type': 'bidi_stream',
476
+ 'network.transport': 'tcp',
477
+ },
478
+ options
479
+ );
480
+
481
+ if (!span) return original.call(this, method, ...args);
482
+
483
+ for (let i = 0; i < args.length; i++) {
484
+ if (args[i] && typeof args[i] === 'object' && typeof args[i].set === 'function') {
485
+ injectMetadata(args[i], trace.id, span.spanId);
486
+ break;
487
+ }
488
+ }
489
+
490
+ return runWithCapturedSpan(span, () => {
491
+ try {
492
+ const call = original.call(this, method, ...args);
493
+
494
+ if (call && typeof call.on === 'function') {
495
+ let ended = false;
496
+ call.on('status', (status: any) => {
497
+ if (ended) return;
498
+ ended = true;
499
+ const grpcCode = status?.code ?? 0;
500
+ span.end(grpcStatusToHttp(grpcCode), {
501
+ 'rpc.grpc.status_code': grpcCode,
502
+ });
503
+ });
504
+ call.on('error', (err: any) => {
505
+ if (ended) return;
506
+ ended = true;
507
+ span.end(grpcStatusToHttp(err?.code ?? 2), {
508
+ 'rpc.grpc.status_code': err?.code ?? 2,
509
+ 'error.message': err?.details || err?.message,
510
+ });
511
+ });
512
+ }
513
+
514
+ return call;
515
+ } catch (error: any) {
516
+ span.end(500, { 'error.message': error?.message, 'error.type': error?.name });
517
+ throw error;
518
+ }
519
+ });
520
+ }
521
+ );
522
+ };
523
+
524
+ // ---------------------------------------------------------------------------
525
+ // Server-side patching
526
+ // ---------------------------------------------------------------------------
527
+
528
+ const patchServerRegister = (grpc: any, client: any, options?: SenzorOptions) => {
529
+ const serverProto = grpc?.Server?.prototype;
530
+ if (!serverProto) return;
531
+
532
+ patchMethod(
533
+ serverProto,
534
+ 'register',
535
+ 'senzor.grpc.server.register',
536
+ (original) =>
537
+ function patchedRegister(
538
+ this: any,
539
+ name: string,
540
+ handler: any,
541
+ serialize: any,
542
+ deserialize: any,
543
+ type: any
544
+ ) {
545
+ if (typeof handler !== 'function') {
546
+ return original.call(this, name, handler, serialize, deserialize, type);
547
+ }
548
+
549
+ const wrappedHandler = function (this: any, call: any, callback?: any) {
550
+ const { service, method } = parseGrpcMethod(name);
551
+ const metadata = call?.metadata;
552
+ const parentCtx = extractFromMetadata(metadata);
553
+
554
+ const span = startCapturedSpan(
555
+ `gRPC ${service}/${method}`,
556
+ 'http',
557
+ {
558
+ 'rpc.system': 'grpc',
559
+ 'rpc.service': service,
560
+ 'rpc.method': method,
561
+ 'rpc.grpc.full_method': name,
562
+ 'rpc.grpc.call_type': type || 'unary',
563
+ 'server.type': 'grpc',
564
+ ...(parentCtx.traceId ? { 'parent.trace_id': parentCtx.traceId } : {}),
565
+ },
566
+ options
567
+ );
568
+
569
+ if (!span) {
570
+ return handler.call(this, call, callback);
571
+ }
572
+
573
+ // Wrap callback for unary/client-streaming handlers
574
+ if (typeof callback === 'function') {
575
+ const wrappedCallback = function (err: any, response: any, trailer?: any, flags?: any) {
576
+ if (err) {
577
+ const grpcCode = err.code ?? 2;
578
+ span.end(grpcStatusToHttp(grpcCode), {
579
+ 'rpc.grpc.status_code': grpcCode,
580
+ 'rpc.grpc.status_text': GRPC_STATUS_NAMES[grpcCode] || 'UNKNOWN',
581
+ 'error.message': err.details || err.message,
582
+ 'error.type': err.name || 'GrpcError',
583
+ });
584
+ } else {
585
+ span.end(0, {
586
+ 'rpc.grpc.status_code': 0,
587
+ 'rpc.grpc.status_text': 'OK',
588
+ });
589
+ }
590
+ return callback(err, response, trailer, flags);
591
+ };
592
+
593
+ return runWithCapturedSpan(span, () => {
594
+ try {
595
+ return handler.call(this, call, wrappedCallback);
596
+ } catch (error: any) {
597
+ span.end(500, {
598
+ 'rpc.grpc.status_code': 13,
599
+ 'error.message': error?.message,
600
+ 'error.type': error?.name || 'Error',
601
+ });
602
+ throw error;
603
+ }
604
+ });
605
+ }
606
+
607
+ // Server-streaming / bidi-streaming: listen to call events
608
+ return runWithCapturedSpan(span, () => {
609
+ try {
610
+ const result = handler.call(this, call);
611
+
612
+ if (call && typeof call.on === 'function') {
613
+ let ended = false;
614
+ const endOnce = (status: number, meta: Record<string, any>) => {
615
+ if (ended) return;
616
+ ended = true;
617
+ span.end(status, meta);
618
+ };
619
+
620
+ call.on('error', (err: any) => {
621
+ const grpcCode = err?.code ?? 2;
622
+ endOnce(grpcStatusToHttp(grpcCode), {
623
+ 'rpc.grpc.status_code': grpcCode,
624
+ 'error.message': err?.details || err?.message,
625
+ });
626
+ });
627
+
628
+ call.on('end', () => {
629
+ endOnce(0, { 'rpc.grpc.status_code': 0 });
630
+ });
631
+
632
+ call.on('cancelled', () => {
633
+ endOnce(grpcStatusToHttp(1), {
634
+ 'rpc.grpc.status_code': 1,
635
+ 'rpc.grpc.status_text': 'CANCELLED',
636
+ });
637
+ });
638
+ }
639
+
640
+ return result;
641
+ } catch (error: any) {
642
+ span.end(500, {
643
+ 'rpc.grpc.status_code': 13,
644
+ 'error.message': error?.message,
645
+ 'error.type': error?.name || 'Error',
646
+ });
647
+ throw error;
648
+ }
649
+ });
650
+ };
651
+
652
+ return original.call(this, name, wrappedHandler, serialize, deserialize, type);
653
+ }
654
+ );
655
+ };
656
+
657
+ // ---------------------------------------------------------------------------
658
+ // Public API
659
+ // ---------------------------------------------------------------------------
660
+
661
+ export const instrumentGrpc = (client: any, options?: SenzorOptions) => {
662
+ hookRequire('@grpc/grpc-js', (exports: any) => {
663
+ patchClientMethods(exports, options);
664
+ patchServerRegister(exports, client, options);
665
+ });
666
+ };