@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.
- package/CHANGELOG.md +9 -0
- package/README.md +479 -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/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 +1 -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/register.ts +22 -3
- package/src/wrappers/lambda.ts +417 -0
- package/tsup.config.ts +3 -3
- package/wiki.md +1547 -852
|
@@ -0,0 +1,257 @@
|
|
|
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
|
+
// Hapi Instrumentation
|
|
8
|
+
//
|
|
9
|
+
// Instruments @hapi/hapi at the route registration layer:
|
|
10
|
+
// - Server.prototype.route() — wraps route handlers at registration time
|
|
11
|
+
// so every request generates a span with:
|
|
12
|
+
// hapi.route, hapi.method, http.route
|
|
13
|
+
//
|
|
14
|
+
// Captures the full handler execution including any route-level validation,
|
|
15
|
+
// payload parsing, and response formatting done by Hapi.
|
|
16
|
+
//
|
|
17
|
+
// The handler signature is always: (request, h) => response
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/** HTTP method enum to string mapping for older Hapi versions. */
|
|
21
|
+
const METHOD_MAP: Record<number, string> = {
|
|
22
|
+
0: 'GET', 1: 'HEAD', 2: 'POST', 3: 'PUT',
|
|
23
|
+
4: 'DELETE', 5: 'OPTIONS', 6: 'PATCH',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const normalizeMethod = (method: any): string => {
|
|
27
|
+
if (typeof method === 'string') return method.toUpperCase();
|
|
28
|
+
if (typeof method === 'number') return METHOD_MAP[method] || 'UNKNOWN';
|
|
29
|
+
return 'UNKNOWN';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Handler wrapping
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const wrapHandler = (
|
|
37
|
+
handler: Function,
|
|
38
|
+
routePath: string,
|
|
39
|
+
routeMethod: string,
|
|
40
|
+
options?: SenzorOptions
|
|
41
|
+
): Function => {
|
|
42
|
+
if (typeof handler !== 'function') return handler;
|
|
43
|
+
|
|
44
|
+
return function wrappedHapiHandler(this: any, request: any, h: any) {
|
|
45
|
+
const method = normalizeMethod(routeMethod);
|
|
46
|
+
const path = routePath || request?.route?.path || request?.path || '/';
|
|
47
|
+
|
|
48
|
+
const span = startCapturedSpan(
|
|
49
|
+
`Hapi ${method} ${path}`,
|
|
50
|
+
'function',
|
|
51
|
+
{
|
|
52
|
+
'hapi.type': 'route_handler',
|
|
53
|
+
'hapi.route': path,
|
|
54
|
+
'hapi.method': method,
|
|
55
|
+
'http.route': path,
|
|
56
|
+
framework: 'hapi',
|
|
57
|
+
},
|
|
58
|
+
options
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (!span) return handler.call(this, request, h);
|
|
62
|
+
|
|
63
|
+
return runWithCapturedSpan(span, () => {
|
|
64
|
+
try {
|
|
65
|
+
const result = handler.call(this, request, h);
|
|
66
|
+
|
|
67
|
+
if (result && typeof result.then === 'function') {
|
|
68
|
+
return result.then(
|
|
69
|
+
(val: any) => {
|
|
70
|
+
const statusCode = val?.statusCode || request?.response?.statusCode || 200;
|
|
71
|
+
span.end(statusCode >= 400 ? statusCode : 0, {
|
|
72
|
+
'http.response.status_code': statusCode,
|
|
73
|
+
});
|
|
74
|
+
return val;
|
|
75
|
+
},
|
|
76
|
+
(error: any) => {
|
|
77
|
+
const statusCode = error?.output?.statusCode || 500;
|
|
78
|
+
span.end(statusCode, {
|
|
79
|
+
'error.message': error?.message,
|
|
80
|
+
'error.type': error?.name || 'Error',
|
|
81
|
+
'http.response.status_code': statusCode,
|
|
82
|
+
});
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
span.end(0);
|
|
89
|
+
return result;
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
const statusCode = error?.output?.statusCode || 500;
|
|
92
|
+
span.end(statusCode, {
|
|
93
|
+
'error.message': error?.message,
|
|
94
|
+
'error.type': error?.name || 'Error',
|
|
95
|
+
'http.response.status_code': statusCode,
|
|
96
|
+
});
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Route config processing
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Process a single route configuration object and wrap its handler.
|
|
109
|
+
* Hapi route configs can have handler at config.handler or config.options.handler.
|
|
110
|
+
*/
|
|
111
|
+
const processRouteConfig = (config: any, options?: SenzorOptions): any => {
|
|
112
|
+
if (!config) return config;
|
|
113
|
+
|
|
114
|
+
const method = config.method || 'GET';
|
|
115
|
+
const path = config.path || '/';
|
|
116
|
+
|
|
117
|
+
// Clone config to avoid mutating user's original object
|
|
118
|
+
const wrapped = { ...config };
|
|
119
|
+
|
|
120
|
+
// Handler can be at config.handler or config.options.handler
|
|
121
|
+
if (typeof wrapped.handler === 'function') {
|
|
122
|
+
wrapped.handler = wrapHandler(wrapped.handler, path, method, options);
|
|
123
|
+
} else if (wrapped.options && typeof wrapped.options.handler === 'function') {
|
|
124
|
+
wrapped.options = { ...wrapped.options };
|
|
125
|
+
wrapped.options.handler = wrapHandler(wrapped.options.handler, path, method, options);
|
|
126
|
+
} else if (wrapped.config && typeof wrapped.config.handler === 'function') {
|
|
127
|
+
// Legacy Hapi config location
|
|
128
|
+
wrapped.config = { ...wrapped.config };
|
|
129
|
+
wrapped.config.handler = wrapHandler(wrapped.config.handler, path, method, options);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return wrapped;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Server.route() patching
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
const patchServerRoute = (serverProto: any, options?: SenzorOptions) => {
|
|
140
|
+
if (!serverProto) return;
|
|
141
|
+
|
|
142
|
+
patchMethod(
|
|
143
|
+
serverProto,
|
|
144
|
+
'route',
|
|
145
|
+
'senzor.hapi.server.route',
|
|
146
|
+
(original) =>
|
|
147
|
+
function patchedRoute(this: any, routeConfig: any) {
|
|
148
|
+
// server.route() accepts a single config or an array of configs
|
|
149
|
+
if (Array.isArray(routeConfig)) {
|
|
150
|
+
const wrappedConfigs = routeConfig.map((config: any) =>
|
|
151
|
+
processRouteConfig(config, options)
|
|
152
|
+
);
|
|
153
|
+
return original.call(this, wrappedConfigs);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const wrappedConfig = processRouteConfig(routeConfig, options);
|
|
157
|
+
return original.call(this, wrappedConfig);
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Extension point patching (for lifecycle span visibility)
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
const patchServerExt = (serverProto: any, options?: SenzorOptions) => {
|
|
167
|
+
if (!serverProto) return;
|
|
168
|
+
|
|
169
|
+
patchMethod(
|
|
170
|
+
serverProto,
|
|
171
|
+
'ext',
|
|
172
|
+
'senzor.hapi.server.ext',
|
|
173
|
+
(original) =>
|
|
174
|
+
function patchedExt(this: any, event: any, method?: any, extOptions?: any) {
|
|
175
|
+
// ext() can be called as:
|
|
176
|
+
// ext(event, method, options) — single extension
|
|
177
|
+
// ext([{ type, method, options }]) — array of extensions
|
|
178
|
+
// ext({ type, method, options }) — single object
|
|
179
|
+
|
|
180
|
+
if (typeof event === 'string' && typeof method === 'function') {
|
|
181
|
+
const eventName = event;
|
|
182
|
+
const originalMethod = method;
|
|
183
|
+
|
|
184
|
+
const wrappedMethod = function (this: any, request: any, h: any) {
|
|
185
|
+
const span = startCapturedSpan(
|
|
186
|
+
`Hapi ext ${eventName}`,
|
|
187
|
+
'function',
|
|
188
|
+
{
|
|
189
|
+
'hapi.type': 'lifecycle_hook',
|
|
190
|
+
'hapi.hook': eventName,
|
|
191
|
+
framework: 'hapi',
|
|
192
|
+
},
|
|
193
|
+
options
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (!span) return originalMethod.call(this, request, h);
|
|
197
|
+
|
|
198
|
+
return runWithCapturedSpan(span, () => {
|
|
199
|
+
try {
|
|
200
|
+
const result = originalMethod.call(this, request, h);
|
|
201
|
+
if (result && typeof result.then === 'function') {
|
|
202
|
+
return result.then(
|
|
203
|
+
(val: any) => { span.end(0); return val; },
|
|
204
|
+
(err: any) => {
|
|
205
|
+
span.end(500, { 'error.message': err?.message });
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
span.end(0);
|
|
211
|
+
return result;
|
|
212
|
+
} catch (err: any) {
|
|
213
|
+
span.end(500, { 'error.message': err?.message });
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return original.call(this, event, wrappedMethod, extOptions);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// For array/object form, pass through unchanged (avoid complex nesting)
|
|
223
|
+
return original.call(this, event, method, extOptions);
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Public API
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
export const instrumentHapi = (options?: SenzorOptions) => {
|
|
233
|
+
hookRequire('@hapi/hapi', (exports: any) => {
|
|
234
|
+
// Hapi exports a Server class
|
|
235
|
+
const Server = exports?.Server || exports?.server?.Server;
|
|
236
|
+
|
|
237
|
+
if (Server?.prototype) {
|
|
238
|
+
patchServerRoute(Server.prototype, options);
|
|
239
|
+
|
|
240
|
+
// Only patch ext if framework spans are enabled
|
|
241
|
+
if (options?.frameworkSpans !== false) {
|
|
242
|
+
patchServerExt(Server.prototype, options);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Also try the legacy 'hapi' package name (pre-scoped)
|
|
248
|
+
hookRequire('hapi', (exports: any) => {
|
|
249
|
+
const Server = exports?.Server || exports?.server?.Server;
|
|
250
|
+
if (Server?.prototype) {
|
|
251
|
+
patchServerRoute(Server.prototype, options);
|
|
252
|
+
if (options?.frameworkSpans !== false) {
|
|
253
|
+
patchServerExt(Server.prototype, options);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
};
|
|
@@ -0,0 +1,360 @@
|
|
|
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
|
+
// Kafka (kafkajs) Instrumentation
|
|
10
|
+
//
|
|
11
|
+
// Instruments the kafkajs library:
|
|
12
|
+
// - Producer: send(), sendBatch() — outbound publish spans
|
|
13
|
+
// - Consumer: run() — wraps eachMessage/eachBatch callbacks with spans
|
|
14
|
+
// - Context propagation via message headers (traceparent, x-senzor-*)
|
|
15
|
+
//
|
|
16
|
+
// Follows OTel messaging semantic conventions:
|
|
17
|
+
// messaging.system = kafka
|
|
18
|
+
// messaging.destination.name = topic
|
|
19
|
+
// messaging.operation.name = publish | receive
|
|
20
|
+
// messaging.kafka.consumer.group
|
|
21
|
+
// messaging.kafka.message.offset
|
|
22
|
+
// messaging.batch.message_count
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const TRACEPARENT_KEY = 'traceparent';
|
|
26
|
+
const SENZOR_TRACE_KEY = 'x-senzor-trace-id';
|
|
27
|
+
const SENZOR_SPAN_KEY = 'x-senzor-parent-span-id';
|
|
28
|
+
|
|
29
|
+
/** Inject trace context into Kafka message headers. */
|
|
30
|
+
const injectHeaders = (
|
|
31
|
+
headers: Record<string, any> | undefined,
|
|
32
|
+
traceId: string,
|
|
33
|
+
spanId: string
|
|
34
|
+
): Record<string, any> => {
|
|
35
|
+
const h = headers ? { ...headers } : {};
|
|
36
|
+
h[TRACEPARENT_KEY] = Buffer.from(generateTraceparent(traceId, spanId));
|
|
37
|
+
h[SENZOR_TRACE_KEY] = Buffer.from(traceId);
|
|
38
|
+
h[SENZOR_SPAN_KEY] = Buffer.from(spanId);
|
|
39
|
+
return h;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Extract trace context from incoming Kafka message headers. */
|
|
43
|
+
const extractHeaders = (
|
|
44
|
+
headers: Record<string, any> | undefined
|
|
45
|
+
): { traceId?: string; parentSpanId?: string } => {
|
|
46
|
+
if (!headers) return {};
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const tp = headers[TRACEPARENT_KEY];
|
|
50
|
+
if (tp) {
|
|
51
|
+
const tpStr = Buffer.isBuffer(tp) ? tp.toString() : String(tp);
|
|
52
|
+
const parsed = parseTraceparent(tpStr);
|
|
53
|
+
if (parsed) return parsed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const traceId = headers[SENZOR_TRACE_KEY];
|
|
57
|
+
const spanId = headers[SENZOR_SPAN_KEY];
|
|
58
|
+
return {
|
|
59
|
+
traceId: traceId ? (Buffer.isBuffer(traceId) ? traceId.toString() : String(traceId)) : undefined,
|
|
60
|
+
parentSpanId: spanId ? (Buffer.isBuffer(spanId) ? spanId.toString() : String(spanId)) : undefined,
|
|
61
|
+
};
|
|
62
|
+
} catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Producer patching
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
const patchProducer = (producer: any, options?: SenzorOptions) => {
|
|
72
|
+
// send({ topic, messages, ... })
|
|
73
|
+
patchMethod(
|
|
74
|
+
producer,
|
|
75
|
+
'send',
|
|
76
|
+
'senzor.kafka.producer.send',
|
|
77
|
+
(original) =>
|
|
78
|
+
function patchedSend(this: any, payload: any) {
|
|
79
|
+
const trace = Context.current();
|
|
80
|
+
if (!trace) return original.call(this, payload);
|
|
81
|
+
|
|
82
|
+
const topic = payload?.topic || 'unknown';
|
|
83
|
+
const messageCount = payload?.messages?.length || 0;
|
|
84
|
+
|
|
85
|
+
const span = startCapturedSpan(
|
|
86
|
+
`Kafka publish ${topic}`,
|
|
87
|
+
'messaging',
|
|
88
|
+
{
|
|
89
|
+
'messaging.system': 'kafka',
|
|
90
|
+
'messaging.destination.name': topic,
|
|
91
|
+
'messaging.operation.name': 'publish',
|
|
92
|
+
'messaging.batch.message_count': messageCount,
|
|
93
|
+
},
|
|
94
|
+
options
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!span) return original.call(this, payload);
|
|
98
|
+
|
|
99
|
+
// Inject trace context into each message's headers
|
|
100
|
+
if (payload?.messages && Array.isArray(payload.messages)) {
|
|
101
|
+
payload = {
|
|
102
|
+
...payload,
|
|
103
|
+
messages: payload.messages.map((msg: any) => ({
|
|
104
|
+
...msg,
|
|
105
|
+
headers: injectHeaders(msg.headers, trace.id, span.spanId),
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return runWithCapturedSpan(span, () => {
|
|
111
|
+
const result = original.call(this, payload);
|
|
112
|
+
|
|
113
|
+
if (result && typeof result.then === 'function') {
|
|
114
|
+
return result.then(
|
|
115
|
+
(res: any) => {
|
|
116
|
+
span.end(0, {
|
|
117
|
+
'messaging.kafka.partitions': Array.isArray(res)
|
|
118
|
+
? res.map((r: any) => r.partition).join(',')
|
|
119
|
+
: undefined,
|
|
120
|
+
});
|
|
121
|
+
return res;
|
|
122
|
+
},
|
|
123
|
+
(error: any) => {
|
|
124
|
+
span.end(500, {
|
|
125
|
+
'error.message': error?.message,
|
|
126
|
+
'error.type': error?.name || 'KafkaError',
|
|
127
|
+
});
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
span.end(0);
|
|
134
|
+
return result;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// sendBatch({ topicMessages: [{ topic, messages }], ... })
|
|
140
|
+
patchMethod(
|
|
141
|
+
producer,
|
|
142
|
+
'sendBatch',
|
|
143
|
+
'senzor.kafka.producer.sendBatch',
|
|
144
|
+
(original) =>
|
|
145
|
+
function patchedSendBatch(this: any, payload: any) {
|
|
146
|
+
const trace = Context.current();
|
|
147
|
+
if (!trace) return original.call(this, payload);
|
|
148
|
+
|
|
149
|
+
const topicMessages = payload?.topicMessages || [];
|
|
150
|
+
const topics = topicMessages.map((tm: any) => tm.topic).join(',');
|
|
151
|
+
const totalMessages = topicMessages.reduce(
|
|
152
|
+
(sum: number, tm: any) => sum + (tm.messages?.length || 0),
|
|
153
|
+
0
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const span = startCapturedSpan(
|
|
157
|
+
`Kafka publishBatch ${topics}`,
|
|
158
|
+
'messaging',
|
|
159
|
+
{
|
|
160
|
+
'messaging.system': 'kafka',
|
|
161
|
+
'messaging.destination.name': topics,
|
|
162
|
+
'messaging.operation.name': 'publish',
|
|
163
|
+
'messaging.batch.message_count': totalMessages,
|
|
164
|
+
'messaging.kafka.batch_topic_count': topicMessages.length,
|
|
165
|
+
},
|
|
166
|
+
options
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (!span) return original.call(this, payload);
|
|
170
|
+
|
|
171
|
+
// Inject trace context into all messages
|
|
172
|
+
if (topicMessages.length > 0) {
|
|
173
|
+
payload = {
|
|
174
|
+
...payload,
|
|
175
|
+
topicMessages: topicMessages.map((tm: any) => ({
|
|
176
|
+
...tm,
|
|
177
|
+
messages: (tm.messages || []).map((msg: any) => ({
|
|
178
|
+
...msg,
|
|
179
|
+
headers: injectHeaders(msg.headers, trace.id, span.spanId),
|
|
180
|
+
})),
|
|
181
|
+
})),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return runWithCapturedSpan(span, () => {
|
|
186
|
+
const result = original.call(this, payload);
|
|
187
|
+
|
|
188
|
+
if (result && typeof result.then === 'function') {
|
|
189
|
+
return result.then(
|
|
190
|
+
(res: any) => {
|
|
191
|
+
span.end(0);
|
|
192
|
+
return res;
|
|
193
|
+
},
|
|
194
|
+
(error: any) => {
|
|
195
|
+
span.end(500, {
|
|
196
|
+
'error.message': error?.message,
|
|
197
|
+
'error.type': error?.name || 'KafkaError',
|
|
198
|
+
});
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
span.end(0);
|
|
205
|
+
return result;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Consumer patching
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
const patchConsumer = (consumer: any, options?: SenzorOptions) => {
|
|
216
|
+
patchMethod(
|
|
217
|
+
consumer,
|
|
218
|
+
'run',
|
|
219
|
+
'senzor.kafka.consumer.run',
|
|
220
|
+
(original) =>
|
|
221
|
+
function patchedRun(this: any, config: any) {
|
|
222
|
+
if (!config) return original.call(this, config);
|
|
223
|
+
|
|
224
|
+
const wrappedConfig = { ...config };
|
|
225
|
+
|
|
226
|
+
// Wrap eachMessage
|
|
227
|
+
if (typeof config.eachMessage === 'function') {
|
|
228
|
+
const originalHandler = config.eachMessage;
|
|
229
|
+
wrappedConfig.eachMessage = async (payload: any) => {
|
|
230
|
+
const { topic, partition, message } = payload;
|
|
231
|
+
const parentCtx = extractHeaders(message?.headers);
|
|
232
|
+
|
|
233
|
+
const span = startCapturedSpan(
|
|
234
|
+
`Kafka receive ${topic}`,
|
|
235
|
+
'messaging',
|
|
236
|
+
{
|
|
237
|
+
'messaging.system': 'kafka',
|
|
238
|
+
'messaging.destination.name': topic,
|
|
239
|
+
'messaging.operation.name': 'receive',
|
|
240
|
+
'messaging.kafka.partition': partition,
|
|
241
|
+
'messaging.kafka.message.offset': message?.offset,
|
|
242
|
+
'messaging.kafka.message.key': message?.key?.toString(),
|
|
243
|
+
...(parentCtx.traceId ? { 'messaging.parent_trace_id': parentCtx.traceId } : {}),
|
|
244
|
+
},
|
|
245
|
+
options
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (!span) return originalHandler(payload);
|
|
249
|
+
|
|
250
|
+
return runWithCapturedSpan(span, async () => {
|
|
251
|
+
try {
|
|
252
|
+
const result = await originalHandler(payload);
|
|
253
|
+
span.end(0);
|
|
254
|
+
return result;
|
|
255
|
+
} catch (error: any) {
|
|
256
|
+
span.end(500, {
|
|
257
|
+
'error.message': error?.message,
|
|
258
|
+
'error.type': error?.name || 'Error',
|
|
259
|
+
});
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Wrap eachBatch
|
|
267
|
+
if (typeof config.eachBatch === 'function') {
|
|
268
|
+
const originalBatchHandler = config.eachBatch;
|
|
269
|
+
wrappedConfig.eachBatch = async (payload: any) => {
|
|
270
|
+
const { batch } = payload;
|
|
271
|
+
const topic = batch?.topic || 'unknown';
|
|
272
|
+
const messageCount = batch?.messages?.length || 0;
|
|
273
|
+
|
|
274
|
+
const span = startCapturedSpan(
|
|
275
|
+
`Kafka receiveBatch ${topic}`,
|
|
276
|
+
'messaging',
|
|
277
|
+
{
|
|
278
|
+
'messaging.system': 'kafka',
|
|
279
|
+
'messaging.destination.name': topic,
|
|
280
|
+
'messaging.operation.name': 'receive',
|
|
281
|
+
'messaging.kafka.partition': batch?.partition,
|
|
282
|
+
'messaging.batch.message_count': messageCount,
|
|
283
|
+
'messaging.kafka.first_offset': batch?.messages?.[0]?.offset,
|
|
284
|
+
'messaging.kafka.last_offset': batch?.messages?.[messageCount - 1]?.offset,
|
|
285
|
+
},
|
|
286
|
+
options
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (!span) return originalBatchHandler(payload);
|
|
290
|
+
|
|
291
|
+
return runWithCapturedSpan(span, async () => {
|
|
292
|
+
try {
|
|
293
|
+
const result = await originalBatchHandler(payload);
|
|
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 || 'Error',
|
|
300
|
+
});
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return original.call(this, wrappedConfig);
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Kafka class patching (wraps factory methods)
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
const patchKafkaClass = (kafkaModule: any, options?: SenzorOptions) => {
|
|
317
|
+
const KafkaClass = kafkaModule?.Kafka;
|
|
318
|
+
if (!KafkaClass?.prototype) return;
|
|
319
|
+
|
|
320
|
+
// Wrap producer() factory
|
|
321
|
+
patchMethod(
|
|
322
|
+
KafkaClass.prototype,
|
|
323
|
+
'producer',
|
|
324
|
+
'senzor.kafka.producer',
|
|
325
|
+
(original) =>
|
|
326
|
+
function patchedProducerFactory(this: any, ...args: any[]) {
|
|
327
|
+
const producer = original.apply(this, args);
|
|
328
|
+
if (producer) patchProducer(producer, options);
|
|
329
|
+
return producer;
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// Wrap consumer() factory
|
|
334
|
+
patchMethod(
|
|
335
|
+
KafkaClass.prototype,
|
|
336
|
+
'consumer',
|
|
337
|
+
'senzor.kafka.consumer',
|
|
338
|
+
(original) =>
|
|
339
|
+
function patchedConsumerFactory(this: any, ...args: any[]) {
|
|
340
|
+
const consumer = original.apply(this, args);
|
|
341
|
+
if (consumer) patchConsumer(consumer, options);
|
|
342
|
+
return consumer;
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// Public API
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
export const instrumentKafka = (options?: SenzorOptions) => {
|
|
352
|
+
hookRequire('kafkajs', (exports: any) => {
|
|
353
|
+
patchKafkaClass(exports, options);
|
|
354
|
+
|
|
355
|
+
// Also handle default export
|
|
356
|
+
if (exports?.default?.Kafka) {
|
|
357
|
+
patchKafkaClass(exports.default, options);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
};
|