@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +479 -398
  3. package/dist/index.d.mts +5 -0
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.global.js +1 -1
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/register.js +1 -1
  12. package/dist/register.js.map +1 -1
  13. package/dist/register.mjs +1 -1
  14. package/dist/register.mjs.map +1 -1
  15. package/package.json +1 -1
  16. package/src/core/client.ts +57 -0
  17. package/src/core/transport.ts +20 -3
  18. package/src/core/types.ts +5 -1
  19. package/src/index.ts +4 -0
  20. package/src/instrumentation/amqplib.ts +371 -0
  21. package/src/instrumentation/anthropic.ts +245 -0
  22. package/src/instrumentation/aws-sdk.ts +403 -0
  23. package/src/instrumentation/azure-openai.ts +177 -0
  24. package/src/instrumentation/bunyan.ts +93 -0
  25. package/src/instrumentation/cassandra.ts +367 -0
  26. package/src/instrumentation/cohere.ts +227 -0
  27. package/src/instrumentation/connect.ts +200 -0
  28. package/src/instrumentation/dataloader.ts +291 -0
  29. package/src/instrumentation/dns.ts +220 -0
  30. package/src/instrumentation/firebase.ts +445 -0
  31. package/src/instrumentation/fs.ts +260 -0
  32. package/src/instrumentation/generic-pool.ts +317 -0
  33. package/src/instrumentation/google-genai.ts +426 -0
  34. package/src/instrumentation/graphql.ts +434 -0
  35. package/src/instrumentation/grpc.ts +666 -0
  36. package/src/instrumentation/hapi.ts +257 -0
  37. package/src/instrumentation/kafka.ts +360 -0
  38. package/src/instrumentation/knex.ts +249 -0
  39. package/src/instrumentation/lru-memoizer.ts +175 -0
  40. package/src/instrumentation/memcached.ts +190 -0
  41. package/src/instrumentation/mistral.ts +254 -0
  42. package/src/instrumentation/nestjs.ts +243 -0
  43. package/src/instrumentation/net.ts +171 -0
  44. package/src/instrumentation/openai.ts +281 -0
  45. package/src/instrumentation/pino.ts +170 -0
  46. package/src/instrumentation/restify.ts +213 -0
  47. package/src/instrumentation/runtime.ts +352 -0
  48. package/src/instrumentation/socketio.ts +272 -0
  49. package/src/instrumentation/tedious.ts +509 -0
  50. package/src/instrumentation/winston.ts +149 -0
  51. package/src/register.ts +22 -3
  52. package/src/wrappers/lambda.ts +417 -0
  53. package/tsup.config.ts +3 -3
  54. 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
+ };