@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
|
@@ -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
|
+
};
|