@onlineapps/error-handler-core 1.0.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.
- package/README.md +125 -0
- package/jest.config.js +23 -0
- package/onlineapps-error-handler-core-1.0.0.tgz +0 -0
- package/package.json +34 -0
- package/src/CircuitBreaker.js +110 -0
- package/src/CompensationHandler.js +104 -0
- package/src/DLQRouter.js +98 -0
- package/src/ErrorClassifier.js +142 -0
- package/src/ErrorHandlerCore.js +329 -0
- package/src/RetryHandler.js +74 -0
- package/src/UnifiedLogSchema.js +105 -0
- package/src/index.js +30 -0
- package/tests/unit/DLQRouter.test.js +94 -0
- package/tests/unit/ErrorClassifier.test.js +82 -0
- package/tests/unit/ErrorHandlerCore.test.js +183 -0
- package/tests/unit/RetryHandler.test.js +81 -0
- package/tests/unit/UnifiedLogSchema.test.js +75 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler Core
|
|
3
|
+
* Main class that orchestrates error handling, logging, retry, circuit breaker, DLQ routing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { ErrorClassifier, ErrorTypes } = require('./ErrorClassifier');
|
|
7
|
+
const UnifiedLogSchema = require('./UnifiedLogSchema');
|
|
8
|
+
const RetryHandler = require('./RetryHandler');
|
|
9
|
+
const CircuitBreakerManager = require('./CircuitBreaker');
|
|
10
|
+
const DLQRouter = require('./DLQRouter');
|
|
11
|
+
const CompensationHandler = require('./CompensationHandler');
|
|
12
|
+
|
|
13
|
+
class ErrorHandlerCore {
|
|
14
|
+
/**
|
|
15
|
+
* Create ErrorHandlerCore instance
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} config - Configuration
|
|
18
|
+
* @param {string} config.serviceName - Service name
|
|
19
|
+
* @param {string} [config.serviceVersion] - Service version
|
|
20
|
+
* @param {string} [config.environment] - Environment
|
|
21
|
+
* @param {Object} config.monitoring - Monitoring instance (monitoring-core)
|
|
22
|
+
* @param {Object} [config.handling] - Error handling configuration
|
|
23
|
+
* @param {number} [config.handling.maxRetries=3] - Max retry attempts
|
|
24
|
+
* @param {number} [config.handling.retryDelay=1000] - Initial retry delay
|
|
25
|
+
* @param {number} [config.handling.retryMultiplier=2] - Backoff multiplier
|
|
26
|
+
* @param {boolean} [config.handling.circuitBreakerEnabled=true] - Enable circuit breaker
|
|
27
|
+
* @param {boolean} [config.handling.dlqEnabled=true] - Enable DLQ routing
|
|
28
|
+
* @param {Object} [config.handling.mqClient] - MQ client for DLQ
|
|
29
|
+
* @param {boolean} [config.handling.compensationEnabled=true] - Enable compensation
|
|
30
|
+
*/
|
|
31
|
+
constructor(config) {
|
|
32
|
+
if (!config.serviceName) {
|
|
33
|
+
throw new Error('serviceName is required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!config.monitoring) {
|
|
37
|
+
throw new Error('monitoring instance is required (monitoring-core)');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.serviceName = config.serviceName;
|
|
41
|
+
this.serviceVersion = config.serviceVersion || '0.0.0';
|
|
42
|
+
this.environment = config.environment || process.env.NODE_ENV || 'development';
|
|
43
|
+
this.monitoring = config.monitoring;
|
|
44
|
+
|
|
45
|
+
const handling = config.handling || {};
|
|
46
|
+
|
|
47
|
+
// Initialize components
|
|
48
|
+
this.classifier = new ErrorClassifier();
|
|
49
|
+
this.logSchema = new UnifiedLogSchema();
|
|
50
|
+
this.retryHandler = new RetryHandler({
|
|
51
|
+
maxRetries: handling.maxRetries || 3,
|
|
52
|
+
retryDelay: handling.retryDelay || 1000,
|
|
53
|
+
retryMultiplier: handling.retryMultiplier || 2,
|
|
54
|
+
maxRetryDelay: handling.maxRetryDelay || 30000
|
|
55
|
+
});
|
|
56
|
+
this.circuitBreaker = new CircuitBreakerManager({
|
|
57
|
+
circuitBreakerEnabled: handling.circuitBreakerEnabled !== false,
|
|
58
|
+
timeout: handling.circuitTimeout || 10000,
|
|
59
|
+
errorThreshold: handling.errorThreshold || 50,
|
|
60
|
+
resetTimeout: handling.resetTimeout || 30000
|
|
61
|
+
});
|
|
62
|
+
this.dlqRouter = new DLQRouter({
|
|
63
|
+
dlqEnabled: handling.dlqEnabled !== false,
|
|
64
|
+
mqClient: handling.mqClient || null
|
|
65
|
+
});
|
|
66
|
+
this.compensation = new CompensationHandler({
|
|
67
|
+
compensationEnabled: handling.compensationEnabled !== false
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Log error using monitoring connector
|
|
73
|
+
* Always works: console (via monitoring) + RabbitMQ (if available)
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} errorData - Error data
|
|
76
|
+
* @param {string} errorData.moduleName - Module name
|
|
77
|
+
* @param {string} errorData.operation - Operation name
|
|
78
|
+
* @param {Error} errorData.error - Error object
|
|
79
|
+
* @param {Object} [errorData.context] - Additional context
|
|
80
|
+
* @returns {Promise<Object>} Unified error log entry
|
|
81
|
+
*/
|
|
82
|
+
async logError(errorData) {
|
|
83
|
+
const { moduleName, operation, error, context = {} } = errorData;
|
|
84
|
+
|
|
85
|
+
// Classify error
|
|
86
|
+
const errorType = this.classifier.classify(error);
|
|
87
|
+
|
|
88
|
+
// Create unified log entry
|
|
89
|
+
const errorLog = this.logSchema.create({
|
|
90
|
+
moduleName,
|
|
91
|
+
operation,
|
|
92
|
+
error,
|
|
93
|
+
context,
|
|
94
|
+
serviceName: this.serviceName,
|
|
95
|
+
serviceVersion: this.serviceVersion,
|
|
96
|
+
environment: this.environment,
|
|
97
|
+
errorType,
|
|
98
|
+
handling: {} // Will be filled by handleError
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Log via monitoring connector
|
|
102
|
+
// Monitoring connector has fallback to console if not initialized
|
|
103
|
+
const logMessage = `[${moduleName}] ${operation} failed: ${error.message}`;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Use monitoring logger (monitoring-core returns instance with logger property)
|
|
107
|
+
// logger.error() always logs to console, optionally to RabbitMQ
|
|
108
|
+
if (this.monitoring.logger && this.monitoring.logger.error) {
|
|
109
|
+
this.monitoring.logger.error(logMessage, {
|
|
110
|
+
...errorLog,
|
|
111
|
+
severity: errorLog.metadata.severity
|
|
112
|
+
});
|
|
113
|
+
} else if (this.monitoring.error) {
|
|
114
|
+
// Fallback: direct error method (conn-base-monitoring wrapper)
|
|
115
|
+
this.monitoring.error(logMessage, {
|
|
116
|
+
...errorLog,
|
|
117
|
+
severity: errorLog.metadata.severity
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
// Last resort: console
|
|
121
|
+
console.error(JSON.stringify(errorLog, null, 2));
|
|
122
|
+
}
|
|
123
|
+
} catch (logError) {
|
|
124
|
+
// If monitoring fails, fallback to console
|
|
125
|
+
console.error('[ErrorHandlerCore] Monitoring log failed, using console:', logError.message);
|
|
126
|
+
console.error(JSON.stringify(errorLog, null, 2));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return errorLog;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handle error: classify + log + decide action
|
|
134
|
+
*
|
|
135
|
+
* @param {Object} errorData - Error data
|
|
136
|
+
* @param {string} errorData.moduleName - Module name
|
|
137
|
+
* @param {string} errorData.operation - Operation name
|
|
138
|
+
* @param {Error} errorData.error - Error object
|
|
139
|
+
* @param {Object} [errorData.context] - Additional context
|
|
140
|
+
* @param {number} [errorData.context.retryCount] - Current retry count
|
|
141
|
+
* @param {number} [errorData.context.maxRetries] - Max retries
|
|
142
|
+
* @returns {Promise<Object>} Handling result
|
|
143
|
+
*/
|
|
144
|
+
async handleError(errorData) {
|
|
145
|
+
const { moduleName, operation, error, context = {} } = errorData;
|
|
146
|
+
|
|
147
|
+
// 1. Classify error
|
|
148
|
+
const errorType = this.classifier.classify(error);
|
|
149
|
+
|
|
150
|
+
// 2. Log error (always)
|
|
151
|
+
const errorLog = await this.logError({
|
|
152
|
+
moduleName,
|
|
153
|
+
operation,
|
|
154
|
+
error,
|
|
155
|
+
context
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 3. Determine action based on error type
|
|
159
|
+
const action = this._determineAction(errorType, context);
|
|
160
|
+
|
|
161
|
+
// 4. Prepare handling result
|
|
162
|
+
const result = {
|
|
163
|
+
action: action.type,
|
|
164
|
+
errorLog,
|
|
165
|
+
errorType,
|
|
166
|
+
shouldRetry: action.shouldRetry || false,
|
|
167
|
+
retryDelay: action.retryDelay || null,
|
|
168
|
+
retryCount: context.retryCount || 0,
|
|
169
|
+
maxRetries: context.maxRetries || this.retryHandler.maxRetries
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// 5. Execute action
|
|
173
|
+
if (action.type === 'dlq' && this.dlqRouter.isAvailable()) {
|
|
174
|
+
await this.dlqRouter.routeToDLQ(
|
|
175
|
+
context.message || {},
|
|
176
|
+
error,
|
|
177
|
+
errorType,
|
|
178
|
+
context
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (action.type === 'compensate' && this.compensation.enabled) {
|
|
183
|
+
const compensationResult = await this.compensation.execute(
|
|
184
|
+
operation,
|
|
185
|
+
context
|
|
186
|
+
);
|
|
187
|
+
result.compensated = compensationResult !== null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Determine action based on error type
|
|
195
|
+
* @private
|
|
196
|
+
*/
|
|
197
|
+
_determineAction(errorType, context) {
|
|
198
|
+
const retryCount = context.retryCount || 0;
|
|
199
|
+
const maxRetries = context.maxRetries || this.retryHandler.maxRetries;
|
|
200
|
+
|
|
201
|
+
switch (errorType) {
|
|
202
|
+
case ErrorTypes.TRANSIENT:
|
|
203
|
+
case ErrorTypes.TIMEOUT:
|
|
204
|
+
case ErrorTypes.RATE_LIMIT:
|
|
205
|
+
// Retry if under max retries
|
|
206
|
+
if (retryCount < maxRetries) {
|
|
207
|
+
const delay = this.retryHandler.calculateBackoff(retryCount + 1);
|
|
208
|
+
return {
|
|
209
|
+
type: 'retry',
|
|
210
|
+
shouldRetry: true,
|
|
211
|
+
retryDelay: delay
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Max retries exceeded, route to DLQ
|
|
215
|
+
return { type: 'dlq' };
|
|
216
|
+
|
|
217
|
+
case ErrorTypes.BUSINESS:
|
|
218
|
+
case ErrorTypes.VALIDATION:
|
|
219
|
+
// Permanent errors, route to DLQ
|
|
220
|
+
return { type: 'dlq' };
|
|
221
|
+
|
|
222
|
+
case ErrorTypes.FATAL:
|
|
223
|
+
// Fatal errors, throw immediately
|
|
224
|
+
return { type: 'throw' };
|
|
225
|
+
|
|
226
|
+
default:
|
|
227
|
+
// Unknown errors, log and throw
|
|
228
|
+
return { type: 'throw' };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Execute function with automatic retry
|
|
234
|
+
*
|
|
235
|
+
* @param {Object} options - Execution options
|
|
236
|
+
* @param {string} options.moduleName - Module name
|
|
237
|
+
* @param {string} options.operation - Operation name
|
|
238
|
+
* @param {Function} options.fn - Async function to execute
|
|
239
|
+
* @param {Object} [options.context] - Additional context
|
|
240
|
+
* @returns {Promise<*>} Function result
|
|
241
|
+
*/
|
|
242
|
+
async executeWithRetry(options) {
|
|
243
|
+
const { moduleName, operation, fn, context = {} } = options;
|
|
244
|
+
|
|
245
|
+
return await this.retryHandler.executeWithRetry(fn, {
|
|
246
|
+
maxRetries: context.maxRetries || this.retryHandler.maxRetries,
|
|
247
|
+
onRetry: async (error, attempt, delay) => {
|
|
248
|
+
// Log each retry attempt
|
|
249
|
+
await this.logError({
|
|
250
|
+
moduleName,
|
|
251
|
+
operation,
|
|
252
|
+
error,
|
|
253
|
+
context: {
|
|
254
|
+
...context,
|
|
255
|
+
retryCount: attempt,
|
|
256
|
+
maxRetries: context.maxRetries || this.retryHandler.maxRetries
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Execute function with circuit breaker protection
|
|
265
|
+
*
|
|
266
|
+
* @param {Object} options - Execution options
|
|
267
|
+
* @param {string} options.moduleName - Module name
|
|
268
|
+
* @param {string} options.operation - Operation name
|
|
269
|
+
* @param {string} options.circuitName - Circuit breaker name
|
|
270
|
+
* @param {Function} options.fn - Async function to execute
|
|
271
|
+
* @param {Object} [options.context] - Additional context
|
|
272
|
+
* @returns {Promise<*>} Function result
|
|
273
|
+
*/
|
|
274
|
+
async executeWithCircuitBreaker(options) {
|
|
275
|
+
const { moduleName, operation, circuitName, fn, context = {} } = options;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
return await this.circuitBreaker.executeWithCircuitBreaker(
|
|
279
|
+
circuitName,
|
|
280
|
+
fn,
|
|
281
|
+
{}
|
|
282
|
+
);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
// Log circuit breaker errors
|
|
285
|
+
await this.logError({
|
|
286
|
+
moduleName,
|
|
287
|
+
operation,
|
|
288
|
+
error,
|
|
289
|
+
context: {
|
|
290
|
+
...context,
|
|
291
|
+
circuitName,
|
|
292
|
+
circuitState: this.circuitBreaker.getState(circuitName)
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Register compensation handler
|
|
301
|
+
*
|
|
302
|
+
* @param {string} operation - Operation name
|
|
303
|
+
* @param {Function} handler - Compensation handler
|
|
304
|
+
*/
|
|
305
|
+
registerCompensation(operation, handler) {
|
|
306
|
+
this.compensation.register(operation, handler);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get circuit breaker state
|
|
311
|
+
*
|
|
312
|
+
* @param {string} name - Circuit breaker name
|
|
313
|
+
* @returns {string} State: 'open' | 'half-open' | 'closed'
|
|
314
|
+
*/
|
|
315
|
+
getCircuitBreakerState(name) {
|
|
316
|
+
return this.circuitBreaker.getState(name);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get all circuit breaker states
|
|
321
|
+
* @returns {Object} Map of circuit breaker states
|
|
322
|
+
*/
|
|
323
|
+
getAllCircuitBreakerStates() {
|
|
324
|
+
return this.circuitBreaker.getAllStates();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = ErrorHandlerCore;
|
|
329
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry Handler
|
|
3
|
+
* Implements exponential backoff retry logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class RetryHandler {
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.maxRetries = config.maxRetries || 3;
|
|
9
|
+
this.retryDelay = config.retryDelay || 1000;
|
|
10
|
+
this.retryMultiplier = config.retryMultiplier || 2;
|
|
11
|
+
this.maxRetryDelay = config.maxRetryDelay || 30000;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculate exponential backoff delay
|
|
16
|
+
*
|
|
17
|
+
* @param {number} attempt - Current attempt number (1-based)
|
|
18
|
+
* @returns {number} Delay in milliseconds
|
|
19
|
+
*/
|
|
20
|
+
calculateBackoff(attempt) {
|
|
21
|
+
const delay = this.retryDelay * Math.pow(this.retryMultiplier, attempt - 1);
|
|
22
|
+
return Math.min(delay, this.maxRetryDelay);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sleep for specified milliseconds
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
async sleep(ms) {
|
|
30
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute function with retry
|
|
35
|
+
*
|
|
36
|
+
* @param {Function} fn - Async function to execute
|
|
37
|
+
* @param {Object} options - Retry options
|
|
38
|
+
* @param {number} [options.maxRetries] - Override max retries
|
|
39
|
+
* @param {Function} [options.onRetry] - Callback on retry (error, attempt, delay)
|
|
40
|
+
* @returns {Promise<*>} Function result
|
|
41
|
+
* @throws {Error} Last error if all retries fail
|
|
42
|
+
*/
|
|
43
|
+
async executeWithRetry(fn, options = {}) {
|
|
44
|
+
const maxAttempts = options.maxRetries || this.maxRetries;
|
|
45
|
+
let lastError;
|
|
46
|
+
|
|
47
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
lastError = error;
|
|
52
|
+
|
|
53
|
+
// If last attempt, throw
|
|
54
|
+
if (attempt === maxAttempts) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Calculate delay and wait
|
|
59
|
+
const delay = this.calculateBackoff(attempt);
|
|
60
|
+
|
|
61
|
+
if (options.onRetry) {
|
|
62
|
+
options.onRetry(error, attempt, delay);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await this.sleep(delay);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw lastError;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = RetryHandler;
|
|
74
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Error Log Schema
|
|
3
|
+
* Creates structured error log entries following unified schema
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
class UnifiedLogSchema {
|
|
9
|
+
/**
|
|
10
|
+
* Create unified error log entry
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} params - Error data
|
|
13
|
+
* @param {string} params.moduleName - Module/component name
|
|
14
|
+
* @param {string} params.operation - Operation that failed
|
|
15
|
+
* @param {Error} params.error - Error object
|
|
16
|
+
* @param {Object} params.context - Additional context
|
|
17
|
+
* @param {string} params.serviceName - Service name
|
|
18
|
+
* @param {string} params.serviceVersion - Service version
|
|
19
|
+
* @param {string} params.environment - Environment (production, staging, etc.)
|
|
20
|
+
* @param {string} params.errorType - Classified error type
|
|
21
|
+
* @param {Object} params.handling - Handling decision
|
|
22
|
+
* @returns {Object} Unified error log entry
|
|
23
|
+
*/
|
|
24
|
+
create(params) {
|
|
25
|
+
const {
|
|
26
|
+
moduleName,
|
|
27
|
+
operation,
|
|
28
|
+
error,
|
|
29
|
+
context = {},
|
|
30
|
+
serviceName,
|
|
31
|
+
serviceVersion,
|
|
32
|
+
environment,
|
|
33
|
+
errorType,
|
|
34
|
+
handling = {}
|
|
35
|
+
} = params;
|
|
36
|
+
|
|
37
|
+
// Extract error details
|
|
38
|
+
const errorDetails = {
|
|
39
|
+
message: error?.message || 'Unknown error',
|
|
40
|
+
code: error?.code || null,
|
|
41
|
+
type: errorType || 'UNKNOWN',
|
|
42
|
+
stack: error?.stack || null
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Build unified log entry
|
|
46
|
+
return {
|
|
47
|
+
// Core identification
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
serviceName: serviceName || 'unknown-service',
|
|
50
|
+
moduleName: moduleName || 'unknown-module',
|
|
51
|
+
operation: operation || 'unknown-operation',
|
|
52
|
+
|
|
53
|
+
// Error details
|
|
54
|
+
error: errorDetails,
|
|
55
|
+
|
|
56
|
+
// Context
|
|
57
|
+
context: {
|
|
58
|
+
workflowId: context.workflowId || null,
|
|
59
|
+
userId: context.userId || null,
|
|
60
|
+
requestId: context.requestId || null,
|
|
61
|
+
...context
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// Handling decision
|
|
65
|
+
handling: {
|
|
66
|
+
action: handling.action || 'throw',
|
|
67
|
+
retryCount: handling.retryCount || null,
|
|
68
|
+
maxRetries: handling.maxRetries || null,
|
|
69
|
+
retryDelay: handling.retryDelay || null,
|
|
70
|
+
circuitBreakerState: handling.circuitBreakerState || null,
|
|
71
|
+
circuitName: handling.circuitName || null
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Metadata
|
|
75
|
+
metadata: {
|
|
76
|
+
severity: this._determineSeverity(errorType),
|
|
77
|
+
environment: environment || process.env.NODE_ENV || 'development',
|
|
78
|
+
version: serviceVersion || '0.0.0',
|
|
79
|
+
hostname: os.hostname(),
|
|
80
|
+
pid: process.pid
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Determine severity from error type
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
_determineSeverity(errorType) {
|
|
90
|
+
const severityMap = {
|
|
91
|
+
FATAL: 'fatal',
|
|
92
|
+
BUSINESS: 'error',
|
|
93
|
+
VALIDATION: 'warn',
|
|
94
|
+
TRANSIENT: 'error',
|
|
95
|
+
TIMEOUT: 'error',
|
|
96
|
+
RATE_LIMIT: 'warn',
|
|
97
|
+
UNKNOWN: 'error'
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return severityMap[errorType] || 'error';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = UnifiedLogSchema;
|
|
105
|
+
|
package/src/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @onlineapps/error-handler-core
|
|
3
|
+
* Core error handling functionality for OA Drive microservices
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ErrorHandlerCore = require('./ErrorHandlerCore');
|
|
7
|
+
const { ErrorClassifier, ErrorTypes, ErrorCodes } = require('./ErrorClassifier');
|
|
8
|
+
const UnifiedLogSchema = require('./UnifiedLogSchema');
|
|
9
|
+
const RetryHandler = require('./RetryHandler');
|
|
10
|
+
const CircuitBreakerManager = require('./CircuitBreaker');
|
|
11
|
+
const DLQRouter = require('./DLQRouter');
|
|
12
|
+
const CompensationHandler = require('./CompensationHandler');
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
// Main class
|
|
16
|
+
ErrorHandlerCore,
|
|
17
|
+
|
|
18
|
+
// Components (for advanced usage)
|
|
19
|
+
ErrorClassifier,
|
|
20
|
+
UnifiedLogSchema,
|
|
21
|
+
RetryHandler,
|
|
22
|
+
CircuitBreakerManager,
|
|
23
|
+
DLQRouter,
|
|
24
|
+
CompensationHandler,
|
|
25
|
+
|
|
26
|
+
// Constants
|
|
27
|
+
ErrorTypes,
|
|
28
|
+
ErrorCodes
|
|
29
|
+
};
|
|
30
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DLQRouter Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DLQRouter = require('../../src/DLQRouter');
|
|
6
|
+
|
|
7
|
+
describe('DLQRouter @unit', () => {
|
|
8
|
+
let dlqRouter;
|
|
9
|
+
let mockMQClient;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockMQClient = {
|
|
13
|
+
publish: jest.fn().mockResolvedValue()
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
dlqRouter = new DLQRouter({
|
|
17
|
+
dlqEnabled: true,
|
|
18
|
+
mqClient: mockMQClient,
|
|
19
|
+
dlqNamePattern: '{queue}.dlq'
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('routeToDLQ', () => {
|
|
24
|
+
test('should route message to DLQ', async () => {
|
|
25
|
+
const message = { id: 'msg-123', data: 'test' };
|
|
26
|
+
const error = new Error('Test error');
|
|
27
|
+
const context = { queue: 'test.queue' };
|
|
28
|
+
|
|
29
|
+
await dlqRouter.routeToDLQ(message, error, 'BUSINESS', context);
|
|
30
|
+
|
|
31
|
+
expect(mockMQClient.publish).toHaveBeenCalledWith(
|
|
32
|
+
'test.queue.dlq',
|
|
33
|
+
expect.objectContaining({
|
|
34
|
+
id: 'msg-123',
|
|
35
|
+
error: expect.objectContaining({
|
|
36
|
+
message: 'Test error',
|
|
37
|
+
type: 'BUSINESS'
|
|
38
|
+
}),
|
|
39
|
+
originalQueue: 'test.queue'
|
|
40
|
+
}),
|
|
41
|
+
{ persistent: true }
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should not route if DLQ disabled', async () => {
|
|
46
|
+
const disabledRouter = new DLQRouter({
|
|
47
|
+
dlqEnabled: false,
|
|
48
|
+
mqClient: mockMQClient
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await disabledRouter.routeToDLQ({}, new Error('Test'), 'BUSINESS', {});
|
|
52
|
+
|
|
53
|
+
expect(mockMQClient.publish).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should not route if no MQ client', async () => {
|
|
57
|
+
const noClientRouter = new DLQRouter({
|
|
58
|
+
dlqEnabled: true,
|
|
59
|
+
mqClient: null
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await noClientRouter.routeToDLQ({}, new Error('Test'), 'BUSINESS', {});
|
|
63
|
+
|
|
64
|
+
expect(mockMQClient.publish).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should handle DLQ routing failure gracefully', async () => {
|
|
68
|
+
mockMQClient.publish.mockRejectedValue(new Error('DLQ failed'));
|
|
69
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
70
|
+
|
|
71
|
+
await dlqRouter.routeToDLQ({}, new Error('Test'), 'BUSINESS', { queue: 'test' });
|
|
72
|
+
|
|
73
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
74
|
+
consoleSpy.mockRestore();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('isAvailable', () => {
|
|
79
|
+
test('should return true if enabled and client available', () => {
|
|
80
|
+
expect(dlqRouter.isAvailable()).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should return false if disabled', () => {
|
|
84
|
+
const disabledRouter = new DLQRouter({ dlqEnabled: false });
|
|
85
|
+
expect(disabledRouter.isAvailable()).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should return false if no client', () => {
|
|
89
|
+
const noClientRouter = new DLQRouter({ mqClient: null });
|
|
90
|
+
expect(noClientRouter.isAvailable()).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|