@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.
@@ -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
+