@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,254 @@
|
|
|
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
|
+
// Mistral AI SDK Instrumentation
|
|
8
|
+
//
|
|
9
|
+
// Instruments the official `@mistralai/mistralai` package.
|
|
10
|
+
//
|
|
11
|
+
// Mistral SDK architecture: The `Mistral` class has resource namespaces
|
|
12
|
+
// (chat, fim, embeddings, classifiers, models, agents, files, etc.) that
|
|
13
|
+
// each have async methods returning promises.
|
|
14
|
+
//
|
|
15
|
+
// The SDK also has an internal `_request` or `_fetch` method on the base
|
|
16
|
+
// client. We patch both the high-level resource methods AND the internal
|
|
17
|
+
// dispatch for full coverage.
|
|
18
|
+
//
|
|
19
|
+
// Patches:
|
|
20
|
+
// - Mistral.prototype methods (post, get, etc.) — internal HTTP dispatch
|
|
21
|
+
// - chat.complete() / chat.stream() — via resource namespaces
|
|
22
|
+
// - embeddings.create()
|
|
23
|
+
// - fim.complete() / fim.stream()
|
|
24
|
+
// - classifiers.moderate() / classifiers.moderateChat()
|
|
25
|
+
//
|
|
26
|
+
// Captured attributes (OTel GenAI semantic conventions):
|
|
27
|
+
// - gen_ai.system: 'mistral'
|
|
28
|
+
// - gen_ai.request.model: mistral-large, codestral, etc.
|
|
29
|
+
// - gen_ai.operation.name: chat, embeddings, fim, classify
|
|
30
|
+
// - gen_ai.usage.input_tokens: prompt tokens
|
|
31
|
+
// - gen_ai.usage.output_tokens: completion tokens
|
|
32
|
+
// - gen_ai.response.model: actual model from response
|
|
33
|
+
// - gen_ai.response.finish_reason: stop, length, etc.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/** Extract token usage from Mistral response. */
|
|
37
|
+
const extractMistralUsage = (result: any): Record<string, any> => {
|
|
38
|
+
const meta: Record<string, any> = {};
|
|
39
|
+
|
|
40
|
+
if (result?.usage) {
|
|
41
|
+
meta['gen_ai.usage.input_tokens'] = result.usage.promptTokens ?? result.usage.prompt_tokens;
|
|
42
|
+
meta['gen_ai.usage.output_tokens'] = result.usage.completionTokens ?? result.usage.completion_tokens;
|
|
43
|
+
meta['gen_ai.usage.total_tokens'] = result.usage.totalTokens ?? result.usage.total_tokens;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (result?.model) {
|
|
47
|
+
meta['gen_ai.response.model'] = result.model;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const finishReason = result?.choices?.[0]?.finishReason
|
|
51
|
+
?? result?.choices?.[0]?.finish_reason;
|
|
52
|
+
if (finishReason) {
|
|
53
|
+
meta['gen_ai.response.finish_reason'] = finishReason;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return meta;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Resource method patching
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
/** Patch a specific method on a resource namespace object. */
|
|
64
|
+
const patchResourceMethod = (
|
|
65
|
+
resource: any,
|
|
66
|
+
methodName: string,
|
|
67
|
+
operation: string,
|
|
68
|
+
getModel: (args: any[]) => string | undefined,
|
|
69
|
+
extractUsage: (result: any) => Record<string, any>,
|
|
70
|
+
patchKey: string,
|
|
71
|
+
options?: SenzorOptions
|
|
72
|
+
) => {
|
|
73
|
+
if (!resource || typeof resource[methodName] !== 'function') return;
|
|
74
|
+
|
|
75
|
+
patchMethod(
|
|
76
|
+
resource,
|
|
77
|
+
methodName,
|
|
78
|
+
patchKey,
|
|
79
|
+
(original) =>
|
|
80
|
+
function patchedMistralMethod(this: any, ...args: any[]) {
|
|
81
|
+
const model = getModel(args);
|
|
82
|
+
const spanName = model
|
|
83
|
+
? `Mistral ${operation} ${model}`
|
|
84
|
+
: `Mistral ${operation}`;
|
|
85
|
+
|
|
86
|
+
const span = startCapturedSpan(
|
|
87
|
+
spanName,
|
|
88
|
+
'http',
|
|
89
|
+
{
|
|
90
|
+
'gen_ai.system': 'mistral',
|
|
91
|
+
'gen_ai.operation.name': operation,
|
|
92
|
+
'gen_ai.request.model': model,
|
|
93
|
+
library: 'mistral',
|
|
94
|
+
},
|
|
95
|
+
options
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!span) return original.apply(this, args);
|
|
99
|
+
|
|
100
|
+
return runWithCapturedSpan(span, () => {
|
|
101
|
+
try {
|
|
102
|
+
const result = original.apply(this, args);
|
|
103
|
+
|
|
104
|
+
if (result && typeof result.then === 'function') {
|
|
105
|
+
return result.then(
|
|
106
|
+
(value: any) => {
|
|
107
|
+
span.end(0, extractUsage(value));
|
|
108
|
+
return value;
|
|
109
|
+
},
|
|
110
|
+
(error: any) => {
|
|
111
|
+
span.end(error?.statusCode || error?.status || 500, {
|
|
112
|
+
'error.message': error?.message,
|
|
113
|
+
'error.type': error?.name || 'MistralError',
|
|
114
|
+
});
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
span.end(0);
|
|
121
|
+
return result;
|
|
122
|
+
} catch (error: any) {
|
|
123
|
+
span.end(500, { 'error.message': error?.message });
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Mistral client patching
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
const patchMistralClient = (mistralModule: any, options?: SenzorOptions) => {
|
|
136
|
+
const Mistral = mistralModule?.Mistral
|
|
137
|
+
|| mistralModule?.MistralClient
|
|
138
|
+
|| mistralModule?.default;
|
|
139
|
+
|
|
140
|
+
if (!Mistral || typeof Mistral !== 'function') return;
|
|
141
|
+
|
|
142
|
+
const proto = Mistral.prototype;
|
|
143
|
+
if (!proto) return;
|
|
144
|
+
|
|
145
|
+
// Patch internal HTTP methods on the client prototype
|
|
146
|
+
const httpMethods = ['post', 'get', 'put', 'patch', 'delete'] as const;
|
|
147
|
+
for (const method of httpMethods) {
|
|
148
|
+
if (typeof proto[method] !== 'function') continue;
|
|
149
|
+
|
|
150
|
+
patchMethod(
|
|
151
|
+
proto,
|
|
152
|
+
method,
|
|
153
|
+
`senzor.mistral.client.${method}`,
|
|
154
|
+
(original) =>
|
|
155
|
+
function patchedHttpMethod(this: any, path: string, ...args: any[]) {
|
|
156
|
+
const body = args[0]?.body || args[0];
|
|
157
|
+
const model = body?.model;
|
|
158
|
+
const operation = path?.replace(/^\/?(v1\/)?/, '').split('/')[0] || 'api';
|
|
159
|
+
|
|
160
|
+
const spanName = model
|
|
161
|
+
? `Mistral ${operation} ${model}`
|
|
162
|
+
: `Mistral ${operation}`;
|
|
163
|
+
|
|
164
|
+
const span = startCapturedSpan(
|
|
165
|
+
spanName,
|
|
166
|
+
'http',
|
|
167
|
+
{
|
|
168
|
+
'gen_ai.system': 'mistral',
|
|
169
|
+
'gen_ai.operation.name': operation,
|
|
170
|
+
'gen_ai.request.model': model,
|
|
171
|
+
'http.request.method': method.toUpperCase(),
|
|
172
|
+
'url.path': path,
|
|
173
|
+
library: 'mistral',
|
|
174
|
+
},
|
|
175
|
+
options
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!span) return original.call(this, path, ...args);
|
|
179
|
+
|
|
180
|
+
return runWithCapturedSpan(span, () => {
|
|
181
|
+
try {
|
|
182
|
+
const result = original.call(this, path, ...args);
|
|
183
|
+
|
|
184
|
+
if (result && typeof result.then === 'function') {
|
|
185
|
+
return result.then(
|
|
186
|
+
(value: any) => {
|
|
187
|
+
span.end(0, extractMistralUsage(value));
|
|
188
|
+
return value;
|
|
189
|
+
},
|
|
190
|
+
(error: any) => {
|
|
191
|
+
span.end(error?.status || 500, {
|
|
192
|
+
'error.message': error?.message,
|
|
193
|
+
});
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
span.end(0);
|
|
200
|
+
return result;
|
|
201
|
+
} catch (error: any) {
|
|
202
|
+
span.end(500, { 'error.message': error?.message });
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Try to patch resource namespaces on instances
|
|
211
|
+
// These are created in the constructor, so we wrap the constructor
|
|
212
|
+
try {
|
|
213
|
+
const tempClient = new Mistral({ apiKey: '__senzor_probe__' });
|
|
214
|
+
|
|
215
|
+
// Patch chat resource
|
|
216
|
+
if (tempClient.chat) {
|
|
217
|
+
const chatProto = Object.getPrototypeOf(tempClient.chat);
|
|
218
|
+
if (chatProto) {
|
|
219
|
+
patchResourceMethod(chatProto, 'complete', 'chat', (a) => a[0]?.model, extractMistralUsage, 'senzor.mistral.chat.complete', options);
|
|
220
|
+
patchResourceMethod(chatProto, 'stream', 'chat.stream', (a) => a[0]?.model, () => ({}), 'senzor.mistral.chat.stream', options);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Patch embeddings resource
|
|
225
|
+
if (tempClient.embeddings) {
|
|
226
|
+
const embedProto = Object.getPrototypeOf(tempClient.embeddings);
|
|
227
|
+
if (embedProto) {
|
|
228
|
+
patchResourceMethod(embedProto, 'create', 'embeddings', (a) => a[0]?.model, extractMistralUsage, 'senzor.mistral.embeddings.create', options);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Patch fim resource
|
|
233
|
+
if ((tempClient as any).fim) {
|
|
234
|
+
const fimProto = Object.getPrototypeOf((tempClient as any).fim);
|
|
235
|
+
if (fimProto) {
|
|
236
|
+
patchResourceMethod(fimProto, 'complete', 'fim', (a) => a[0]?.model, extractMistralUsage, 'senzor.mistral.fim.complete', options);
|
|
237
|
+
patchResourceMethod(fimProto, 'stream', 'fim.stream', (a) => a[0]?.model, () => ({}), 'senzor.mistral.fim.stream', options);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// Mistral constructor may require a real API key — resource patching is best-effort
|
|
242
|
+
// The HTTP method patches on the prototype still cover all calls
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Public API
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
export const instrumentMistral = (options?: SenzorOptions) => {
|
|
251
|
+
hookRequire('@mistralai/mistralai', (exports: any) => {
|
|
252
|
+
patchMistralClient(exports, options);
|
|
253
|
+
});
|
|
254
|
+
};
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
// NestJS Instrumentation
|
|
8
|
+
//
|
|
9
|
+
// Instruments @nestjs/core at the router execution layer:
|
|
10
|
+
// - RouterExecutionContext.prototype.create() — wraps the handler factory
|
|
11
|
+
// so every controller method invocation generates a span with:
|
|
12
|
+
// nestjs.controller, nestjs.method, nestjs.route, http.route
|
|
13
|
+
//
|
|
14
|
+
// This captures the full NestJS request lifecycle including guards,
|
|
15
|
+
// interceptors, pipes, and the controller method execution.
|
|
16
|
+
//
|
|
17
|
+
// Works with both Express and Fastify adapters since we patch at the
|
|
18
|
+
// NestJS layer above the HTTP adapter.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** Extract a human-readable name from a NestJS controller class. */
|
|
22
|
+
const getControllerName = (instance: any): string => {
|
|
23
|
+
if (!instance) return 'UnknownController';
|
|
24
|
+
return instance.constructor?.name || 'UnknownController';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// RouterExecutionContext patching
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
const patchRouterExecutionContext = (nestCore: any, options?: SenzorOptions) => {
|
|
32
|
+
// Try to access RouterExecutionContext from the module
|
|
33
|
+
let RouterExecutionContext: any;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
RouterExecutionContext = require('@nestjs/core/router/router-execution-context')?.RouterExecutionContext;
|
|
37
|
+
} catch { }
|
|
38
|
+
|
|
39
|
+
// Fallback: search in exports
|
|
40
|
+
if (!RouterExecutionContext) {
|
|
41
|
+
RouterExecutionContext = nestCore?.RouterExecutionContext;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!RouterExecutionContext?.prototype?.create) return;
|
|
45
|
+
|
|
46
|
+
patchMethod(
|
|
47
|
+
RouterExecutionContext.prototype,
|
|
48
|
+
'create',
|
|
49
|
+
'senzor.nestjs.routerExecutionContext.create',
|
|
50
|
+
(original) =>
|
|
51
|
+
function patchedCreate(
|
|
52
|
+
this: any,
|
|
53
|
+
instance: any,
|
|
54
|
+
callback: Function,
|
|
55
|
+
methodName: string,
|
|
56
|
+
moduleKey: string,
|
|
57
|
+
requestMethod: number,
|
|
58
|
+
...rest: any[]
|
|
59
|
+
) {
|
|
60
|
+
const handler = original.call(this, instance, callback, methodName, moduleKey, requestMethod, ...rest);
|
|
61
|
+
|
|
62
|
+
if (typeof handler !== 'function') return handler;
|
|
63
|
+
|
|
64
|
+
const controllerName = getControllerName(instance);
|
|
65
|
+
|
|
66
|
+
return function wrappedNestHandler(this: any, req: any, res: any, next: any) {
|
|
67
|
+
// Extract route from request
|
|
68
|
+
const route = req?.route?.path || req?.url?.split('?')[0] || '/';
|
|
69
|
+
|
|
70
|
+
const span = startCapturedSpan(
|
|
71
|
+
`NestJS ${controllerName}.${methodName}`,
|
|
72
|
+
'function',
|
|
73
|
+
{
|
|
74
|
+
'nestjs.controller': controllerName,
|
|
75
|
+
'nestjs.method': methodName,
|
|
76
|
+
'nestjs.module': moduleKey,
|
|
77
|
+
'nestjs.type': 'request_handler',
|
|
78
|
+
'http.route': route,
|
|
79
|
+
},
|
|
80
|
+
options
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (!span) return handler.call(this, req, res, next);
|
|
84
|
+
|
|
85
|
+
return runWithCapturedSpan(span, () => {
|
|
86
|
+
try {
|
|
87
|
+
const result = handler.call(this, req, res, next);
|
|
88
|
+
|
|
89
|
+
if (result && typeof result.then === 'function') {
|
|
90
|
+
return result.then(
|
|
91
|
+
(val: any) => {
|
|
92
|
+
span.end(0);
|
|
93
|
+
return val;
|
|
94
|
+
},
|
|
95
|
+
(error: any) => {
|
|
96
|
+
span.end(500, {
|
|
97
|
+
'error.message': error?.message,
|
|
98
|
+
'error.type': error?.name || 'Error',
|
|
99
|
+
});
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
span.end(0);
|
|
106
|
+
return result;
|
|
107
|
+
} catch (error: any) {
|
|
108
|
+
span.end(500, {
|
|
109
|
+
'error.message': error?.message,
|
|
110
|
+
'error.type': error?.name || 'Error',
|
|
111
|
+
});
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// RouterExplorer patching (alternative / complementary)
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
const patchRouterExplorer = (nestCore: any, options?: SenzorOptions) => {
|
|
125
|
+
let RouterExplorer: any;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
RouterExplorer = require('@nestjs/core/router/router-explorer')?.RouterExplorer;
|
|
129
|
+
} catch { }
|
|
130
|
+
|
|
131
|
+
if (!RouterExplorer) {
|
|
132
|
+
RouterExplorer = nestCore?.RouterExplorer;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!RouterExplorer?.prototype?.applyCallbackToRouter) return;
|
|
136
|
+
|
|
137
|
+
patchMethod(
|
|
138
|
+
RouterExplorer.prototype,
|
|
139
|
+
'applyCallbackToRouter',
|
|
140
|
+
'senzor.nestjs.routerExplorer.applyCallbackToRouter',
|
|
141
|
+
(original) =>
|
|
142
|
+
function patchedApplyCallback(
|
|
143
|
+
this: any,
|
|
144
|
+
router: any,
|
|
145
|
+
routeDefinition: any,
|
|
146
|
+
instanceWrapper: any,
|
|
147
|
+
moduleKey: string,
|
|
148
|
+
...rest: any[]
|
|
149
|
+
) {
|
|
150
|
+
// Extract metadata before registration
|
|
151
|
+
const methodName = routeDefinition?.methodName || 'unknown';
|
|
152
|
+
const path = routeDefinition?.path || '/';
|
|
153
|
+
const requestMethod = routeDefinition?.requestMethod;
|
|
154
|
+
|
|
155
|
+
const controllerName = instanceWrapper?.metatype?.name
|
|
156
|
+
|| instanceWrapper?.name
|
|
157
|
+
|| 'UnknownController';
|
|
158
|
+
|
|
159
|
+
// Let NestJS register the route normally
|
|
160
|
+
const result = original.call(this, router, routeDefinition, instanceWrapper, moduleKey, ...rest);
|
|
161
|
+
|
|
162
|
+
// Log registration for debug
|
|
163
|
+
if (options?.debug) {
|
|
164
|
+
console.log(`[Senzor] NestJS route registered: ${controllerName}.${methodName} → ${path}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Public API
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
export const instrumentNestJS = (options?: SenzorOptions) => {
|
|
177
|
+
hookRequire('@nestjs/core', (exports: any) => {
|
|
178
|
+
patchRouterExecutionContext(exports, options);
|
|
179
|
+
patchRouterExplorer(exports, options);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Also try the specific sub-module paths (varies by NestJS version)
|
|
183
|
+
hookRequire('@nestjs/core/router/router-execution-context', (exports: any) => {
|
|
184
|
+
if (exports?.RouterExecutionContext?.prototype?.create) {
|
|
185
|
+
patchMethod(
|
|
186
|
+
exports.RouterExecutionContext.prototype,
|
|
187
|
+
'create',
|
|
188
|
+
'senzor.nestjs.rec.create.direct',
|
|
189
|
+
(original) =>
|
|
190
|
+
function patchedCreateDirect(
|
|
191
|
+
this: any,
|
|
192
|
+
instance: any,
|
|
193
|
+
callback: Function,
|
|
194
|
+
methodName: string,
|
|
195
|
+
moduleKey: string,
|
|
196
|
+
requestMethod: number,
|
|
197
|
+
...rest: any[]
|
|
198
|
+
) {
|
|
199
|
+
const handler = original.call(this, instance, callback, methodName, moduleKey, requestMethod, ...rest);
|
|
200
|
+
if (typeof handler !== 'function') return handler;
|
|
201
|
+
|
|
202
|
+
const controllerName = getControllerName(instance);
|
|
203
|
+
|
|
204
|
+
return function wrappedHandler(this: any, req: any, res: any, next: any) {
|
|
205
|
+
const route = req?.route?.path || req?.url?.split('?')[0] || '/';
|
|
206
|
+
|
|
207
|
+
const span = startCapturedSpan(
|
|
208
|
+
`NestJS ${controllerName}.${methodName}`,
|
|
209
|
+
'function',
|
|
210
|
+
{
|
|
211
|
+
'nestjs.controller': controllerName,
|
|
212
|
+
'nestjs.method': methodName,
|
|
213
|
+
'nestjs.module': moduleKey,
|
|
214
|
+
'nestjs.type': 'request_handler',
|
|
215
|
+
'http.route': route,
|
|
216
|
+
},
|
|
217
|
+
options
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (!span) return handler.call(this, req, res, next);
|
|
221
|
+
|
|
222
|
+
return runWithCapturedSpan(span, () => {
|
|
223
|
+
try {
|
|
224
|
+
const result = handler.call(this, req, res, next);
|
|
225
|
+
if (result && typeof result.then === 'function') {
|
|
226
|
+
return result.then(
|
|
227
|
+
(val: any) => { span.end(0); return val; },
|
|
228
|
+
(err: any) => { span.end(500, { 'error.message': err?.message }); throw err; }
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
span.end(0);
|
|
232
|
+
return result;
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
span.end(500, { 'error.message': err?.message });
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { SenzorOptions } from '../core/types';
|
|
2
|
+
import { patchMethod } from './patch';
|
|
3
|
+
import { runWithCapturedSpan, startCapturedSpan } from './span';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Net (TCP) Instrumentation
|
|
7
|
+
//
|
|
8
|
+
// Instruments Node.js core `net` module:
|
|
9
|
+
// - net.connect() / net.createConnection() — TCP connection spans
|
|
10
|
+
// - net.Socket.prototype.connect() — socket-level connection
|
|
11
|
+
//
|
|
12
|
+
// Captures TCP connection establishment latency and peer information.
|
|
13
|
+
// Follows OTel semantic conventions: net.peer.name, net.peer.port, net.transport
|
|
14
|
+
//
|
|
15
|
+
// NOTE: This instrumentation only captures connection establishment, not
|
|
16
|
+
// data transfer. This prevents excessive span noise while still providing
|
|
17
|
+
// visibility into network bottlenecks (slow connects, timeouts, refused).
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize connection options from the various net.connect() signatures:
|
|
22
|
+
* - connect(port, host, cb)
|
|
23
|
+
* - connect({ port, host }, cb)
|
|
24
|
+
* - connect(path, cb) — IPC / Unix domain socket
|
|
25
|
+
* - connect({ path }, cb) — IPC / Unix domain socket
|
|
26
|
+
*/
|
|
27
|
+
const normalizeConnectArgs = (
|
|
28
|
+
args: any[]
|
|
29
|
+
): { host: string; port: number | string; isIPC: boolean } => {
|
|
30
|
+
const first = args[0];
|
|
31
|
+
|
|
32
|
+
// Object form: { port, host } or { path }
|
|
33
|
+
if (typeof first === 'object' && first !== null && !Array.isArray(first)) {
|
|
34
|
+
if (first.path) {
|
|
35
|
+
return { host: first.path, port: 'ipc', isIPC: true };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
host: first.host || 'localhost',
|
|
39
|
+
port: first.port || 0,
|
|
40
|
+
isIPC: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// String form: path (IPC)
|
|
45
|
+
if (typeof first === 'string' && !Number.isFinite(Number(first))) {
|
|
46
|
+
return { host: first, port: 'ipc', isIPC: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Numeric form: port, [host]
|
|
50
|
+
const port = Number(first) || 0;
|
|
51
|
+
const host = typeof args[1] === 'string' ? args[1] : 'localhost';
|
|
52
|
+
return { host, port, isIPC: false };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Patching
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
const patchSocketConnect = (netModule: any, options?: SenzorOptions) => {
|
|
60
|
+
const socketProto = netModule?.Socket?.prototype;
|
|
61
|
+
if (!socketProto) return;
|
|
62
|
+
|
|
63
|
+
patchMethod(
|
|
64
|
+
socketProto,
|
|
65
|
+
'connect',
|
|
66
|
+
'senzor.net.socket.connect',
|
|
67
|
+
(original) =>
|
|
68
|
+
function patchedSocketConnect(this: any, ...args: any[]) {
|
|
69
|
+
const { host, port, isIPC } = normalizeConnectArgs(args);
|
|
70
|
+
|
|
71
|
+
const spanName = isIPC
|
|
72
|
+
? `TCP connect ${host}`
|
|
73
|
+
: `TCP connect ${host}:${port}`;
|
|
74
|
+
|
|
75
|
+
const span = startCapturedSpan(
|
|
76
|
+
spanName,
|
|
77
|
+
'custom',
|
|
78
|
+
{
|
|
79
|
+
'net.peer.name': host,
|
|
80
|
+
'net.peer.port': isIPC ? undefined : port,
|
|
81
|
+
'net.transport': isIPC ? 'unix' : 'tcp',
|
|
82
|
+
'network.transport': isIPC ? 'unix' : 'tcp',
|
|
83
|
+
},
|
|
84
|
+
options
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!span) return original.apply(this, args);
|
|
88
|
+
|
|
89
|
+
return runWithCapturedSpan(span, () => {
|
|
90
|
+
const socket = original.apply(this, args);
|
|
91
|
+
|
|
92
|
+
let ended = false;
|
|
93
|
+
const endOnce = (status: number, meta: Record<string, any> = {}) => {
|
|
94
|
+
if (ended) return;
|
|
95
|
+
ended = true;
|
|
96
|
+
span.end(status, meta);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Connection established successfully
|
|
100
|
+
socket.once('connect', () => {
|
|
101
|
+
endOnce(0, {
|
|
102
|
+
'net.peer.address': socket.remoteAddress,
|
|
103
|
+
'net.peer.port': socket.remotePort,
|
|
104
|
+
'net.local.address': socket.localAddress,
|
|
105
|
+
'net.local.port': socket.localPort,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Connection failed
|
|
110
|
+
socket.once('error', (err: any) => {
|
|
111
|
+
endOnce(500, {
|
|
112
|
+
'error.message': err?.message,
|
|
113
|
+
'error.type': err?.code || err?.name || 'NetError',
|
|
114
|
+
'net.error_code': err?.code,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Connection timed out
|
|
119
|
+
socket.once('timeout', () => {
|
|
120
|
+
endOnce(504, {
|
|
121
|
+
'error.message': 'Connection timed out',
|
|
122
|
+
'error.type': 'TimeoutError',
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Connection closed before establishing
|
|
127
|
+
socket.once('close', (hadError: boolean) => {
|
|
128
|
+
if (hadError) {
|
|
129
|
+
endOnce(500, {
|
|
130
|
+
'error.message': 'Connection closed with error',
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
endOnce(0);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return socket;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Patch net.connect() and net.createConnection() factory functions.
|
|
145
|
+
* These create a new Socket and immediately call socket.connect().
|
|
146
|
+
* Since we patch Socket.prototype.connect, these are automatically covered.
|
|
147
|
+
* However, we add a thin wrapper for consistency in span naming.
|
|
148
|
+
*/
|
|
149
|
+
const patchNetFactories = (netModule: any, options?: SenzorOptions) => {
|
|
150
|
+
// net.connect and net.createConnection are usually the same function
|
|
151
|
+
// Since we patch Socket.prototype.connect, the factory functions are
|
|
152
|
+
// automatically instrumented. No additional patching needed.
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Public API
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
export const instrumentNet = (options?: SenzorOptions) => {
|
|
160
|
+
let net: any;
|
|
161
|
+
try {
|
|
162
|
+
net = require('net');
|
|
163
|
+
} catch {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!net) return;
|
|
168
|
+
|
|
169
|
+
patchSocketConnect(net, options);
|
|
170
|
+
patchNetFactories(net, options);
|
|
171
|
+
};
|