@senzops/apm-node 1.1.11 → 1.1.14

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.
@@ -1,45 +1,195 @@
1
1
  import type { SenzorClient } from '../core/client';
2
2
  import { hookRequire } from './hook';
3
+ import { Context } from '../core/context';
3
4
 
4
- export const instrumentBullMQ = (client: SenzorClient, debug: boolean) => {
5
- hookRequire('bullmq', (bullExports) => {
5
+ const PATCHED =
6
+ Symbol.for(
7
+ 'senzor.bullmq.patched'
8
+ );
6
9
 
7
- const patchWorker = (target: any) => {
8
- if (!target || !target.Worker || !target.Worker.prototype.processJob || target.Worker.prototype.processJob.__senzorPatched) return;
10
+ function patchWorker(
11
+ target: any,
12
+ client: SenzorClient,
13
+ debug: boolean
14
+ ) {
9
15
 
10
- const originalProcessJob = target.Worker.prototype.processJob;
16
+ if (
17
+ !target?.Worker?.prototype
18
+ ) {
19
+ return;
20
+ }
11
21
 
12
- target.Worker.prototype.processJob = async function (job: any) {
13
- const queueDelay = job.timestamp ? Date.now() - job.timestamp : 0;
14
- const attempts = (job.attemptsMade || 0) + 1;
15
- const taskName = job.name === '__default__' ? job.queueName : `${job.queueName}:${job.name}`;
22
+ const proto =
23
+ target.Worker.prototype;
24
+
25
+ const original =
26
+ proto.processJob;
27
+
28
+ if (
29
+ typeof original !==
30
+ 'function' ||
31
+ original[PATCHED]
32
+ ) {
33
+ return;
34
+ }
35
+
36
+ proto.processJob =
37
+ async function (
38
+ job: any
39
+ ) {
40
+
41
+ const queueDelay =
42
+ job.timestamp
43
+ ? Date.now() -
44
+ job.timestamp
45
+ : 0;
46
+
47
+ const currentAttempt =
48
+ (job.attemptsMade || 0)
49
+ + 1;
50
+
51
+ const maxAttempts =
52
+ job.opts?.attempts
53
+ ?? 1;
54
+
55
+ const isFinal =
56
+ currentAttempt >=
57
+ maxAttempts;
58
+
59
+ const taskName =
60
+ job.name ===
61
+ '__default__'
62
+ ? job.queueName
63
+ : `${job.queueName}:${job.name}`;
64
+
65
+ return client.startTask(
66
+
67
+ taskName,
68
+
69
+ 'queue',
70
+
71
+ {
72
+ queueDelay,
73
+ attempts:
74
+ currentAttempt,
75
+ isDeadLetter: false,
76
+ metadata: {
77
+ jobId: job.id,
78
+ queueName:
79
+ job.queueName,
80
+ maxAttempts
81
+ }
82
+ },
83
+
84
+ async () => {
85
+
86
+ try {
87
+
88
+ const result =
89
+ await original.call(
90
+ this,
91
+ job
92
+ );
93
+
94
+ client.endTask(
95
+ 'success'
96
+ );
97
+
98
+ return result;
99
+
100
+ }
101
+ catch (error) {
16
102
 
17
- return client.startTask(
18
- taskName,
19
- 'queue',
20
- { queueDelay, attempts, metadata: { jobId: job.id, queueName: job.queueName } },
21
- async () => {
22
103
  try {
23
- const result = await originalProcessJob.apply(this, arguments);
24
- client.endTask('success');
25
- return result;
26
- } catch (error) {
27
- client.captureError(error, { queueName: job.queueName, jobId: job.id });
28
- client.endTask('failed');
29
- throw error;
104
+
105
+ const ctx =
106
+ Context.current();
107
+
108
+ if (
109
+ ctx &&
110
+ ctx.contextType === 'task' &&
111
+ isFinal
112
+ ) {
113
+
114
+ ctx.data
115
+ .isDeadLetter =
116
+ true;
117
+
118
+ }
119
+
30
120
  }
121
+ catch { }
122
+
123
+ client.captureError(
124
+ error,
125
+ {
126
+ queueName:
127
+ job.queueName,
128
+ jobId: job.id,
129
+ isDeadLetter:
130
+ isFinal
131
+ }
132
+ );
133
+
134
+ client.endTask(
135
+ 'failed'
136
+ );
137
+
138
+ throw error;
139
+
31
140
  }
32
- );
33
- };
34
141
 
35
- Object.defineProperty(target.Worker.prototype.processJob, '__senzorPatched', { value: true, enumerable: false, writable: true });
36
- if (debug) console.log('[Senzor] BullMQ Worker successfully instrumented');
37
- };
142
+ }
143
+
144
+ );
38
145
 
39
- patchWorker(bullExports);
146
+ };
40
147
 
41
- if (bullExports.default) {
42
- patchWorker(bullExports.default);
148
+ Object.defineProperty(
149
+ proto.processJob,
150
+ PATCHED,
151
+ {
152
+ value: true
43
153
  }
44
- });
45
- };
154
+ );
155
+
156
+ if (debug) {
157
+
158
+ console.log(
159
+ '[Senzor] BullMQ instrumented'
160
+ );
161
+
162
+ }
163
+
164
+ }
165
+
166
+ export const instrumentBullMQ =
167
+ (
168
+ client: SenzorClient,
169
+ debug: boolean
170
+ ) => {
171
+
172
+ hookRequire(
173
+ 'bullmq',
174
+ (exports: any) => {
175
+
176
+ patchWorker(
177
+ exports,
178
+ client,
179
+ debug
180
+ );
181
+
182
+ if (exports?.default) {
183
+
184
+ patchWorker(
185
+ exports.default,
186
+ client,
187
+ debug
188
+ );
189
+
190
+ }
191
+
192
+ }
193
+ );
194
+
195
+ };
@@ -1,41 +1,204 @@
1
1
  import type { SenzorClient } from '../core/client';
2
2
  import { hookRequire } from './hook';
3
3
 
4
- export const instrumentNodeCron = (client: SenzorClient, debug: boolean) => {
5
- hookRequire('node-cron', (cronExports) => {
4
+ const PATCHED =
5
+ Symbol.for('senzor.cron.patched');
6
6
 
7
- // Abstracted patcher so we can apply it to both the root and the .default export
8
- const patchSchedule = (target: any) => {
9
- if (!target || typeof target.schedule !== 'function' || target.__senzorPatched) return;
7
+ type CronHandler =
8
+ (...args: unknown[]) => unknown;
10
9
 
11
- const originalSchedule = target.schedule;
10
+ type CronSchedule =
11
+ (
12
+ expression: string,
13
+ handler: CronHandler,
14
+ options?: unknown
15
+ ) => unknown;
12
16
 
13
- target.schedule = function (expression: string, func: (...args: any[]) => any, options: any) {
14
- // Handle node-cron's dynamic options argument (can be string or object)
15
- const optsObj = typeof options === 'object' ? options : { timezone: options };
16
- const taskName = optsObj?.name || `cron: ${expression}`;
17
+ function normalizeOptions(
18
+ options: unknown
19
+ ): Record<string, unknown> {
17
20
 
18
- const wrappedFunc = client.wrapTask(
19
- taskName,
20
- 'cron',
21
- { metadata: optsObj },
22
- func
21
+ if (
22
+ typeof options === 'object' &&
23
+ options !== null
24
+ ) {
25
+ return options as Record<
26
+ string,
27
+ unknown
28
+ >;
29
+ }
30
+
31
+ // backward compatibility with:
32
+ // cron.schedule(expr, fn, "UTC")
33
+ if (options) {
34
+ return { timezone: options };
35
+ }
36
+
37
+ return {};
38
+
39
+ }
40
+
41
+ function patchTarget(
42
+ target: Record<string, unknown>,
43
+ client: SenzorClient,
44
+ debug: boolean
45
+ ): void {
46
+
47
+ const schedule =
48
+ target.schedule as
49
+ CronSchedule | undefined;
50
+
51
+ if (
52
+ typeof schedule !== 'function' ||
53
+ (schedule as any)[PATCHED]
54
+ ) {
55
+ return;
56
+ }
57
+
58
+ const original =
59
+ schedule;
60
+
61
+ const wrapped: CronSchedule =
62
+ function (
63
+ this: unknown,
64
+ expression,
65
+ handler,
66
+ options
67
+ ) {
68
+
69
+ if (
70
+ typeof handler !==
71
+ 'function'
72
+ ) {
73
+
74
+ return original.call(
75
+ this,
76
+ expression,
77
+ handler,
78
+ options
79
+ );
80
+
81
+ }
82
+
83
+ try {
84
+
85
+ const opts =
86
+ normalizeOptions(
87
+ options
88
+ );
89
+
90
+ const taskName =
91
+ (opts as any)?.name ??
92
+ `cron: ${expression}`;
93
+
94
+ const wrappedHandler =
95
+ client.wrapTask(
96
+ taskName,
97
+ 'cron',
98
+ {
99
+ expression,
100
+ metadata: opts
101
+ },
102
+ handler
103
+ );
104
+
105
+ return original.call(
106
+ this,
107
+ expression,
108
+ wrappedHandler,
109
+ options
110
+ );
111
+
112
+ }
113
+ catch (err) {
114
+
115
+ if (debug) {
116
+
117
+ console.error(
118
+ '[Senzor] cron wrap failed',
119
+ err
120
+ );
121
+
122
+ }
123
+
124
+ return original.call(
125
+ this,
126
+ expression,
127
+ handler,
128
+ options
23
129
  );
24
130
 
25
- return originalSchedule.call(this, expression, wrappedFunc, options);
26
- };
131
+ }
27
132
 
28
- // Safely mark as patched to prevent infinite loops
29
- Object.defineProperty(target, '__senzorPatched', { value: true, enumerable: false, writable: true });
30
- if (debug) console.log('[Senzor] Node-Cron successfully instrumented');
31
133
  };
32
134
 
33
- // Apply patch to root (for const cron = require('node-cron'))
34
- patchSchedule(cronExports);
135
+ Object.defineProperty(
136
+ wrapped,
137
+ PATCHED,
138
+ {
139
+ value: true,
140
+ enumerable: false
141
+ }
142
+ );
143
+
144
+ // Some ESM namespace exports are frozen
145
+ try {
146
+
147
+ target.schedule =
148
+ wrapped;
149
+
150
+ }
151
+ catch {
152
+
153
+ if (debug) {
154
+
155
+ console.warn(
156
+ '[Senzor] unable to patch cron schedule (readonly export)'
157
+ );
35
158
 
36
- // Apply patch to default (for import cron from 'node-cron')
37
- if (cronExports.default) {
38
- patchSchedule(cronExports.default);
39
159
  }
40
- });
41
- };
160
+
161
+ }
162
+
163
+ if (debug) {
164
+
165
+ console.log(
166
+ '[Senzor] node-cron instrumented'
167
+ );
168
+
169
+ }
170
+
171
+ }
172
+
173
+ export const instrumentNodeCron =
174
+ (
175
+ client: SenzorClient,
176
+ debug: boolean
177
+ ): void => {
178
+
179
+ hookRequire(
180
+ 'node-cron',
181
+ (exports: any) => {
182
+
183
+ if (!exports) return;
184
+
185
+ patchTarget(
186
+ exports,
187
+ client,
188
+ debug
189
+ );
190
+
191
+ if (exports.default) {
192
+
193
+ patchTarget(
194
+ exports.default,
195
+ client,
196
+ debug
197
+ );
198
+
199
+ }
200
+
201
+ }
202
+ );
203
+
204
+ };
@@ -1,24 +1,247 @@
1
1
  import Module from 'module';
2
2
 
3
- export const hookRequire = (moduleName: string, onRequire: (exports: any) => void) => {
4
- // 1. If it was already loaded (e.g., imported at the top of the file before init)
3
+ const SENZOR_PATCHED =
4
+ Symbol.for('senzor.require.patched');
5
+
6
+ const SENZOR_HOOKS =
7
+ Symbol.for('senzor.require.hooks');
8
+
9
+ type HookFn =
10
+ (exports: unknown) => void;
11
+
12
+ type HookMap =
13
+ Map<string, HookFn[]>;
14
+
15
+ function getHookRegistry(): HookMap {
16
+
17
+ const mod =
18
+ Module as unknown as Record<
19
+ symbol,
20
+ HookMap
21
+ >;
22
+
23
+ if (!mod[SENZOR_HOOKS]) {
24
+
25
+ Object.defineProperty(
26
+ mod,
27
+ SENZOR_HOOKS,
28
+ {
29
+ value: new Map(),
30
+ enumerable: false
31
+ }
32
+ );
33
+
34
+ }
35
+
36
+ return mod[SENZOR_HOOKS];
37
+
38
+ }
39
+
40
+ function runHooks(
41
+ moduleName: string,
42
+ exports: unknown
43
+ ) {
44
+
45
+ const registry =
46
+ (Module as unknown as Record<
47
+ symbol,
48
+ HookMap
49
+ >)[SENZOR_HOOKS];
50
+
51
+ if (!registry) return;
52
+
53
+ const hooks =
54
+ registry.get(moduleName);
55
+
56
+ if (!hooks?.length) return;
57
+
58
+ for (const hook of hooks) {
59
+
60
+ try {
61
+ hook(exports);
62
+ }
63
+ catch (err) {
64
+
65
+ console.error(
66
+ `[Senzor] instrumentation failed for ${moduleName}`,
67
+ err
68
+ );
69
+
70
+ }
71
+
72
+ }
73
+
74
+ }
75
+
76
+ function patchLoaderOnce() {
77
+
78
+ const mod =
79
+ Module as unknown as any;
80
+
81
+ if (mod[SENZOR_PATCHED]) {
82
+ return;
83
+ }
84
+
85
+ const previousLoad =
86
+ mod._load;
87
+
88
+ mod._load =
89
+ function patchedLoad(
90
+ request: string,
91
+ parent: unknown,
92
+ isMain: boolean
93
+ ) {
94
+
95
+ const exports =
96
+ previousLoad.apply(
97
+ this,
98
+ arguments
99
+ );
100
+
101
+ runHooks(
102
+ request,
103
+ exports
104
+ );
105
+
106
+ return exports;
107
+
108
+ };
109
+
110
+ Object.defineProperty(
111
+ mod,
112
+ SENZOR_PATCHED,
113
+ {
114
+ value: true,
115
+ enumerable: false
116
+ }
117
+ );
118
+
119
+ }
120
+
121
+ function patchCached(
122
+ moduleName: string,
123
+ hook: HookFn
124
+ ) {
125
+
5
126
  try {
6
- const resolvedPath = require.resolve(moduleName);
7
- const cached = require.cache[resolvedPath];
8
- if (cached && cached.exports) {
9
- onRequire(cached.exports);
127
+
128
+ const resolved =
129
+ require.resolve(
130
+ moduleName
131
+ );
132
+
133
+ const cached =
134
+ require.cache?.[
135
+ resolved
136
+ ];
137
+
138
+ if (cached?.exports) {
139
+
140
+ hook(
141
+ cached.exports
142
+ );
143
+
144
+ }
145
+
146
+ }
147
+ catch { }
148
+
149
+ }
150
+
151
+ function tryRequire(
152
+ moduleName: string,
153
+ hook: HookFn
154
+ ) {
155
+
156
+ try {
157
+
158
+ const mod =
159
+ require(moduleName);
160
+
161
+ if (mod) {
162
+ hook(mod);
10
163
  }
11
- } catch (e) {
12
- // Silently ignore if module is not installed
164
+
13
165
  }
166
+ catch { }
167
+
168
+ }
169
+
170
+ function retryPatch(
171
+ moduleName: string,
172
+ hook: HookFn
173
+ ) {
174
+
175
+ let attempts = 0;
176
+
177
+ const max = 5;
178
+
179
+ const timer =
180
+ setInterval(() => {
181
+
182
+ attempts++;
183
+
184
+ try {
185
+
186
+ const mod =
187
+ require(moduleName);
188
+
189
+ if (mod) {
190
+
191
+ hook(mod);
192
+
193
+ clearInterval(timer);
194
+
195
+ }
196
+
197
+ }
198
+ catch { }
199
+
200
+ if (attempts >= max) {
201
+ clearInterval(timer);
202
+ }
203
+
204
+ }, 200);
205
+
206
+ }
207
+
208
+ export const hookRequire =
209
+ (
210
+ moduleName: string,
211
+ onRequire: HookFn
212
+ ) => {
213
+
214
+ const registry =
215
+ getHookRegistry();
216
+
217
+ if (!registry.has(moduleName)) {
218
+
219
+ registry.set(
220
+ moduleName,
221
+ []
222
+ );
14
223
 
15
- // 2. Intercept future requires
16
- const originalLoad = (Module as any)._load;
17
- (Module as any)._load = function (request: string, parent: any, isMain: boolean) {
18
- const exports = originalLoad.apply(this, arguments);
19
- if (request === moduleName && exports) {
20
- onRequire(exports);
21
224
  }
22
- return exports;
23
- };
24
- };
225
+
226
+ registry
227
+ .get(moduleName)!
228
+ .push(onRequire);
229
+
230
+ patchLoaderOnce();
231
+
232
+ patchCached(
233
+ moduleName,
234
+ onRequire
235
+ );
236
+
237
+ tryRequire(
238
+ moduleName,
239
+ onRequire
240
+ );
241
+
242
+ retryPatch(
243
+ moduleName,
244
+ onRequire
245
+ );
246
+
247
+ };