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