@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senzops/apm-node",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Universal APM SDK for Senzor",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
"types": "./dist/register.d.ts",
|
|
16
16
|
"require": "./dist/register.js",
|
|
17
17
|
"import": "./dist/register.mjs"
|
|
18
|
+
},
|
|
19
|
+
"./lambda-handler": {
|
|
20
|
+
"types": "./dist/lambda-handler.d.ts",
|
|
21
|
+
"require": "./dist/lambda-handler.js",
|
|
22
|
+
"import": "./dist/lambda-handler.mjs"
|
|
18
23
|
}
|
|
19
24
|
},
|
|
20
25
|
"scripts": {
|
package/src/core/client.ts
CHANGED
|
@@ -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;
|
package/src/core/transport.ts
CHANGED
|
@@ -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
|
+
};
|