@sentienguard/apm 1.0.4 → 1.0.6

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,254 +1,264 @@
1
- /**
2
- * Circuit Breaker Module
3
- *
4
- * Provides circuit breaker functionality using Opossum library.
5
- * Wraps operations (especially database calls) to prevent cascade failures.
6
- *
7
- * Circuit States:
8
- * - CLOSED: Normal operation, requests pass through
9
- * - OPEN: Circuit tripped due to failures, requests fail fast
10
- * - HALF-OPEN: Testing if service recovered, limited requests allowed
11
- *
12
- * Usage:
13
- * import { createBreaker, wrapMongoOperation } from './circuitBreaker.js';
14
- *
15
- * // Wrap an async operation
16
- * const breaker = createBreaker(async () => await db.query(), { name: 'db-query' });
17
- * const result = await breaker.fire();
18
- *
19
- * // Or use the MongoDB helper
20
- * const findUsers = wrapMongoOperation('User.find', async () => User.find());
21
- * const users = await findUsers();
22
- */
23
-
24
- import config, { debug, warn } from './config.js';
25
- import { getAggregator } from './aggregator.js';
26
-
27
- // Store active circuit breakers
28
- const breakers = new Map();
29
-
30
- // Check if opossum is available
31
- let CircuitBreaker = null;
32
-
33
- /**
34
- * Initialize Opossum (lazy load)
35
- */
36
- async function initOpossum() {
37
- if (CircuitBreaker) return true;
38
-
39
- try {
40
- const opossum = await import('opossum');
41
- CircuitBreaker = opossum.default || opossum;
42
- debug('Opossum circuit breaker loaded');
43
- return true;
44
- } catch (err) {
45
- debug('Opossum not installed. Circuit breaker disabled. Install with: npm install opossum');
46
- return false;
47
- }
48
- }
49
-
50
- /**
51
- * Default circuit breaker options
52
- */
53
- function getDefaultOptions() {
54
- return {
55
- timeout: config.circuitBreaker.timeout,
56
- errorThresholdPercentage: config.circuitBreaker.errorThresholdPercentage,
57
- resetTimeout: config.circuitBreaker.resetTimeout,
58
- volumeThreshold: config.circuitBreaker.volumeThreshold
59
- };
60
- }
61
-
62
- /**
63
- * Create a circuit breaker for an async operation
64
- *
65
- * @param {Function} asyncFn - The async function to wrap
66
- * @param {Object} options - Circuit breaker options
67
- * @param {string} options.name - Name for the circuit (used for metrics)
68
- * @param {number} options.timeout - Timeout in ms
69
- * @param {number} options.errorThresholdPercentage - Error threshold to trip
70
- * @param {number} options.resetTimeout - Time before attempting recovery
71
- * @param {Function} options.fallback - Fallback function when circuit is open
72
- * @returns {Object} Circuit breaker instance or wrapped function if opossum unavailable
73
- */
74
- export async function createBreaker(asyncFn, options = {}) {
75
- if (!config.circuitBreaker.enabled) {
76
- debug('Circuit breaker disabled, returning passthrough');
77
- return createPassthrough(asyncFn);
78
- }
79
-
80
- const initialized = await initOpossum();
81
- if (!initialized) {
82
- return createPassthrough(asyncFn);
83
- }
84
-
85
- const name = options.name || 'unnamed';
86
- const breakerOptions = {
87
- ...getDefaultOptions(),
88
- ...options,
89
- name
90
- };
91
-
92
- const breaker = new CircuitBreaker(asyncFn, breakerOptions);
93
- const aggregator = getAggregator();
94
-
95
- // Attach event listeners for metrics
96
- breaker.on('open', () => {
97
- aggregator.recordCircuitState(name, 'open');
98
- warn(`Circuit breaker OPEN: ${name}`);
99
- });
100
-
101
- breaker.on('halfOpen', () => {
102
- aggregator.recordCircuitState(name, 'halfOpen');
103
- debug(`Circuit breaker HALF-OPEN: ${name}`);
104
- });
105
-
106
- breaker.on('close', () => {
107
- aggregator.recordCircuitState(name, 'close');
108
- debug(`Circuit breaker CLOSED: ${name}`);
109
- });
110
-
111
- breaker.on('fallback', (result) => {
112
- debug(`Circuit breaker fallback executed: ${name}`);
113
- });
114
-
115
- breaker.on('timeout', () => {
116
- debug(`Circuit breaker timeout: ${name}`);
117
- });
118
-
119
- breaker.on('reject', () => {
120
- debug(`Circuit breaker rejected (circuit open): ${name}`);
121
- });
122
-
123
- // Register fallback if provided
124
- if (options.fallback) {
125
- breaker.fallback(options.fallback);
126
- }
127
-
128
- // Store reference
129
- breakers.set(name, breaker);
130
-
131
- return breaker;
132
- }
133
-
134
- /**
135
- * Create a passthrough wrapper when circuit breaker is disabled
136
- */
137
- function createPassthrough(asyncFn) {
138
- return {
139
- fire: (...args) => asyncFn(...args),
140
- fallback: () => {},
141
- on: () => {},
142
- isOpen: () => false,
143
- isClosed: () => true,
144
- stats: { fires: 0, failures: 0, successes: 0 }
145
- };
146
- }
147
-
148
- /**
149
- * Create a circuit breaker specifically for MongoDB operations
150
- *
151
- * @param {string} operationName - Name of the operation (e.g., 'User.find')
152
- * @param {Function} mongoOperation - The MongoDB operation function
153
- * @param {Object} options - Additional options
154
- * @returns {Function} Wrapped function that uses circuit breaker
155
- */
156
- export async function wrapMongoOperation(operationName, mongoOperation, options = {}) {
157
- const breaker = await createBreaker(mongoOperation, {
158
- name: `mongodb:${operationName}`,
159
- // MongoDB-specific defaults
160
- timeout: options.timeout || config.circuitBreaker.timeout,
161
- errorThresholdPercentage: options.errorThresholdPercentage || 50,
162
- resetTimeout: options.resetTimeout || 30000,
163
- ...options
164
- });
165
-
166
- // Return a function that fires the breaker
167
- return async function wrappedOperation(...args) {
168
- return breaker.fire(...args);
169
- };
170
- }
171
-
172
- /**
173
- * Get a registered circuit breaker by name
174
- *
175
- * @param {string} name - Circuit breaker name
176
- * @returns {Object|null} Circuit breaker instance or null
177
- */
178
- export function getBreaker(name) {
179
- return breakers.get(name) || null;
180
- }
181
-
182
- /**
183
- * Get all registered circuit breakers
184
- *
185
- * @returns {Map} Map of circuit breaker instances
186
- */
187
- export function getAllBreakers() {
188
- return new Map(breakers);
189
- }
190
-
191
- /**
192
- * Get circuit breaker statistics
193
- *
194
- * @param {string} name - Circuit breaker name (optional, returns all if not provided)
195
- * @returns {Object} Statistics object
196
- */
197
- export function getBreakerStats(name) {
198
- if (name) {
199
- const breaker = breakers.get(name);
200
- if (!breaker) return null;
201
-
202
- return {
203
- name,
204
- state: breaker.opened ? 'open' : (breaker.halfOpen ? 'halfOpen' : 'closed'),
205
- stats: breaker.stats
206
- };
207
- }
208
-
209
- // Return stats for all breakers
210
- const allStats = {};
211
- for (const [breakerName, breaker] of breakers) {
212
- allStats[breakerName] = {
213
- state: breaker.opened ? 'open' : (breaker.halfOpen ? 'halfOpen' : 'closed'),
214
- stats: breaker.stats
215
- };
216
- }
217
- return allStats;
218
- }
219
-
220
- /**
221
- * Reset a circuit breaker (close it)
222
- *
223
- * @param {string} name - Circuit breaker name
224
- */
225
- export function resetBreaker(name) {
226
- const breaker = breakers.get(name);
227
- if (breaker && typeof breaker.close === 'function') {
228
- breaker.close();
229
- debug(`Circuit breaker reset: ${name}`);
230
- }
231
- }
232
-
233
- /**
234
- * Shutdown all circuit breakers
235
- */
236
- export function shutdownBreakers() {
237
- for (const [name, breaker] of breakers) {
238
- if (typeof breaker.shutdown === 'function') {
239
- breaker.shutdown();
240
- }
241
- }
242
- breakers.clear();
243
- debug('All circuit breakers shutdown');
244
- }
245
-
246
- export default {
247
- createBreaker,
248
- wrapMongoOperation,
249
- getBreaker,
250
- getAllBreakers,
251
- getBreakerStats,
252
- resetBreaker,
253
- shutdownBreakers
254
- };
1
+ /**
2
+ * Circuit Breaker Module
3
+ *
4
+ * Provides circuit breaker functionality using Opossum library.
5
+ * Wraps operations (especially database calls) to prevent cascade failures.
6
+ *
7
+ * Circuit States:
8
+ * - CLOSED: Normal operation, requests pass through
9
+ * - OPEN: Circuit tripped due to failures, requests fail fast
10
+ * - HALF-OPEN: Testing if service recovered, limited requests allowed
11
+ *
12
+ * Usage:
13
+ * import { createBreaker, wrapMongoOperation } from './circuitBreaker.js';
14
+ *
15
+ * // Wrap an async operation
16
+ * const breaker = createBreaker(async () => await db.query(), { name: 'db-query' });
17
+ * const result = await breaker.fire();
18
+ *
19
+ * // Or use the MongoDB helper
20
+ * const findUsers = wrapMongoOperation('User.find', async () => User.find());
21
+ * const users = await findUsers();
22
+ */
23
+
24
+ import config, { debug, warn } from './config.js';
25
+ import { getAggregator } from './aggregator.js';
26
+
27
+ // Store active circuit breakers
28
+ const breakers = new Map();
29
+
30
+ // Check if opossum is available
31
+ let CircuitBreaker = null;
32
+
33
+ /**
34
+ * Initialize Opossum (lazy load)
35
+ */
36
+ async function initOpossum() {
37
+ if (CircuitBreaker) return true;
38
+
39
+ try {
40
+ const opossum = await import('opossum');
41
+ CircuitBreaker = opossum.default || opossum;
42
+ debug('Opossum circuit breaker loaded');
43
+ return true;
44
+ } catch (err) {
45
+ debug('Opossum not installed. Circuit breaker disabled. Install with: npm install opossum');
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Default circuit breaker options
52
+ */
53
+ function getDefaultOptions() {
54
+ return {
55
+ timeout: config.circuitBreaker.timeout,
56
+ errorThresholdPercentage: config.circuitBreaker.errorThresholdPercentage,
57
+ resetTimeout: config.circuitBreaker.resetTimeout,
58
+ volumeThreshold: config.circuitBreaker.volumeThreshold
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Create a circuit breaker for an async operation
64
+ *
65
+ * @param {Function} asyncFn - The async function to wrap
66
+ * @param {Object} options - Circuit breaker options
67
+ * @param {string} options.name - Name for the circuit (used for metrics)
68
+ * @param {number} options.timeout - Timeout in ms
69
+ * @param {number} options.errorThresholdPercentage - Error threshold to trip
70
+ * @param {number} options.resetTimeout - Time before attempting recovery
71
+ * @param {Function} options.fallback - Fallback function when circuit is open
72
+ * @returns {Object} Circuit breaker instance or wrapped function if opossum unavailable
73
+ */
74
+ export async function createBreaker(asyncFn, options = {}) {
75
+ if (!config.circuitBreaker.enabled) {
76
+ debug('Circuit breaker disabled, returning passthrough');
77
+ return createPassthrough(asyncFn);
78
+ }
79
+
80
+ const initialized = await initOpossum();
81
+ if (!initialized) {
82
+ return createPassthrough(asyncFn);
83
+ }
84
+
85
+ const name = options.name || 'unnamed';
86
+ const breakerOptions = {
87
+ ...getDefaultOptions(),
88
+ ...options,
89
+ name
90
+ };
91
+
92
+ const breaker = new CircuitBreaker(asyncFn, breakerOptions);
93
+ const aggregator = getAggregator();
94
+
95
+ // Attach event listeners for metrics
96
+ breaker.on('open', () => {
97
+ aggregator.recordCircuitState(name, 'open');
98
+ warn(`Circuit breaker OPEN: ${name}`);
99
+ });
100
+
101
+ breaker.on('halfOpen', () => {
102
+ aggregator.recordCircuitState(name, 'halfOpen');
103
+ debug(`Circuit breaker HALF-OPEN: ${name}`);
104
+ });
105
+
106
+ breaker.on('close', () => {
107
+ aggregator.recordCircuitState(name, 'close');
108
+ debug(`Circuit breaker CLOSED: ${name}`);
109
+ });
110
+
111
+ breaker.on('fallback', (result) => {
112
+ debug(`Circuit breaker fallback executed: ${name}`);
113
+ });
114
+
115
+ breaker.on('timeout', () => {
116
+ debug(`Circuit breaker timeout: ${name}`);
117
+ });
118
+
119
+ breaker.on('reject', () => {
120
+ debug(`Circuit breaker rejected (circuit open): ${name}`);
121
+ });
122
+
123
+ // Register fallback if provided
124
+ if (options.fallback) {
125
+ breaker.fallback(options.fallback);
126
+ }
127
+
128
+ // Store reference
129
+ breakers.set(name, breaker);
130
+
131
+ return breaker;
132
+ }
133
+
134
+ /**
135
+ * Create a passthrough wrapper when circuit breaker is disabled
136
+ */
137
+ function createPassthrough(asyncFn) {
138
+ return {
139
+ fire: (...args) => asyncFn(...args),
140
+ fallback: () => {},
141
+ on: () => {},
142
+ isOpen: () => false,
143
+ isClosed: () => true,
144
+ stats: { fires: 0, failures: 0, successes: 0 }
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Create a circuit breaker specifically for MongoDB operations
150
+ *
151
+ * @param {string} operationName - Name of the operation (e.g., 'User.find')
152
+ * @param {Function} mongoOperation - The MongoDB operation function
153
+ * @param {Object} options - Additional options
154
+ * @returns {Function} Wrapped function that uses circuit breaker
155
+ */
156
+ export async function wrapMongoOperation(operationName, mongoOperation, options = {}) {
157
+ const breaker = await createBreaker(mongoOperation, {
158
+ name: `mongodb:${operationName}`,
159
+ // MongoDB-specific defaults
160
+ timeout: options.timeout || config.circuitBreaker.timeout,
161
+ errorThresholdPercentage: options.errorThresholdPercentage || 50,
162
+ resetTimeout: options.resetTimeout || 30000,
163
+ ...options
164
+ });
165
+
166
+ // Return a function that fires the breaker
167
+ return async function wrappedOperation(...args) {
168
+ return breaker.fire(...args);
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Get a registered circuit breaker by name
174
+ *
175
+ * @param {string} name - Circuit breaker name
176
+ * @returns {Object|null} Circuit breaker instance or null
177
+ */
178
+ export function getBreaker(name) {
179
+ return breakers.get(name) || null;
180
+ }
181
+
182
+ /**
183
+ * Get all registered circuit breakers
184
+ *
185
+ * @returns {Map} Map of circuit breaker instances
186
+ */
187
+ export function getAllBreakers() {
188
+ return new Map(breakers);
189
+ }
190
+
191
+ /**
192
+ * Get circuit breaker statistics
193
+ *
194
+ * @param {string} name - Circuit breaker name (optional, returns all if not provided)
195
+ * @returns {Object} Statistics object
196
+ */
197
+ export function getBreakerStats(name) {
198
+ const getState = (breaker) => {
199
+ if (breaker.opened) {
200
+ return 'open';
201
+ }
202
+ if (breaker.halfOpen) {
203
+ return 'halfOpen';
204
+ }
205
+ return 'closed';
206
+ };
207
+
208
+ if (name) {
209
+ const breaker = breakers.get(name);
210
+ if (!breaker) return null;
211
+
212
+ return {
213
+ name,
214
+ state: getState(breaker),
215
+ stats: breaker.stats
216
+ };
217
+ }
218
+
219
+ // Return stats for all breakers
220
+ const allStats = {};
221
+ for (const [breakerName, breaker] of breakers) {
222
+ allStats[breakerName] = {
223
+ state: getState(breaker),
224
+ stats: breaker.stats
225
+ };
226
+ }
227
+ return allStats;
228
+ }
229
+
230
+ /**
231
+ * Reset a circuit breaker (close it)
232
+ *
233
+ * @param {string} name - Circuit breaker name
234
+ */
235
+ export function resetBreaker(name) {
236
+ const breaker = breakers.get(name);
237
+ if (breaker && typeof breaker.close === 'function') {
238
+ breaker.close();
239
+ debug(`Circuit breaker reset: ${name}`);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Shutdown all circuit breakers
245
+ */
246
+ export function shutdownBreakers() {
247
+ for (const [ breaker] of breakers) {
248
+ if (typeof breaker.shutdown === 'function') {
249
+ breaker.shutdown();
250
+ }
251
+ }
252
+ breakers.clear();
253
+ debug('All circuit breakers shutdown');
254
+ }
255
+
256
+ export default {
257
+ createBreaker,
258
+ wrapMongoOperation,
259
+ getBreaker,
260
+ getAllBreakers,
261
+ getBreakerStats,
262
+ resetBreaker,
263
+ shutdownBreakers
264
+ };