@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senzops/apm-node",
3
- "version": "1.2.8",
3
+ "version": "1.3.0",
4
4
  "description": "Universal APM SDK for Senzor",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -7,6 +7,7 @@ import { parseTraceparent } from '../utils/traceContext';
7
7
  import { generateSpanId, generateTraceId } from '../utils/ids';
8
8
  import { sanitizeAttributes } from './sanitizer';
9
9
  import { startCapturedSpan } from '../instrumentation/span';
10
+ import { RuntimeMetricsCollector } from '../instrumentation/runtime';
10
11
 
11
12
  // Memory-safe JSON stringifier to handle cyclical objects
12
13
  // (like Express 'req' objects) passed into console.log
@@ -25,6 +26,7 @@ export class SenzorClient {
25
26
  private transport: Transport | null = null;
26
27
  private options: SenzorOptions | null = null;
27
28
  private isInstrumented = false;
29
+ private runtimeMetricsCollector: RuntimeMetricsCollector | null = null;
28
30
 
29
31
  public preload(options: Partial<SenzorOptions> = {}) {
30
32
  const endpoint = options.endpoint || 'https://api.senzor.dev/api/ingest/apm';
@@ -87,6 +89,61 @@ export class SenzorClient {
87
89
  try { if (this.isInstrumentationEnabled('redis')) { const { instrumentRedis } = require('../instrumentation/redis'); instrumentRedis(this.options || undefined); } } catch {}
88
90
  try { if (this.isInstrumentationEnabled('bullmq')) { const { instrumentBullMQ } = require('../instrumentation/bullmq'); instrumentBullMQ(this, debug); } } catch {}
89
91
  try { if (this.isInstrumentationEnabled('cron')) { const { instrumentNodeCron } = require('../instrumentation/cron'); instrumentNodeCron(this, debug); } } catch {}
92
+
93
+ // --- Phase 1 Instrumentations ---
94
+ try { if (this.isInstrumentationEnabled('grpc')) { const { instrumentGrpc } = require('../instrumentation/grpc'); instrumentGrpc(this, this.options || undefined); } } catch {}
95
+ try { if (this.isInstrumentationEnabled('graphql')) { const { instrumentGraphQL } = require('../instrumentation/graphql'); instrumentGraphQL(this.options || undefined); } } catch {}
96
+ try { if (this.isInstrumentationEnabled('dns')) { const { instrumentDns } = require('../instrumentation/dns'); instrumentDns(this.options || undefined); } } catch {}
97
+ try { if (this.isInstrumentationEnabled('net')) { const { instrumentNet } = require('../instrumentation/net'); instrumentNet(this.options || undefined); } } catch {}
98
+
99
+ // --- Phase 2 Instrumentations: Messaging ---
100
+ try { if (this.isInstrumentationEnabled('kafka')) { const { instrumentKafka } = require('../instrumentation/kafka'); instrumentKafka(this.options || undefined); } } catch {}
101
+ try { if (this.isInstrumentationEnabled('amqplib')) { const { instrumentAmqplib } = require('../instrumentation/amqplib'); instrumentAmqplib(this.options || undefined); } } catch {}
102
+ try { if (this.isInstrumentationEnabled('socketio')) { const { instrumentSocketIO } = require('../instrumentation/socketio'); instrumentSocketIO(this.options || undefined); } } catch {}
103
+
104
+ // --- Phase 3 Instrumentations: Frameworks & Log Correlation ---
105
+ try { if (this.isInstrumentationEnabled('nestjs')) { const { instrumentNestJS } = require('../instrumentation/nestjs'); instrumentNestJS(this.options || undefined); } } catch {}
106
+ try { if (this.isInstrumentationEnabled('hapi')) { const { instrumentHapi } = require('../instrumentation/hapi'); instrumentHapi(this.options || undefined); } } catch {}
107
+ try { if (this.isInstrumentationEnabled('pino')) { const { instrumentPino } = require('../instrumentation/pino'); instrumentPino(this.options || undefined); } } catch {}
108
+ try { if (this.isInstrumentationEnabled('winston')) { const { instrumentWinston } = require('../instrumentation/winston'); instrumentWinston(this.options || undefined); } } catch {}
109
+ try { if (this.isInstrumentationEnabled('bunyan')) { const { instrumentBunyan } = require('../instrumentation/bunyan'); instrumentBunyan(this.options || undefined); } } catch {}
110
+
111
+ // --- Phase 4 Instrumentations: Cloud & Database ---
112
+ try { if (this.isInstrumentationEnabled('aws-sdk')) { const { instrumentAwsSdk } = require('../instrumentation/aws-sdk'); instrumentAwsSdk(this.options || undefined); } } catch {}
113
+ try { if (this.isInstrumentationEnabled('knex')) { const { instrumentKnex } = require('../instrumentation/knex'); instrumentKnex(this.options || undefined); } } catch {}
114
+ try { if (this.isInstrumentationEnabled('tedious')) { const { instrumentTedious } = require('../instrumentation/tedious'); instrumentTedious(this.options || undefined); } } catch {}
115
+ try { if (this.isInstrumentationEnabled('cassandra')) { const { instrumentCassandra } = require('../instrumentation/cassandra'); instrumentCassandra(this.options || undefined); } } catch {}
116
+ try { if (this.isInstrumentationEnabled('memcached')) { const { instrumentMemcached } = require('../instrumentation/memcached'); instrumentMemcached(this.options || undefined); } } catch {}
117
+ try { if (this.isInstrumentationEnabled('generic-pool')) { const { instrumentGenericPool } = require('../instrumentation/generic-pool'); instrumentGenericPool(this.options || undefined); } } catch {}
118
+
119
+ // --- Phase 5 Instrumentations: Frameworks, Utilities & AI ---
120
+ try { if (this.isInstrumentationEnabled('restify')) { const { instrumentRestify } = require('../instrumentation/restify'); instrumentRestify(this.options || undefined); } } catch {}
121
+ try { if (this.isInstrumentationEnabled('connect')) { const { instrumentConnect } = require('../instrumentation/connect'); instrumentConnect(this.options || undefined); } } catch {}
122
+ try { if (this.isInstrumentationEnabled('dataloader')) { const { instrumentDataloader } = require('../instrumentation/dataloader'); instrumentDataloader(this.options || undefined); } } catch {}
123
+ try { if (this.isInstrumentationEnabled('lru-memoizer')) { const { instrumentLruMemoizer } = require('../instrumentation/lru-memoizer'); instrumentLruMemoizer(this.options || undefined); } } catch {}
124
+ try { if (this.isInstrumentationEnabled('fs')) { const { instrumentFs } = require('../instrumentation/fs'); instrumentFs(this.options || undefined); } } catch {}
125
+ try { if (this.isInstrumentationEnabled('openai')) { const { instrumentOpenAI } = require('../instrumentation/openai'); instrumentOpenAI(this.options || undefined); } } catch {}
126
+
127
+ // --- Phase 6 Instrumentations: AI SDKs & Firebase ---
128
+ try { if (this.isInstrumentationEnabled('anthropic')) { const { instrumentAnthropic } = require('../instrumentation/anthropic'); instrumentAnthropic(this.options || undefined); } } catch {}
129
+ try { if (this.isInstrumentationEnabled('google-genai')) { const { instrumentGoogleGenAI } = require('../instrumentation/google-genai'); instrumentGoogleGenAI(this.options || undefined); } } catch {}
130
+ try { if (this.isInstrumentationEnabled('azure-openai')) { const { instrumentAzureOpenAI } = require('../instrumentation/azure-openai'); instrumentAzureOpenAI(this.options || undefined); } } catch {}
131
+ try { if (this.isInstrumentationEnabled('cohere')) { const { instrumentCohere } = require('../instrumentation/cohere'); instrumentCohere(this.options || undefined); } } catch {}
132
+ try { if (this.isInstrumentationEnabled('mistral')) { const { instrumentMistral } = require('../instrumentation/mistral'); instrumentMistral(this.options || undefined); } } catch {}
133
+ try { if (this.isInstrumentationEnabled('firebase')) { const { instrumentFirebase } = require('../instrumentation/firebase'); instrumentFirebase(this.options || undefined); } } catch {}
134
+
135
+ // --- Runtime Metrics ---
136
+ if (this.options?.runtimeMetrics !== false && this.transport) {
137
+ try {
138
+ this.runtimeMetricsCollector = new RuntimeMetricsCollector({
139
+ interval: this.options?.runtimeMetricsInterval ?? 15000,
140
+ onMetrics: (payload) => {
141
+ this.transport?.addRuntimeMetrics(payload);
142
+ },
143
+ });
144
+ this.runtimeMetricsCollector.start();
145
+ } catch {}
146
+ }
90
147
  }
91
148
 
92
149
  this.isInstrumented = true;
@@ -1,10 +1,12 @@
1
1
  import { SENZOR_INTERNAL_HEADER } from '../utils/internal';
2
2
  import { SenzorOptions, Trace, TaskRun, SenzorError, SenzorLog } from './types';
3
+ import type { RuntimeMetricsPayload } from '../instrumentation/runtime';
3
4
 
4
5
  interface ApmPayload {
5
6
  traces: Trace[];
6
7
  errors: SenzorError[];
7
8
  logs: SenzorLog[];
9
+ runtimeMetrics?: RuntimeMetricsPayload[];
8
10
  }
9
11
 
10
12
  interface TaskPayload {
@@ -17,6 +19,7 @@ export class Transport {
17
19
  private traceQueue: Trace[] = [];
18
20
  private apmErrorQueue: SenzorError[] = [];
19
21
  private apmLogQueue: SenzorLog[] = [];
22
+ private runtimeMetricsQueue: RuntimeMetricsPayload[] = [];
20
23
 
21
24
  private taskQueue: TaskRun[] = [];
22
25
  private taskErrorQueue: SenzorError[] = [];
@@ -89,6 +92,11 @@ export class Transport {
89
92
  this.checkFlush();
90
93
  }
91
94
 
95
+ public addRuntimeMetrics(payload: RuntimeMetricsPayload) {
96
+ this.enqueue(this.runtimeMetricsQueue, payload);
97
+ // Runtime metrics don't trigger immediate flush — they ride the next timer
98
+ }
99
+
92
100
  private enqueue<T>(queue: T[], item: T) {
93
101
  this.ensureTimer();
94
102
  queue.push(item);
@@ -130,12 +138,17 @@ export class Transport {
130
138
  }
131
139
 
132
140
  private takeApmPayload(): ApmPayload {
133
- const payload = {
141
+ const payload: ApmPayload = {
134
142
  traces: this.traceQueue,
135
143
  errors: this.apmErrorQueue,
136
- logs: this.apmLogQueue
144
+ logs: this.apmLogQueue,
137
145
  };
138
146
 
147
+ if (this.runtimeMetricsQueue.length > 0) {
148
+ payload.runtimeMetrics = this.runtimeMetricsQueue;
149
+ this.runtimeMetricsQueue = [];
150
+ }
151
+
139
152
  this.traceQueue = [];
140
153
  this.apmErrorQueue = [];
141
154
  this.apmLogQueue = [];
@@ -159,6 +172,9 @@ export class Transport {
159
172
  this.prependWithLimit(this.apmLogQueue, payload.logs);
160
173
  this.prependWithLimit(this.apmErrorQueue, payload.errors);
161
174
  this.prependWithLimit(this.traceQueue, payload.traces);
175
+ if (payload.runtimeMetrics) {
176
+ this.prependWithLimit(this.runtimeMetricsQueue, payload.runtimeMetrics);
177
+ }
162
178
  }
163
179
 
164
180
  private restoreTaskPayload(payload: TaskPayload) {
@@ -171,7 +187,8 @@ export class Transport {
171
187
  return (
172
188
  payload.traces.length > 0 ||
173
189
  payload.errors.length > 0 ||
174
- payload.logs.length > 0
190
+ payload.logs.length > 0 ||
191
+ (payload.runtimeMetrics?.length ?? 0) > 0
175
192
  );
176
193
  }
177
194
 
package/src/core/types.ts CHANGED
@@ -18,13 +18,17 @@ export interface SenzorOptions {
18
18
  ignoreFrameworkSpanTypes?: string[];
19
19
  debug?: boolean;
20
20
  autoLogs?: boolean;
21
+ /** Enable runtime metrics collection (event loop, GC, heap). Default: true */
22
+ runtimeMetrics?: boolean;
23
+ /** Runtime metrics collection interval in milliseconds. Default: 15000 */
24
+ runtimeMetricsInterval?: number;
21
25
  }
22
26
 
23
27
  export interface Span {
24
28
  spanId: string;
25
29
  parentSpanId?: string;
26
30
  name: string;
27
- type: 'db' | 'http' | 'function' | 'custom';
31
+ type: 'db' | 'http' | 'function' | 'custom' | 'rpc' | 'messaging' | 'dns' | 'net';
28
32
  startTime: number;
29
33
  duration: number;
30
34
  status?: number;
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import { expressMiddleware, expressErrorHandler } from './middleware/express';
3
3
  import { wrapH3 } from './wrappers/h3';
4
4
  import { wrapNextRoute, wrapNextPages } from './wrappers/next';
5
5
  import { wrapWorker } from './wrappers/worker';
6
+ import { wrapLambda } from './wrappers/lambda';
6
7
  import { nitroPlugin } from './wrappers/nitro';
7
8
  import { senzorPlugin } from './wrappers/fastify';
8
9
  import { SenzorOptions } from './core/types';
@@ -36,6 +37,9 @@ const Senzor = {
36
37
  // Cloudflare Workers
37
38
  worker: wrapWorker,
38
39
 
40
+ // AWS Lambda
41
+ wrapLambda,
42
+
39
43
  // Nitro / Nuxt
40
44
  nitroPlugin
41
45
  };
@@ -0,0 +1,371 @@
1
+ import { Context } from '../core/context';
2
+ import { SenzorOptions } from '../core/types';
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
+ // RabbitMQ (amqplib) Instrumentation
10
+ //
11
+ // Instruments the amqplib library:
12
+ // - Channel.publish() — outbound publish spans
13
+ // - Channel.sendToQueue() — outbound send spans (shorthand for publish)
14
+ // - Channel.consume() — wraps consumer callback with receive spans
15
+ //
16
+ // Context propagation via message headers (msg.properties.headers).
17
+ //
18
+ // ConfirmChannel inherits from Channel, so patches propagate automatically.
19
+ //
20
+ // Follows OTel messaging semantic conventions:
21
+ // messaging.system = rabbitmq
22
+ // messaging.destination.name = exchange or queue
23
+ // messaging.operation.name = publish | receive
24
+ // messaging.rabbitmq.routing_key
25
+ // messaging.rabbitmq.delivery_tag
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const TRACEPARENT_KEY = 'traceparent';
29
+ const SENZOR_TRACE_KEY = 'x-senzor-trace-id';
30
+ const SENZOR_SPAN_KEY = 'x-senzor-parent-span-id';
31
+
32
+ /** Inject trace context into AMQP message headers. */
33
+ const injectHeaders = (
34
+ options: any,
35
+ traceId: string,
36
+ spanId: string
37
+ ): any => {
38
+ const opts = options ? { ...options } : {};
39
+ const headers = opts.headers ? { ...opts.headers } : {};
40
+ headers[TRACEPARENT_KEY] = generateTraceparent(traceId, spanId);
41
+ headers[SENZOR_TRACE_KEY] = traceId;
42
+ headers[SENZOR_SPAN_KEY] = spanId;
43
+ opts.headers = headers;
44
+ return opts;
45
+ };
46
+
47
+ /** Extract trace context from incoming AMQP message properties. */
48
+ const extractHeaders = (
49
+ msg: any
50
+ ): { traceId?: string; parentSpanId?: string } => {
51
+ const headers = msg?.properties?.headers;
52
+ if (!headers) return {};
53
+
54
+ try {
55
+ const tp = headers[TRACEPARENT_KEY];
56
+ if (tp) {
57
+ const tpStr = Buffer.isBuffer(tp) ? tp.toString() : String(tp);
58
+ const parsed = parseTraceparent(tpStr);
59
+ if (parsed) return parsed;
60
+ }
61
+
62
+ const traceId = headers[SENZOR_TRACE_KEY];
63
+ const spanId = headers[SENZOR_SPAN_KEY];
64
+ return {
65
+ traceId: traceId ? (Buffer.isBuffer(traceId) ? traceId.toString() : String(traceId)) : undefined,
66
+ parentSpanId: spanId ? (Buffer.isBuffer(spanId) ? spanId.toString() : String(spanId)) : undefined,
67
+ };
68
+ } catch {
69
+ return {};
70
+ }
71
+ };
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Channel patching
75
+ // ---------------------------------------------------------------------------
76
+
77
+ const patchChannel = (channelProto: any, options?: SenzorOptions) => {
78
+ if (!channelProto) return;
79
+
80
+ // --- publish(exchange, routingKey, content, options?) ---
81
+ patchMethod(
82
+ channelProto,
83
+ 'publish',
84
+ 'senzor.amqplib.channel.publish',
85
+ (original) =>
86
+ function patchedPublish(
87
+ this: any,
88
+ exchange: string,
89
+ routingKey: string,
90
+ content: Buffer,
91
+ publishOptions?: any
92
+ ) {
93
+ const trace = Context.current();
94
+ if (!trace) return original.call(this, exchange, routingKey, content, publishOptions);
95
+
96
+ const destination = exchange || routingKey || 'default';
97
+
98
+ const span = startCapturedSpan(
99
+ `RabbitMQ publish ${destination}`,
100
+ 'messaging',
101
+ {
102
+ 'messaging.system': 'rabbitmq',
103
+ 'messaging.destination.name': destination,
104
+ 'messaging.operation.name': 'publish',
105
+ 'messaging.rabbitmq.exchange': exchange || '(default)',
106
+ 'messaging.rabbitmq.routing_key': routingKey,
107
+ 'messaging.message.body_size': content?.length || 0,
108
+ },
109
+ options
110
+ );
111
+
112
+ if (!span) return original.call(this, exchange, routingKey, content, publishOptions);
113
+
114
+ // Inject trace context into headers
115
+ const enrichedOptions = injectHeaders(publishOptions, trace.id, span.spanId);
116
+
117
+ return runWithCapturedSpan(span, () => {
118
+ try {
119
+ const result = original.call(this, exchange, routingKey, content, enrichedOptions);
120
+
121
+ // publish returns boolean (backpressure signal), not a promise
122
+ span.end(0, {
123
+ 'messaging.rabbitmq.backpressure': result === false,
124
+ });
125
+
126
+ return result;
127
+ } catch (error: any) {
128
+ span.end(500, {
129
+ 'error.message': error?.message,
130
+ 'error.type': error?.name || 'AmqpError',
131
+ });
132
+ throw error;
133
+ }
134
+ });
135
+ }
136
+ );
137
+
138
+ // --- sendToQueue(queue, content, options?) ---
139
+ patchMethod(
140
+ channelProto,
141
+ 'sendToQueue',
142
+ 'senzor.amqplib.channel.sendToQueue',
143
+ (original) =>
144
+ function patchedSendToQueue(
145
+ this: any,
146
+ queue: string,
147
+ content: Buffer,
148
+ sendOptions?: any
149
+ ) {
150
+ const trace = Context.current();
151
+ if (!trace) return original.call(this, queue, content, sendOptions);
152
+
153
+ const span = startCapturedSpan(
154
+ `RabbitMQ send ${queue}`,
155
+ 'messaging',
156
+ {
157
+ 'messaging.system': 'rabbitmq',
158
+ 'messaging.destination.name': queue,
159
+ 'messaging.operation.name': 'publish',
160
+ 'messaging.rabbitmq.routing_key': queue,
161
+ 'messaging.message.body_size': content?.length || 0,
162
+ },
163
+ options
164
+ );
165
+
166
+ if (!span) return original.call(this, queue, content, sendOptions);
167
+
168
+ const enrichedOptions = injectHeaders(sendOptions, trace.id, span.spanId);
169
+
170
+ return runWithCapturedSpan(span, () => {
171
+ try {
172
+ const result = original.call(this, queue, content, enrichedOptions);
173
+ span.end(0, {
174
+ 'messaging.rabbitmq.backpressure': result === false,
175
+ });
176
+ return result;
177
+ } catch (error: any) {
178
+ span.end(500, {
179
+ 'error.message': error?.message,
180
+ 'error.type': error?.name || 'AmqpError',
181
+ });
182
+ throw error;
183
+ }
184
+ });
185
+ }
186
+ );
187
+
188
+ // --- consume(queue, callback, options?) ---
189
+ patchMethod(
190
+ channelProto,
191
+ 'consume',
192
+ 'senzor.amqplib.channel.consume',
193
+ (original) =>
194
+ function patchedConsume(
195
+ this: any,
196
+ queue: string,
197
+ callback: (msg: any) => void,
198
+ consumeOptions?: any
199
+ ) {
200
+ if (typeof callback !== 'function') {
201
+ return original.call(this, queue, callback, consumeOptions);
202
+ }
203
+
204
+ const wrappedCallback = function (msg: any) {
205
+ // Null msg means consumer was cancelled
206
+ if (!msg) return callback(msg);
207
+
208
+ const parentCtx = extractHeaders(msg);
209
+ const exchange = msg.fields?.exchange || '';
210
+ const routingKey = msg.fields?.routingKey || queue;
211
+ const destination = exchange || routingKey || queue;
212
+
213
+ const span = startCapturedSpan(
214
+ `RabbitMQ receive ${destination}`,
215
+ 'messaging',
216
+ {
217
+ 'messaging.system': 'rabbitmq',
218
+ 'messaging.destination.name': destination,
219
+ 'messaging.operation.name': 'receive',
220
+ 'messaging.rabbitmq.exchange': exchange || '(default)',
221
+ 'messaging.rabbitmq.routing_key': routingKey,
222
+ 'messaging.rabbitmq.delivery_tag': msg.fields?.deliveryTag,
223
+ 'messaging.rabbitmq.redelivered': msg.fields?.redelivered,
224
+ 'messaging.rabbitmq.consumer_tag': msg.fields?.consumerTag,
225
+ 'messaging.message.body_size': msg.content?.length || 0,
226
+ ...(parentCtx.traceId ? { 'messaging.parent_trace_id': parentCtx.traceId } : {}),
227
+ },
228
+ options
229
+ );
230
+
231
+ if (!span) return callback(msg);
232
+
233
+ return runWithCapturedSpan(span, () => {
234
+ try {
235
+ const result = callback(msg);
236
+
237
+ // Handle async consumers (returning promises)
238
+ if (result && typeof (result as any).then === 'function') {
239
+ return (result as any).then(
240
+ (val: any) => {
241
+ span.end(0);
242
+ return val;
243
+ },
244
+ (error: any) => {
245
+ span.end(500, {
246
+ 'error.message': error?.message,
247
+ 'error.type': error?.name || 'Error',
248
+ });
249
+ throw error;
250
+ }
251
+ );
252
+ }
253
+
254
+ span.end(0);
255
+ return result;
256
+ } catch (error: any) {
257
+ span.end(500, {
258
+ 'error.message': error?.message,
259
+ 'error.type': error?.name || 'Error',
260
+ });
261
+ throw error;
262
+ }
263
+ });
264
+ };
265
+
266
+ return original.call(this, queue, wrappedCallback, consumeOptions);
267
+ }
268
+ );
269
+ };
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Module patching strategies
273
+ // ---------------------------------------------------------------------------
274
+
275
+ /**
276
+ * Strategy 1: Patch Channel prototypes from the internal module structure.
277
+ * amqplib/lib/channel_model exports Channel and ConfirmChannel.
278
+ */
279
+ const patchFromInternals = (amqplib: any, options?: SenzorOptions) => {
280
+ // Try to reach Channel prototype via internal module
281
+ try {
282
+ const channelModel = require('amqplib/lib/channel_model');
283
+ if (channelModel?.Channel?.prototype) {
284
+ patchChannel(channelModel.Channel.prototype, options);
285
+ }
286
+ if (channelModel?.ConfirmChannel?.prototype) {
287
+ patchChannel(channelModel.ConfirmChannel.prototype, options);
288
+ }
289
+ } catch { }
290
+ };
291
+
292
+ /**
293
+ * Strategy 2: Wrap createChannel/createConfirmChannel to patch returned instances.
294
+ * Works even if internal structure changes between versions.
295
+ */
296
+ const patchConnectionFactory = (amqplib: any, options?: SenzorOptions) => {
297
+ // Wrap amqplib.connect to intercept the connection object
298
+ patchMethod(
299
+ amqplib,
300
+ 'connect',
301
+ 'senzor.amqplib.connect',
302
+ (original) =>
303
+ function patchedConnect(this: any, ...args: any[]) {
304
+ const result = original.apply(this, args);
305
+
306
+ if (result && typeof result.then === 'function') {
307
+ return result.then((connection: any) => {
308
+ if (!connection) return connection;
309
+
310
+ // Wrap createChannel
311
+ patchMethod(
312
+ connection,
313
+ 'createChannel',
314
+ 'senzor.amqplib.createChannel',
315
+ (origCreate) =>
316
+ function patchedCreateChannel(this: any, ...createArgs: any[]) {
317
+ const chResult = origCreate.apply(this, createArgs);
318
+ if (chResult && typeof chResult.then === 'function') {
319
+ return chResult.then((channel: any) => {
320
+ if (channel) patchChannel(channel, options);
321
+ return channel;
322
+ });
323
+ }
324
+ if (chResult) patchChannel(chResult, options);
325
+ return chResult;
326
+ }
327
+ );
328
+
329
+ // Wrap createConfirmChannel
330
+ patchMethod(
331
+ connection,
332
+ 'createConfirmChannel',
333
+ 'senzor.amqplib.createConfirmChannel',
334
+ (origCreate) =>
335
+ function patchedCreateConfirmChannel(this: any, ...createArgs: any[]) {
336
+ const chResult = origCreate.apply(this, createArgs);
337
+ if (chResult && typeof chResult.then === 'function') {
338
+ return chResult.then((channel: any) => {
339
+ if (channel) patchChannel(channel, options);
340
+ return channel;
341
+ });
342
+ }
343
+ if (chResult) patchChannel(chResult, options);
344
+ return chResult;
345
+ }
346
+ );
347
+
348
+ return connection;
349
+ });
350
+ }
351
+
352
+ return result;
353
+ }
354
+ );
355
+ };
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Public API
359
+ // ---------------------------------------------------------------------------
360
+
361
+ export const instrumentAmqplib = (options?: SenzorOptions) => {
362
+ hookRequire('amqplib', (exports: any) => {
363
+ patchFromInternals(exports, options);
364
+ patchConnectionFactory(exports, options);
365
+ });
366
+
367
+ // Also hook the callback API variant
368
+ hookRequire('amqplib/callback_api', (exports: any) => {
369
+ patchConnectionFactory(exports, options);
370
+ });
371
+ };