@senzops/apm-node 1.2.7 → 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/context.ts +71 -9
- 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,281 @@
|
|
|
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
|
+
// OpenAI SDK Instrumentation
|
|
8
|
+
//
|
|
9
|
+
// Instruments the official `openai` npm package (v4+) to capture API calls
|
|
10
|
+
// to OpenAI services (GPT, DALL-E, Whisper, Embeddings, Assistants, etc.).
|
|
11
|
+
//
|
|
12
|
+
// Strategy: Patch the core APIClient._request() method — the single
|
|
13
|
+
// dispatch point for ALL OpenAI API calls. This covers:
|
|
14
|
+
// - chat.completions.create()
|
|
15
|
+
// - completions.create()
|
|
16
|
+
// - embeddings.create()
|
|
17
|
+
// - images.generate()
|
|
18
|
+
// - audio.transcriptions.create()
|
|
19
|
+
// - moderations.create()
|
|
20
|
+
// - files.*, fine_tuning.*, assistants.*, threads.*, etc.
|
|
21
|
+
//
|
|
22
|
+
// Also patches specific resource methods for richer attribute capture
|
|
23
|
+
// (model name, token usage, etc.).
|
|
24
|
+
//
|
|
25
|
+
// Captured attributes (following emerging GenAI OTel conventions):
|
|
26
|
+
// - gen_ai.system: 'openai'
|
|
27
|
+
// - gen_ai.request.model: model name (gpt-4, gpt-3.5-turbo, etc.)
|
|
28
|
+
// - gen_ai.operation.name: chat, completions, embeddings, etc.
|
|
29
|
+
// - gen_ai.response.model: actual model used in response
|
|
30
|
+
// - gen_ai.usage.input_tokens: prompt tokens
|
|
31
|
+
// - gen_ai.usage.output_tokens: completion tokens
|
|
32
|
+
// - gen_ai.request.max_tokens: requested max tokens
|
|
33
|
+
// - gen_ai.request.temperature: temperature setting
|
|
34
|
+
// - http.response.status_code: API response status
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Map of resource path segments to operation names. */
|
|
38
|
+
const OPERATION_MAP: Record<string, string> = {
|
|
39
|
+
'chat/completions': 'chat',
|
|
40
|
+
completions: 'completions',
|
|
41
|
+
embeddings: 'embeddings',
|
|
42
|
+
images: 'images',
|
|
43
|
+
'images/generations': 'images.generate',
|
|
44
|
+
'images/edits': 'images.edit',
|
|
45
|
+
'images/variations': 'images.variation',
|
|
46
|
+
'audio/transcriptions': 'audio.transcribe',
|
|
47
|
+
'audio/translations': 'audio.translate',
|
|
48
|
+
'audio/speech': 'audio.speech',
|
|
49
|
+
moderations: 'moderations',
|
|
50
|
+
'fine_tuning/jobs': 'fine_tuning',
|
|
51
|
+
files: 'files',
|
|
52
|
+
assistants: 'assistants',
|
|
53
|
+
threads: 'threads',
|
|
54
|
+
'threads/runs': 'threads.runs',
|
|
55
|
+
'threads/messages': 'threads.messages',
|
|
56
|
+
batches: 'batches',
|
|
57
|
+
'vector_stores': 'vector_stores',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Extract operation name from the API path. */
|
|
61
|
+
const getOperationName = (path: string): string => {
|
|
62
|
+
if (!path) return 'unknown';
|
|
63
|
+
|
|
64
|
+
// Normalize path: strip leading slash, version prefix
|
|
65
|
+
const normalized = path.replace(/^\/?(v1\/)?/, '').replace(/\/[a-f0-9-]{20,}(\/|$)/g, '/');
|
|
66
|
+
|
|
67
|
+
// Try exact match first, then prefix match
|
|
68
|
+
for (const [pattern, name] of Object.entries(OPERATION_MAP)) {
|
|
69
|
+
if (normalized === pattern || normalized.startsWith(pattern + '/')) {
|
|
70
|
+
return name;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fallback: first path segment
|
|
75
|
+
return normalized.split('/')[0] || 'api';
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Extract model from request body. */
|
|
79
|
+
const getRequestModel = (body: any): string | undefined => {
|
|
80
|
+
if (!body || typeof body !== 'object') return undefined;
|
|
81
|
+
return body.model || undefined;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Core APIClient._request patching
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
const patchOpenAIClient = (openaiModule: any, options?: SenzorOptions) => {
|
|
89
|
+
// openai v4 exports OpenAI class (default export)
|
|
90
|
+
const OpenAI = openaiModule?.OpenAI || openaiModule?.default || openaiModule;
|
|
91
|
+
|
|
92
|
+
if (!OpenAI || typeof OpenAI !== 'function') return;
|
|
93
|
+
|
|
94
|
+
const proto = OpenAI.prototype;
|
|
95
|
+
if (!proto) return;
|
|
96
|
+
|
|
97
|
+
// Find the internal request method — could be _request, post, get, etc.
|
|
98
|
+
// In openai v4, the base client (APIClient) has these methods:
|
|
99
|
+
// post(), get(), put(), patch(), delete() which all call _request()
|
|
100
|
+
|
|
101
|
+
// Patch the HTTP methods on the prototype
|
|
102
|
+
const httpMethods = ['post', 'get', 'put', 'patch', 'delete'] as const;
|
|
103
|
+
|
|
104
|
+
for (const method of httpMethods) {
|
|
105
|
+
if (typeof proto[method] !== 'function') continue;
|
|
106
|
+
|
|
107
|
+
patchMethod(
|
|
108
|
+
proto,
|
|
109
|
+
method,
|
|
110
|
+
`senzor.openai.client.${method}`,
|
|
111
|
+
(original) =>
|
|
112
|
+
function patchedMethod(this: any, path: string, opts?: any) {
|
|
113
|
+
const operationName = getOperationName(path);
|
|
114
|
+
const model = getRequestModel(opts?.body);
|
|
115
|
+
const httpMethod = method.toUpperCase();
|
|
116
|
+
|
|
117
|
+
const spanName = model
|
|
118
|
+
? `OpenAI ${operationName} ${model}`
|
|
119
|
+
: `OpenAI ${operationName}`;
|
|
120
|
+
|
|
121
|
+
const span = startCapturedSpan(
|
|
122
|
+
spanName,
|
|
123
|
+
'http',
|
|
124
|
+
{
|
|
125
|
+
'gen_ai.system': 'openai',
|
|
126
|
+
'gen_ai.operation.name': operationName,
|
|
127
|
+
'gen_ai.request.model': model,
|
|
128
|
+
'gen_ai.request.max_tokens': opts?.body?.max_tokens,
|
|
129
|
+
'gen_ai.request.temperature': opts?.body?.temperature,
|
|
130
|
+
'http.request.method': httpMethod,
|
|
131
|
+
'url.path': path,
|
|
132
|
+
library: 'openai',
|
|
133
|
+
},
|
|
134
|
+
options
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (!span) return original.call(this, path, opts);
|
|
138
|
+
|
|
139
|
+
return runWithCapturedSpan(span, () => {
|
|
140
|
+
try {
|
|
141
|
+
const result = original.call(this, path, opts);
|
|
142
|
+
|
|
143
|
+
if (result && typeof result.then === 'function') {
|
|
144
|
+
return result.then(
|
|
145
|
+
(response: any) => {
|
|
146
|
+
const endMeta: Record<string, any> = {};
|
|
147
|
+
|
|
148
|
+
// Extract usage from response
|
|
149
|
+
if (response?.usage) {
|
|
150
|
+
endMeta['gen_ai.usage.input_tokens'] = response.usage.prompt_tokens;
|
|
151
|
+
endMeta['gen_ai.usage.output_tokens'] = response.usage.completion_tokens;
|
|
152
|
+
endMeta['gen_ai.usage.total_tokens'] = response.usage.total_tokens;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Extract actual model used
|
|
156
|
+
if (response?.model) {
|
|
157
|
+
endMeta['gen_ai.response.model'] = response.model;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Extract finish reason
|
|
161
|
+
if (response?.choices?.[0]?.finish_reason) {
|
|
162
|
+
endMeta['gen_ai.response.finish_reason'] = response.choices[0].finish_reason;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
span.end(0, endMeta);
|
|
166
|
+
return response;
|
|
167
|
+
},
|
|
168
|
+
(error: any) => {
|
|
169
|
+
const statusCode = error?.status || error?.statusCode || 500;
|
|
170
|
+
span.end(statusCode, {
|
|
171
|
+
'error.message': error?.message,
|
|
172
|
+
'error.type': error?.name || error?.type || 'OpenAIError',
|
|
173
|
+
'http.response.status_code': statusCode,
|
|
174
|
+
'gen_ai.error.code': error?.code,
|
|
175
|
+
});
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
span.end(0);
|
|
182
|
+
return result;
|
|
183
|
+
} catch (error: any) {
|
|
184
|
+
const statusCode = error?.status || 500;
|
|
185
|
+
span.end(statusCode, {
|
|
186
|
+
'error.message': error?.message,
|
|
187
|
+
'error.type': error?.name || 'Error',
|
|
188
|
+
'http.response.status_code': statusCode,
|
|
189
|
+
});
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Also try to patch the internal request dispatcher
|
|
198
|
+
if (typeof proto._request === 'function') {
|
|
199
|
+
patchMethod(
|
|
200
|
+
proto,
|
|
201
|
+
'_request',
|
|
202
|
+
'senzor.openai.client._request',
|
|
203
|
+
(original) =>
|
|
204
|
+
function patchedRequest(this: any, requestOptions: any, ...args: any[]) {
|
|
205
|
+
// _request receives the full request options object
|
|
206
|
+
const path = requestOptions?.path || '';
|
|
207
|
+
const method = requestOptions?.method || 'POST';
|
|
208
|
+
const operationName = getOperationName(path);
|
|
209
|
+
const model = getRequestModel(requestOptions?.body);
|
|
210
|
+
|
|
211
|
+
const spanName = model
|
|
212
|
+
? `OpenAI ${operationName} ${model}`
|
|
213
|
+
: `OpenAI ${operationName}`;
|
|
214
|
+
|
|
215
|
+
const span = startCapturedSpan(
|
|
216
|
+
spanName,
|
|
217
|
+
'http',
|
|
218
|
+
{
|
|
219
|
+
'gen_ai.system': 'openai',
|
|
220
|
+
'gen_ai.operation.name': operationName,
|
|
221
|
+
'gen_ai.request.model': model,
|
|
222
|
+
'http.request.method': method,
|
|
223
|
+
'url.path': path,
|
|
224
|
+
library: 'openai',
|
|
225
|
+
},
|
|
226
|
+
options
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (!span) return original.call(this, requestOptions, ...args);
|
|
230
|
+
|
|
231
|
+
return runWithCapturedSpan(span, () => {
|
|
232
|
+
try {
|
|
233
|
+
const result = original.call(this, requestOptions, ...args);
|
|
234
|
+
|
|
235
|
+
if (result && typeof result.then === 'function') {
|
|
236
|
+
return result.then(
|
|
237
|
+
(response: any) => {
|
|
238
|
+
const endMeta: Record<string, any> = {};
|
|
239
|
+
|
|
240
|
+
if (response?.usage) {
|
|
241
|
+
endMeta['gen_ai.usage.input_tokens'] = response.usage.prompt_tokens;
|
|
242
|
+
endMeta['gen_ai.usage.output_tokens'] = response.usage.completion_tokens;
|
|
243
|
+
}
|
|
244
|
+
if (response?.model) {
|
|
245
|
+
endMeta['gen_ai.response.model'] = response.model;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
span.end(0, endMeta);
|
|
249
|
+
return response;
|
|
250
|
+
},
|
|
251
|
+
(error: any) => {
|
|
252
|
+
span.end(error?.status || 500, {
|
|
253
|
+
'error.message': error?.message,
|
|
254
|
+
'error.type': error?.name || 'OpenAIError',
|
|
255
|
+
});
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
span.end(0);
|
|
262
|
+
return result;
|
|
263
|
+
} catch (error: any) {
|
|
264
|
+
span.end(500, { 'error.message': error?.message });
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Public API
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
export const instrumentOpenAI = (options?: SenzorOptions) => {
|
|
278
|
+
hookRequire('openai', (exports: any) => {
|
|
279
|
+
patchOpenAIClient(exports, options);
|
|
280
|
+
});
|
|
281
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Context } from '../core/context';
|
|
2
|
+
import { SenzorOptions } from '../core/types';
|
|
3
|
+
import { hookRequire } from './hook';
|
|
4
|
+
import { patchMethod } from './patch';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Pino Log Correlation
|
|
8
|
+
//
|
|
9
|
+
// Injects traceId and spanId from the active Senzor context into every
|
|
10
|
+
// pino log record. This enables log-to-trace correlation in the dashboard.
|
|
11
|
+
//
|
|
12
|
+
// Strategy: Wrap the pino factory to inject a mixin function that reads
|
|
13
|
+
// from AsyncLocalStorage on every log call. If the user provides their
|
|
14
|
+
// own mixin, both are composed together.
|
|
15
|
+
//
|
|
16
|
+
// Also patches existing logger prototypes to ensure loggers created before
|
|
17
|
+
// instrumentation are also covered.
|
|
18
|
+
//
|
|
19
|
+
// Injected fields:
|
|
20
|
+
// - traceId: string (APM trace ID or Task run ID)
|
|
21
|
+
// - spanId: string (active span ID)
|
|
22
|
+
// - senzor.context: 'apm' | 'task'
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** Get trace correlation fields from the current async context. */
|
|
26
|
+
const getTraceFields = (): Record<string, string> | null => {
|
|
27
|
+
const trace = Context.current();
|
|
28
|
+
if (!trace) return null;
|
|
29
|
+
|
|
30
|
+
const fields: Record<string, string> = {
|
|
31
|
+
traceId: trace.id,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (trace.activeSpanId) {
|
|
35
|
+
fields.spanId = trace.activeSpanId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fields['senzor.context'] = trace.contextType;
|
|
39
|
+
|
|
40
|
+
return fields;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Create a mixin function that injects trace context. */
|
|
44
|
+
const createTraceMixin = (userMixin?: Function): Function => {
|
|
45
|
+
return (mergeObject: any, level: number) => {
|
|
46
|
+
const traceFields = getTraceFields();
|
|
47
|
+
const userFields = typeof userMixin === 'function'
|
|
48
|
+
? userMixin(mergeObject, level)
|
|
49
|
+
: {};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...userFields,
|
|
53
|
+
...(traceFields || {}),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Factory wrapping
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
const patchPinoFactory = (pinoModule: any, _options?: SenzorOptions) => {
|
|
63
|
+
// pino is exported as a function (the factory) with properties on it
|
|
64
|
+
// We need to wrap the function itself, which is tricky since it's the module export
|
|
65
|
+
|
|
66
|
+
// Strategy: Patch the internal prototype's write method for already-created loggers
|
|
67
|
+
// AND wrap the factory for new loggers
|
|
68
|
+
|
|
69
|
+
// 1. Wrap the factory
|
|
70
|
+
const originalPino = pinoModule;
|
|
71
|
+
|
|
72
|
+
if (typeof pinoModule !== 'function') return;
|
|
73
|
+
|
|
74
|
+
// We can't replace the module export directly from hookRequire,
|
|
75
|
+
// but we can patch the prototype for existing loggers
|
|
76
|
+
|
|
77
|
+
// 2. Patch the internal prototype
|
|
78
|
+
// Create a temp logger to get the prototype
|
|
79
|
+
try {
|
|
80
|
+
const devNull = { write: () => {} };
|
|
81
|
+
const tempLogger = pinoModule({ level: 'silent' }, devNull);
|
|
82
|
+
const proto = Object.getPrototypeOf(tempLogger);
|
|
83
|
+
|
|
84
|
+
if (proto && !proto.__senzorPatched) {
|
|
85
|
+
// Find the write symbol or method
|
|
86
|
+
const writeSymbol = Object.getOwnPropertySymbols(proto).find(
|
|
87
|
+
(sym) => sym.toString().includes('write') || sym.toString().includes('pino.write')
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (writeSymbol) {
|
|
91
|
+
const originalWrite = proto[writeSymbol];
|
|
92
|
+
if (typeof originalWrite === 'function') {
|
|
93
|
+
proto[writeSymbol] = function patchedWrite(this: any, obj: any, msg: any, num: any) {
|
|
94
|
+
const traceFields = getTraceFields();
|
|
95
|
+
if (traceFields && obj && typeof obj === 'object') {
|
|
96
|
+
Object.assign(obj, traceFields);
|
|
97
|
+
}
|
|
98
|
+
return originalWrite.call(this, obj, msg, num);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Also try patching the level methods directly as fallback
|
|
104
|
+
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
|
|
105
|
+
for (const level of levels) {
|
|
106
|
+
if (typeof proto[level] === 'function') {
|
|
107
|
+
patchMethod(
|
|
108
|
+
proto,
|
|
109
|
+
level,
|
|
110
|
+
`senzor.pino.${level}`,
|
|
111
|
+
(original) =>
|
|
112
|
+
function patchedLevel(this: any, ...args: any[]) {
|
|
113
|
+
const traceFields = getTraceFields();
|
|
114
|
+
if (!traceFields) return original.apply(this, args);
|
|
115
|
+
|
|
116
|
+
// pino level methods accept:
|
|
117
|
+
// .info(obj, msg, ...args)
|
|
118
|
+
// .info(msg, ...args)
|
|
119
|
+
// .info(err, msg, ...args)
|
|
120
|
+
if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null && !(args[0] instanceof Error)) {
|
|
121
|
+
// First arg is a merge object — inject trace fields
|
|
122
|
+
args[0] = { ...args[0], ...traceFields };
|
|
123
|
+
} else if (args.length > 0 && typeof args[0] === 'string') {
|
|
124
|
+
// First arg is message string — prepend a merge object
|
|
125
|
+
args.unshift(traceFields);
|
|
126
|
+
} else if (args.length > 0 && args[0] instanceof Error) {
|
|
127
|
+
// First arg is an error — add trace fields alongside
|
|
128
|
+
const err = args[0];
|
|
129
|
+
args[0] = { ...traceFields, err };
|
|
130
|
+
if (typeof args[1] !== 'string') {
|
|
131
|
+
args.splice(1, 0, err.message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return original.apply(this, args);
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Patch child() to ensure child loggers also get correlation
|
|
142
|
+
if (typeof proto.child === 'function') {
|
|
143
|
+
patchMethod(
|
|
144
|
+
proto,
|
|
145
|
+
'child',
|
|
146
|
+
'senzor.pino.child',
|
|
147
|
+
(original) =>
|
|
148
|
+
function patchedChild(this: any, bindings: any, ...args: any[]) {
|
|
149
|
+
// Child loggers inherit the patched prototype automatically
|
|
150
|
+
return original.call(this, bindings, ...args);
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
proto.__senzorPatched = true;
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Pino may not be fully loaded yet — the hookRequire retry will catch it
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Public API
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
export const instrumentPino = (_options?: SenzorOptions) => {
|
|
167
|
+
hookRequire('pino', (exports: any) => {
|
|
168
|
+
patchPinoFactory(exports, _options);
|
|
169
|
+
});
|
|
170
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
// Restify Instrumentation
|
|
8
|
+
//
|
|
9
|
+
// Instruments the `restify` HTTP framework at the server layer:
|
|
10
|
+
// - Server route registration methods (get, post, put, del, patch, head, opts)
|
|
11
|
+
// to wrap route handlers and generate spans per request.
|
|
12
|
+
// - Server.prototype.use() to optionally capture middleware spans.
|
|
13
|
+
//
|
|
14
|
+
// Restify handler signature: (req, res, next) — same as Express/Connect.
|
|
15
|
+
// Route path is available at registration time via the route config.
|
|
16
|
+
//
|
|
17
|
+
// Captured attributes:
|
|
18
|
+
// - http.route: registered route path
|
|
19
|
+
// - http.method: HTTP method
|
|
20
|
+
// - restify.type: 'route_handler' | 'middleware'
|
|
21
|
+
// - restify.version: route version (if versioned routes)
|
|
22
|
+
// - framework: 'restify'
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'del', 'patch', 'head', 'opts'] as const;
|
|
26
|
+
|
|
27
|
+
/** Normalize restify method name to HTTP method. */
|
|
28
|
+
const METHOD_MAP: Record<string, string> = {
|
|
29
|
+
get: 'GET', post: 'POST', put: 'PUT', del: 'DELETE',
|
|
30
|
+
patch: 'PATCH', head: 'HEAD', opts: 'OPTIONS',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Handler wrapping
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const wrapHandler = (
|
|
38
|
+
handler: Function,
|
|
39
|
+
method: string,
|
|
40
|
+
path: string,
|
|
41
|
+
type: 'route_handler' | 'middleware',
|
|
42
|
+
options?: SenzorOptions
|
|
43
|
+
): Function => {
|
|
44
|
+
if (typeof handler !== 'function') return handler;
|
|
45
|
+
if ((handler as any).__senzorWrapped) return handler;
|
|
46
|
+
|
|
47
|
+
const wrapped = function wrappedRestifyHandler(this: any, req: any, res: any, next: any) {
|
|
48
|
+
const httpMethod = METHOD_MAP[method] || method.toUpperCase();
|
|
49
|
+
const routePath = path || req?.route?.path || req?.getPath?.() || req?.url?.split('?')[0] || '/';
|
|
50
|
+
|
|
51
|
+
const spanName = type === 'middleware'
|
|
52
|
+
? `Restify middleware ${handler.name || 'anonymous'}`
|
|
53
|
+
: `Restify ${httpMethod} ${routePath}`;
|
|
54
|
+
|
|
55
|
+
const span = startCapturedSpan(
|
|
56
|
+
spanName,
|
|
57
|
+
'function',
|
|
58
|
+
{
|
|
59
|
+
'restify.type': type,
|
|
60
|
+
'http.route': routePath,
|
|
61
|
+
'http.method': httpMethod,
|
|
62
|
+
framework: 'restify',
|
|
63
|
+
},
|
|
64
|
+
options
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (!span) return handler.call(this, req, res, next);
|
|
68
|
+
|
|
69
|
+
return runWithCapturedSpan(span, () => {
|
|
70
|
+
// Wrap next() to end span when handler passes control
|
|
71
|
+
const wrappedNext = function (...args: any[]) {
|
|
72
|
+
const hasError = args.length > 0 && args[0] instanceof Error;
|
|
73
|
+
if (hasError) {
|
|
74
|
+
const err = args[0];
|
|
75
|
+
span.end(err?.statusCode || 500, {
|
|
76
|
+
'error.message': err.message,
|
|
77
|
+
'error.type': err.name || 'Error',
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
span.end(0);
|
|
81
|
+
}
|
|
82
|
+
return next?.(...args);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = handler.call(this, req, res, wrappedNext);
|
|
87
|
+
|
|
88
|
+
// Handle async handlers returning promises
|
|
89
|
+
if (result && typeof result.then === 'function') {
|
|
90
|
+
return result.then(
|
|
91
|
+
(val: any) => val,
|
|
92
|
+
(error: any) => {
|
|
93
|
+
span.end(error?.statusCode || 500, {
|
|
94
|
+
'error.message': error?.message,
|
|
95
|
+
'error.type': error?.name || 'Error',
|
|
96
|
+
});
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
span.end(error?.statusCode || 500, {
|
|
105
|
+
'error.message': error?.message,
|
|
106
|
+
'error.type': error?.name || 'Error',
|
|
107
|
+
});
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
(wrapped as any).__senzorWrapped = true;
|
|
114
|
+
return wrapped;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Server route method patching
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
const patchRestifyServer = (restify: any, options?: SenzorOptions) => {
|
|
122
|
+
// restify.createServer() returns a Server instance
|
|
123
|
+
// We need to patch Server.prototype
|
|
124
|
+
|
|
125
|
+
let ServerProto: any;
|
|
126
|
+
|
|
127
|
+
// Try to get Server prototype from a temp server
|
|
128
|
+
try {
|
|
129
|
+
const tempServer = restify.createServer({ name: '__senzor_probe' });
|
|
130
|
+
ServerProto = Object.getPrototypeOf(tempServer);
|
|
131
|
+
// Close the temp server immediately
|
|
132
|
+
try { tempServer.close(); } catch { }
|
|
133
|
+
} catch { }
|
|
134
|
+
|
|
135
|
+
// Fallback: try restify.Server
|
|
136
|
+
if (!ServerProto) {
|
|
137
|
+
ServerProto = restify?.Server?.prototype;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!ServerProto) return;
|
|
141
|
+
|
|
142
|
+
// Patch route registration methods
|
|
143
|
+
for (const method of HTTP_METHODS) {
|
|
144
|
+
if (typeof ServerProto[method] !== 'function') continue;
|
|
145
|
+
|
|
146
|
+
patchMethod(
|
|
147
|
+
ServerProto,
|
|
148
|
+
method,
|
|
149
|
+
`senzor.restify.server.${method}`,
|
|
150
|
+
(original) =>
|
|
151
|
+
function patchedRouteMethod(this: any, ...args: any[]) {
|
|
152
|
+
// Restify route methods accept:
|
|
153
|
+
// server.get(path, handler1, handler2, ...)
|
|
154
|
+
// server.get({ path, version }, handler1, handler2, ...)
|
|
155
|
+
// server.get(path, [handler1, handler2])
|
|
156
|
+
|
|
157
|
+
let path = '/';
|
|
158
|
+
|
|
159
|
+
// Extract path from first argument
|
|
160
|
+
if (typeof args[0] === 'string') {
|
|
161
|
+
path = args[0];
|
|
162
|
+
} else if (args[0] && typeof args[0] === 'object') {
|
|
163
|
+
path = args[0].path || args[0].url || '/';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Wrap all handler arguments
|
|
167
|
+
for (let i = 0; i < args.length; i++) {
|
|
168
|
+
if (typeof args[i] === 'function') {
|
|
169
|
+
args[i] = wrapHandler(args[i], method, path, 'route_handler', options);
|
|
170
|
+
} else if (Array.isArray(args[i])) {
|
|
171
|
+
args[i] = args[i].map((h: any) =>
|
|
172
|
+
typeof h === 'function' ? wrapHandler(h, method, path, 'route_handler', options) : h
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return original.apply(this, args);
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Patch use() for middleware spans (optional)
|
|
183
|
+
if (options?.captureMiddlewareSpans !== false && typeof ServerProto.use === 'function') {
|
|
184
|
+
patchMethod(
|
|
185
|
+
ServerProto,
|
|
186
|
+
'use',
|
|
187
|
+
'senzor.restify.server.use',
|
|
188
|
+
(original) =>
|
|
189
|
+
function patchedUse(this: any, ...args: any[]) {
|
|
190
|
+
for (let i = 0; i < args.length; i++) {
|
|
191
|
+
if (typeof args[i] === 'function') {
|
|
192
|
+
args[i] = wrapHandler(args[i], 'use', '*', 'middleware', options);
|
|
193
|
+
} else if (Array.isArray(args[i])) {
|
|
194
|
+
args[i] = args[i].map((h: any) =>
|
|
195
|
+
typeof h === 'function' ? wrapHandler(h, 'use', '*', 'middleware', options) : h
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return original.apply(this, args);
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Public API
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
export const instrumentRestify = (options?: SenzorOptions) => {
|
|
210
|
+
hookRequire('restify', (exports: any) => {
|
|
211
|
+
patchRestifyServer(exports, options);
|
|
212
|
+
});
|
|
213
|
+
};
|