@onlineapps/infrastructure-tools 1.0.5 → 1.0.7

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 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.5",
3
+ "version": "1.0.7",
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.0"
22
+ "@onlineapps/service-common": "^1.0.1"
23
23
  },
24
24
  "devDependencies": {
25
25
  "jest": "^29.7.0"
package/src/index.js CHANGED
@@ -11,7 +11,11 @@
11
11
  */
12
12
 
13
13
  // Re-export infrastructure readiness utilities from service-common (no duplication)
14
- const { waitForInfrastructureReady, waitForHealthCheckQueueReady } = require('@onlineapps/service-common');
14
+ const { waitForInfrastructureReady, waitForHealthCheckQueueReady, sendMonitoringFailFallbackEmail } = require('@onlineapps/service-common');
15
+
16
+ // Re-export MQ client core utilities
17
+ const BaseClient = require('@onlineapps/mq-client-core');
18
+ const queueConfig = require('@onlineapps/mq-client-core/src/config/queueConfig');
15
19
 
16
20
  const { initInfrastructureQueues } = require('./orchestration/initInfrastructureQueues');
17
21
  const {
@@ -20,12 +24,20 @@ const {
20
24
  createAmqplibAdapter
21
25
  } = require('./health/healthPublisher');
22
26
  const { createLogger } = require('./utils/logger');
27
+ const { sendQueueMismatchAlert } = require('./monitoring/queueMismatchReporter');
23
28
 
24
29
  module.exports = {
25
- // Orchestration utilities
26
- // waitForInfrastructureReady and waitForHealthCheckQueueReady are re-exported from @onlineapps/service-common
30
+ // MQ Client Core (re-exported for convenience)
31
+ BaseClient,
32
+ MQClient: BaseClient, // Alias for compatibility
33
+ queueConfig,
34
+
35
+ // Service Common utilities (re-exported for convenience)
27
36
  waitForInfrastructureReady,
28
37
  waitForHealthCheckQueueReady,
38
+ sendMonitoringFailFallbackEmail,
39
+
40
+ // Orchestration utilities
29
41
  initInfrastructureQueues,
30
42
 
31
43
  // Health tracking utilities
@@ -34,6 +46,9 @@ module.exports = {
34
46
  createAmqplibAdapter,
35
47
 
36
48
  // Logger utility (consistent with conn-infra-mq pattern)
37
- createLogger
49
+ createLogger,
50
+
51
+ // Monitoring utilities
52
+ sendQueueMismatchAlert
38
53
  };
39
54
 
@@ -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,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
+