@senzops/apm-node 1.1.12 → 1.1.15

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,70 +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;
22
+ const proto =
23
+ target.Worker.prototype;
14
24
 
15
- // BullMQ increments attemptsMade *after* a failure.
16
- // So the current run attempt is attemptsMade + 1.
17
- const currentAttempt = (job.attemptsMade || 0) + 1;
18
- const maxAttempts = job.opts?.attempts || 1;
25
+ const original =
26
+ proto.processJob;
19
27
 
20
- // If it fails on this run, and it's >= the max allowed attempts, it's entering the DLQ.
21
- const isFinalAttempt = currentAttempt >= maxAttempts;
28
+ if (
29
+ typeof original !==
30
+ 'function' ||
31
+ original[PATCHED]
32
+ ) {
33
+ return;
34
+ }
22
35
 
23
- const taskName = job.name === '__default__' ? job.queueName : `${job.queueName}:${job.name}`;
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) {
24
102
 
25
- return client.startTask(
26
- taskName,
27
- 'queue',
28
- {
29
- queueDelay,
30
- attempts: currentAttempt,
31
- // We preset isDeadLetter to false, but if it throws an error and isFinalAttempt is true,
32
- // we will mutate this in the catch block.
33
- isDeadLetter: false,
34
- metadata: { jobId: job.id, queueName: job.queueName, maxAttempts }
35
- },
36
- async () => {
37
103
  try {
38
- const result = await originalProcessJob.apply(this, arguments);
39
- client.endTask('success');
40
- return result;
41
- } catch (error) {
42
- const context = require('../core/context').Context.current();
43
- if (context && context.contextType === 'task' && isFinalAttempt) {
44
- context.data.isDeadLetter = true; // Flag it as a permanent failure
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
+
45
118
  }
46
119
 
47
- client.captureError(error, {
48
- queueName: job.queueName,
120
+ }
121
+ catch { }
122
+
123
+ client.captureError(
124
+ error,
125
+ {
126
+ queueName:
127
+ job.queueName,
49
128
  jobId: job.id,
50
- isDeadLetter: isFinalAttempt
51
- });
129
+ isDeadLetter:
130
+ isFinal
131
+ }
132
+ );
133
+
134
+ client.endTask(
135
+ 'failed'
136
+ );
137
+
138
+ throw error;
52
139
 
53
- client.endTask('failed');
54
- throw error;
55
- }
56
140
  }
57
- );
58
- };
59
141
 
60
- Object.defineProperty(target.Worker.prototype.processJob, '__senzorPatched', { value: true, enumerable: false, writable: true });
61
- if (debug) console.log('[Senzor] BullMQ Worker successfully instrumented with DLQ tracking');
62
- };
142
+ }
143
+
144
+ );
63
145
 
64
- patchWorker(bullExports);
146
+ };
65
147
 
66
- if (bullExports.default) {
67
- patchWorker(bullExports.default);
148
+ Object.defineProperty(
149
+ proto.processJob,
150
+ PATCHED,
151
+ {
152
+ value: true
68
153
  }
69
- });
70
- };
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
+ };