@onlineapps/infrastructure-tools 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.
- package/README.md +3 -0
- package/package.json +2 -3
- package/src/health/healthPublisher.js +148 -16
- package/src/index.js +13 -4
- package/src/monitoring/queueMismatchReporter.js +94 -0
- package/src/orchestration/initInfrastructureQueues.js +20 -0
- package/src/utils/logger.js +41 -0
- package/tests/queueMismatchReporter.test.js +49 -0
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ const { initInfrastructureQueues } = require('@onlineapps/infrastructure-tools')
|
|
|
67
67
|
await initInfrastructureQueues(channel, {
|
|
68
68
|
queues: ['workflow.init'], // Only create specific queues
|
|
69
69
|
connection: connection,
|
|
70
|
+
serviceName: 'api-gateway',
|
|
70
71
|
logger: logger
|
|
71
72
|
});
|
|
72
73
|
```
|
|
@@ -94,6 +95,8 @@ Initializes infrastructure queues with correct parameters from `queueConfig`.
|
|
|
94
95
|
- `connection` (Object): RabbitMQ connection (for channel recreation)
|
|
95
96
|
- `logger` (Object): Logger instance (default: console)
|
|
96
97
|
- `queueConfig` (Object): Queue config instance (default: from mq-client-core)
|
|
98
|
+
- `serviceName` (string): Used in queue mismatch alerts (default: `unknown-service`)
|
|
99
|
+
- `alertOnMismatch` (boolean): Disable automatic 406 alerts (default: `true`)
|
|
97
100
|
|
|
98
101
|
**Returns:** `Promise<void>`
|
|
99
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/infrastructure-tools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Infrastructure orchestration utilities for OA Drive infrastructure services (health tracking, queue initialization, service discovery)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@onlineapps/mq-client-core": "^1.0.25",
|
|
22
|
-
"@onlineapps/service-common": "^1.0.
|
|
22
|
+
"@onlineapps/service-common": "^1.0.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"jest": "^29.7.0"
|
|
@@ -28,4 +28,3 @@
|
|
|
28
28
|
"access": "public"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* - Custom publish function
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
const { createLogger } = require('../utils/logger');
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Create infrastructure health publisher
|
|
17
19
|
* @param {Object} options - Configuration options
|
|
@@ -32,9 +34,12 @@ function createHealthPublisher(options) {
|
|
|
32
34
|
publishFunction,
|
|
33
35
|
getHealthData,
|
|
34
36
|
config = {},
|
|
35
|
-
logger
|
|
37
|
+
logger: customLogger
|
|
36
38
|
} = options;
|
|
37
39
|
|
|
40
|
+
// Use shared logger utility for consistent logging
|
|
41
|
+
const logger = createLogger(customLogger);
|
|
42
|
+
|
|
38
43
|
if (!serviceName) {
|
|
39
44
|
throw new Error('serviceName is required');
|
|
40
45
|
}
|
|
@@ -71,11 +76,19 @@ function createHealthPublisher(options) {
|
|
|
71
76
|
components: getHealthData()
|
|
72
77
|
};
|
|
73
78
|
|
|
79
|
+
logger.info(`[InfrastructureHealth:${serviceName}] Publishing health check...`, {
|
|
80
|
+
queue: queueName,
|
|
81
|
+
serviceName,
|
|
82
|
+
status: healthData.status,
|
|
83
|
+
timestamp: healthData.timestamp
|
|
84
|
+
});
|
|
85
|
+
|
|
74
86
|
await publishFunction(queueName, healthData);
|
|
75
87
|
|
|
76
|
-
logger.
|
|
88
|
+
logger.info(`[InfrastructureHealth:${serviceName}] ✓ Published health check`, {
|
|
77
89
|
queue: queueName,
|
|
78
|
-
status: healthData.status
|
|
90
|
+
status: healthData.status,
|
|
91
|
+
timestamp: healthData.timestamp
|
|
79
92
|
});
|
|
80
93
|
} catch (error) {
|
|
81
94
|
logger.error(`[InfrastructureHealth:${serviceName}] Failed to publish health check`, {
|
|
@@ -151,12 +164,44 @@ function createHealthPublisher(options) {
|
|
|
151
164
|
* @param {Object} logger - Logger instance
|
|
152
165
|
* @returns {Object} Health publisher instance
|
|
153
166
|
*/
|
|
154
|
-
function createBaseClientAdapter(baseClient, serviceName, getHealthData, config,
|
|
167
|
+
function createBaseClientAdapter(baseClient, serviceName, getHealthData, config, customLogger) {
|
|
168
|
+
// Use shared logger utility for consistent logging
|
|
169
|
+
const logger = createLogger(customLogger);
|
|
155
170
|
const publishFunction = async (queueName, data) => {
|
|
171
|
+
logger.info(`[InfrastructureHealth:${serviceName}] [PUBLISH] Starting publish to queue '${queueName}'...`, {
|
|
172
|
+
queueName,
|
|
173
|
+
serviceName,
|
|
174
|
+
status: data.status,
|
|
175
|
+
timestamp: data.timestamp
|
|
176
|
+
});
|
|
177
|
+
|
|
156
178
|
if (!baseClient || typeof baseClient.publish !== 'function') {
|
|
157
|
-
|
|
179
|
+
const error = new Error('BaseClient instance must have publish() method');
|
|
180
|
+
logger.error(`[InfrastructureHealth:${serviceName}] [PUBLISH] ✗ BaseClient invalid`, {
|
|
181
|
+
queueName,
|
|
182
|
+
error: error.message,
|
|
183
|
+
hasBaseClient: !!baseClient,
|
|
184
|
+
hasPublish: baseClient && typeof baseClient.publish === 'function'
|
|
185
|
+
});
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await baseClient.publish(queueName, data);
|
|
191
|
+
logger.info(`[InfrastructureHealth:${serviceName}] [PUBLISH] ✓ Message sent to queue '${queueName}'`, {
|
|
192
|
+
queueName,
|
|
193
|
+
serviceName,
|
|
194
|
+
status: data.status
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.error(`[InfrastructureHealth:${serviceName}] [PUBLISH] ✗ Failed to publish health check`, {
|
|
198
|
+
queueName,
|
|
199
|
+
error: error.message,
|
|
200
|
+
stack: error.stack,
|
|
201
|
+
code: error.code
|
|
202
|
+
});
|
|
203
|
+
throw error;
|
|
158
204
|
}
|
|
159
|
-
await baseClient.publish(queueName, data);
|
|
160
205
|
};
|
|
161
206
|
|
|
162
207
|
return createHealthPublisher({
|
|
@@ -178,18 +223,105 @@ function createBaseClientAdapter(baseClient, serviceName, getHealthData, config,
|
|
|
178
223
|
* @param {Object} logger - Logger instance
|
|
179
224
|
* @returns {Object} Health publisher instance
|
|
180
225
|
*/
|
|
181
|
-
function createAmqplibAdapter(connection, channel, serviceName, getHealthData, config,
|
|
226
|
+
function createAmqplibAdapter(connection, channel, serviceName, getHealthData, config, customLogger) {
|
|
227
|
+
// Use shared logger utility for consistent logging
|
|
228
|
+
const logger = createLogger(customLogger);
|
|
229
|
+
|
|
230
|
+
// Create a dedicated channel for health checks to avoid conflicts with consumer channels
|
|
231
|
+
let healthCheckChannel = null;
|
|
232
|
+
|
|
182
233
|
const publishFunction = async (queueName, data) => {
|
|
183
|
-
|
|
184
|
-
throw new Error('AMQP channel is not available or closed');
|
|
185
|
-
}
|
|
186
|
-
// Ensure queue exists (it should be created by Registry, but assert just in case)
|
|
187
|
-
await channel.assertQueue(queueName, { durable: true });
|
|
188
|
-
await channel.sendToQueue(
|
|
234
|
+
logger.info(`[InfrastructureHealth:${serviceName}] [PUBLISH] Starting publish to queue '${queueName}'...`, {
|
|
189
235
|
queueName,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
236
|
+
serviceName,
|
|
237
|
+
status: data.status,
|
|
238
|
+
timestamp: data.timestamp
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Check connection first
|
|
242
|
+
if (!connection || connection.closed) {
|
|
243
|
+
const error = new Error('AMQP connection is not available or closed');
|
|
244
|
+
logger.error(`[InfrastructureHealth:${serviceName}] [PUBLISH] ✗ Connection not available`, {
|
|
245
|
+
queueName,
|
|
246
|
+
error: error.message
|
|
247
|
+
});
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Always use dedicated channel for health checks to avoid conflicts with consumer channels
|
|
252
|
+
// Check if we need to create or recreate the channel
|
|
253
|
+
if (!healthCheckChannel || healthCheckChannel.closed) {
|
|
254
|
+
if (healthCheckChannel && healthCheckChannel.closed) {
|
|
255
|
+
logger.warn(`[InfrastructureHealth:${serviceName}] [PUBLISH] Health check channel closed, recreating...`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
healthCheckChannel = await connection.createChannel();
|
|
260
|
+
logger.info(`[InfrastructureHealth:${serviceName}] [PUBLISH] Created dedicated health check channel`);
|
|
261
|
+
} catch (createErr) {
|
|
262
|
+
logger.error(`[InfrastructureHealth:${serviceName}] [PUBLISH] ✗ Failed to create health check channel`, {
|
|
263
|
+
error: createErr.message
|
|
264
|
+
});
|
|
265
|
+
throw createErr;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const activeChannel = healthCheckChannel;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
// Queue should already exist (created by Registry), so we don't assert it
|
|
273
|
+
// This avoids 406 PRECONDITION-FAILED errors if queue exists with different parameters
|
|
274
|
+
// Just send the message directly
|
|
275
|
+
logger.debug(`[InfrastructureHealth:${serviceName}] [PUBLISH] Sending message to queue '${queueName}' (queue should already exist)...`);
|
|
276
|
+
|
|
277
|
+
await activeChannel.sendToQueue(
|
|
278
|
+
queueName,
|
|
279
|
+
Buffer.from(JSON.stringify(data)),
|
|
280
|
+
{ persistent: true }
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
logger.info(`[InfrastructureHealth:${serviceName}] [PUBLISH] ✓ Message sent to queue '${queueName}'`, {
|
|
284
|
+
queueName,
|
|
285
|
+
serviceName,
|
|
286
|
+
status: data.status
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
// If channel was closed during operation, mark it for recreation
|
|
290
|
+
if (error.message && (error.message.includes('Channel closed') || error.message.includes('channel is closed'))) {
|
|
291
|
+
logger.warn(`[InfrastructureHealth:${serviceName}] [PUBLISH] Channel closed during publish, will recreate on next attempt`);
|
|
292
|
+
if (healthCheckChannel) {
|
|
293
|
+
try {
|
|
294
|
+
await healthCheckChannel.close().catch(() => {});
|
|
295
|
+
} catch (closeErr) {
|
|
296
|
+
// Ignore close errors
|
|
297
|
+
}
|
|
298
|
+
healthCheckChannel = null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
logger.error(`[InfrastructureHealth:${serviceName}] [PUBLISH] ✗ Failed to publish health check`, {
|
|
303
|
+
queueName,
|
|
304
|
+
error: error.message,
|
|
305
|
+
stack: error.stack,
|
|
306
|
+
code: error.code
|
|
307
|
+
});
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Cleanup function to close health check channel
|
|
313
|
+
const cleanup = async () => {
|
|
314
|
+
if (healthCheckChannel && !healthCheckChannel.closed) {
|
|
315
|
+
try {
|
|
316
|
+
await healthCheckChannel.close();
|
|
317
|
+
logger.info(`[InfrastructureHealth:${serviceName}] Closed dedicated health check channel`);
|
|
318
|
+
} catch (closeErr) {
|
|
319
|
+
logger.warn(`[InfrastructureHealth:${serviceName}] Failed to close health check channel`, {
|
|
320
|
+
error: closeErr.message
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
healthCheckChannel = null;
|
|
324
|
+
}
|
|
193
325
|
};
|
|
194
326
|
|
|
195
327
|
return createHealthPublisher({
|
package/src/index.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* Business services should NOT use this library.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
// Re-export
|
|
14
|
-
const { waitForInfrastructureReady } = require('@onlineapps/service-common');
|
|
13
|
+
// Re-export infrastructure readiness utilities from service-common (no duplication)
|
|
14
|
+
const { waitForInfrastructureReady, waitForHealthCheckQueueReady } = require('@onlineapps/service-common');
|
|
15
15
|
|
|
16
16
|
const { initInfrastructureQueues } = require('./orchestration/initInfrastructureQueues');
|
|
17
17
|
const {
|
|
@@ -19,16 +19,25 @@ const {
|
|
|
19
19
|
createBaseClientAdapter,
|
|
20
20
|
createAmqplibAdapter
|
|
21
21
|
} = require('./health/healthPublisher');
|
|
22
|
+
const { createLogger } = require('./utils/logger');
|
|
23
|
+
const { sendQueueMismatchAlert } = require('./monitoring/queueMismatchReporter');
|
|
22
24
|
|
|
23
25
|
module.exports = {
|
|
24
26
|
// Orchestration utilities
|
|
25
|
-
// waitForInfrastructureReady
|
|
27
|
+
// waitForInfrastructureReady and waitForHealthCheckQueueReady are re-exported from @onlineapps/service-common
|
|
26
28
|
waitForInfrastructureReady,
|
|
29
|
+
waitForHealthCheckQueueReady,
|
|
27
30
|
initInfrastructureQueues,
|
|
28
31
|
|
|
29
32
|
// Health tracking utilities
|
|
30
33
|
createHealthPublisher,
|
|
31
34
|
createBaseClientAdapter,
|
|
32
|
-
createAmqplibAdapter
|
|
35
|
+
createAmqplibAdapter,
|
|
36
|
+
|
|
37
|
+
// Logger utility (consistent with conn-infra-mq pattern)
|
|
38
|
+
createLogger,
|
|
39
|
+
|
|
40
|
+
// Monitoring utilities
|
|
41
|
+
sendQueueMismatchAlert
|
|
33
42
|
};
|
|
34
43
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { sendMonitoringFailFallbackEmail } = require('@onlineapps/service-common');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
+
const lastAlertMap = new Map();
|
|
7
|
+
|
|
8
|
+
function shouldThrottle(key, cooldownMs) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
const last = lastAlertMap.get(key) || 0;
|
|
11
|
+
if (now - last < cooldownMs) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
lastAlertMap.set(key, now);
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Send structured alert when queue precondition fails (406 mismatch).
|
|
20
|
+
* Falls back to email via service-common if Slack/webhook unavailable.
|
|
21
|
+
* @param {Object} options
|
|
22
|
+
* @param {string} options.serviceName
|
|
23
|
+
* @param {string} options.queueName
|
|
24
|
+
* @param {Object} [options.expectedArguments]
|
|
25
|
+
* @param {string} [options.action]
|
|
26
|
+
* @param {Error} [options.error]
|
|
27
|
+
* @param {Object} [options.logger]
|
|
28
|
+
* @param {number} [options.cooldownMs]
|
|
29
|
+
*/
|
|
30
|
+
async function sendQueueMismatchAlert(options = {}) {
|
|
31
|
+
const {
|
|
32
|
+
serviceName = 'unknown-service',
|
|
33
|
+
queueName = 'unknown-queue',
|
|
34
|
+
expectedArguments = {},
|
|
35
|
+
action = 'assertQueue',
|
|
36
|
+
error,
|
|
37
|
+
logger = console,
|
|
38
|
+
cooldownMs = DEFAULT_COOLDOWN_MS,
|
|
39
|
+
extra = {}
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
const key = `${serviceName}:${queueName}`;
|
|
43
|
+
if (shouldThrottle(key, cooldownMs)) {
|
|
44
|
+
logger.warn('[QueueMismatchAlert] Throttling duplicate alert', { serviceName, queueName });
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const event = {
|
|
49
|
+
serviceName,
|
|
50
|
+
queueName,
|
|
51
|
+
action,
|
|
52
|
+
expectedArguments,
|
|
53
|
+
errorMessage: error?.message,
|
|
54
|
+
errorCode: error?.code,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
...extra
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
logger.error('[QueueMismatchAlert] 406 PRECONDITION_FAILED detected', event);
|
|
60
|
+
|
|
61
|
+
const subject = `[Infra][QueueMismatch] ${serviceName} → ${queueName}`;
|
|
62
|
+
const textLines = [
|
|
63
|
+
`Service: ${serviceName}`,
|
|
64
|
+
`Queue: ${queueName}`,
|
|
65
|
+
`Action: ${action}`,
|
|
66
|
+
`Error: ${error?.message || 'Unknown'}`,
|
|
67
|
+
`Code: ${error?.code || 'N/A'}`,
|
|
68
|
+
`Expected arguments: ${JSON.stringify(expectedArguments, null, 2)}`,
|
|
69
|
+
`Timestamp: ${event.timestamp}`
|
|
70
|
+
];
|
|
71
|
+
const text = textLines.join('\n');
|
|
72
|
+
const html = `<p><strong>Infrastructure Queue Mismatch</strong></p><pre>${text}</pre>`;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const sent = await sendMonitoringFailFallbackEmail(subject, text, html);
|
|
76
|
+
if (!sent) {
|
|
77
|
+
logger.warn('[QueueMismatchAlert] Fallback email skipped (missing config)');
|
|
78
|
+
}
|
|
79
|
+
return sent;
|
|
80
|
+
} catch (emailError) {
|
|
81
|
+
logger.error('[QueueMismatchAlert] Failed to send fallback email', { error: emailError.message });
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
sendQueueMismatchAlert,
|
|
88
|
+
_internal: {
|
|
89
|
+
shouldThrottle,
|
|
90
|
+
lastAlertMap
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { sendQueueMismatchAlert } = require('../monitoring/queueMismatchReporter');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* initInfrastructureQueues.js
|
|
5
7
|
*
|
|
@@ -18,6 +20,8 @@
|
|
|
18
20
|
* @param {Object} [options.logger] - Logger instance (default: console)
|
|
19
21
|
* @param {Object} [options.connection] - RabbitMQ connection (for channel recreation)
|
|
20
22
|
* @param {Object} [options.queueConfig] - Queue config instance (default: from mq-client-core)
|
|
23
|
+
* @param {string} [options.serviceName] - Name of service initializing queues (for alerting context)
|
|
24
|
+
* @param {boolean} [options.alertOnMismatch=true] - Whether to send queue mismatch alerts
|
|
21
25
|
* @returns {Promise<void>}
|
|
22
26
|
*/
|
|
23
27
|
async function initInfrastructureQueues(channel, options = {}) {
|
|
@@ -27,6 +31,8 @@ async function initInfrastructureQueues(channel, options = {}) {
|
|
|
27
31
|
|
|
28
32
|
const logger = options.logger || console;
|
|
29
33
|
const connection = options.connection || channel.connection;
|
|
34
|
+
const serviceName = options.serviceName || process.env.SERVICE_NAME || 'unknown-service';
|
|
35
|
+
const alertOnMismatch = options.alertOnMismatch !== false;
|
|
30
36
|
|
|
31
37
|
// Load queueConfig from mq-client-core
|
|
32
38
|
// NOTE: queueConfig is part of mq-client-core because it's used by both:
|
|
@@ -102,6 +108,20 @@ async function initInfrastructureQueues(channel, options = {}) {
|
|
|
102
108
|
logger.log(`[QueueInit] ✓ Created/verified infrastructure queue: ${queueName}`);
|
|
103
109
|
} catch (assertError) {
|
|
104
110
|
if (assertError.code === 406) {
|
|
111
|
+
if (alertOnMismatch) {
|
|
112
|
+
try {
|
|
113
|
+
await sendQueueMismatchAlert({
|
|
114
|
+
serviceName,
|
|
115
|
+
queueName,
|
|
116
|
+
expectedArguments: config.arguments || {},
|
|
117
|
+
action: 'initInfrastructureQueues.assertQueue',
|
|
118
|
+
error: assertError,
|
|
119
|
+
logger
|
|
120
|
+
});
|
|
121
|
+
} catch (alertError) {
|
|
122
|
+
logger.warn('[QueueInit] Failed to send queue mismatch alert', alertError.message);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
105
125
|
logger.warn(
|
|
106
126
|
`[QueueInit] ⚠ Queue ${queueName} exists with different arguments - deleting and recreating...`
|
|
107
127
|
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* logger.js
|
|
5
|
+
*
|
|
6
|
+
* Provides a simple abstraction over console or a custom logger.
|
|
7
|
+
* If the user passes a custom logger object with methods { info, warn, error, debug },
|
|
8
|
+
* these are used; otherwise console.* is used as fallback.
|
|
9
|
+
*
|
|
10
|
+
* Similar to conn-infra-mq logger utility for consistency across infrastructure libraries.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function createLogger(customLogger) {
|
|
14
|
+
const methods = ['info', 'warn', 'error', 'debug'];
|
|
15
|
+
if (
|
|
16
|
+
customLogger &&
|
|
17
|
+
typeof customLogger === 'object' &&
|
|
18
|
+
methods.every((fn) => typeof customLogger[fn] === 'function')
|
|
19
|
+
) {
|
|
20
|
+
// Wrap custom logger to ensure consistent signature
|
|
21
|
+
return {
|
|
22
|
+
info: (...args) => customLogger.info(...args),
|
|
23
|
+
warn: (...args) => customLogger.warn(...args),
|
|
24
|
+
error: (...args) => customLogger.error(...args),
|
|
25
|
+
debug: (...args) => customLogger.debug(...args),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fallback to console
|
|
30
|
+
return {
|
|
31
|
+
info: (...args) => console.log('[INFO]', ...args),
|
|
32
|
+
warn: (...args) => console.warn('[WARN]', ...args),
|
|
33
|
+
error: (...args) => console.error('[ERROR]', ...args),
|
|
34
|
+
debug: (...args) => console.debug('[DEBUG]', ...args),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
createLogger,
|
|
40
|
+
};
|
|
41
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
jest.mock('@onlineapps/service-common', () => ({
|
|
4
|
+
sendMonitoringFailFallbackEmail: jest.fn().mockResolvedValue(true)
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
sendQueueMismatchAlert,
|
|
9
|
+
_internal
|
|
10
|
+
} = require('../src/monitoring/queueMismatchReporter');
|
|
11
|
+
const { sendMonitoringFailFallbackEmail } = require('@onlineapps/service-common');
|
|
12
|
+
|
|
13
|
+
describe('queueMismatchReporter', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
_internal.lastAlertMap.clear();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('sends fallback email for queue mismatch', async () => {
|
|
20
|
+
await sendQueueMismatchAlert({
|
|
21
|
+
serviceName: 'test-service',
|
|
22
|
+
queueName: 'workflow.init',
|
|
23
|
+
expectedArguments: { 'x-message-ttl': 60000 },
|
|
24
|
+
error: new Error('PRECONDITION_FAILED')
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(sendMonitoringFailFallbackEmail).toHaveBeenCalledTimes(1);
|
|
28
|
+
const [subject, text] = sendMonitoringFailFallbackEmail.mock.calls[0];
|
|
29
|
+
expect(subject).toContain('test-service');
|
|
30
|
+
expect(text).toContain('workflow.init');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('throttles duplicate alerts within cooldown', async () => {
|
|
34
|
+
await sendQueueMismatchAlert({
|
|
35
|
+
serviceName: 'test-service',
|
|
36
|
+
queueName: 'workflow.init',
|
|
37
|
+
cooldownMs: 60000
|
|
38
|
+
});
|
|
39
|
+
await sendQueueMismatchAlert({
|
|
40
|
+
serviceName: 'test-service',
|
|
41
|
+
queueName: 'workflow.init',
|
|
42
|
+
cooldownMs: 60000
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(sendMonitoringFailFallbackEmail).toHaveBeenCalledTimes(1);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
|