@onlineapps/infrastructure-tools 1.0.3 → 1.0.5

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
@@ -55,6 +55,8 @@ await waitForInfrastructureReady({
55
55
  });
56
56
  ```
57
57
 
58
+ **Note:** `waitForInfrastructureReady` is re-exported from `@onlineapps/service-common` to avoid duplication. Both infrastructure and business services can use it.
59
+
58
60
  ### Initialize Infrastructure Queues
59
61
 
60
62
  Initialize infrastructure queues with correct parameters:
@@ -120,7 +122,7 @@ Creates health publisher adapter for amqplib (direct connection + channel).
120
122
  ## Dependencies
121
123
 
122
124
  - `@onlineapps/mq-client-core` - For queue configuration
123
- - `redis` - For health status checking
125
+ - `@onlineapps/service-common` - For `waitForInfrastructureReady` (shared utility)
124
126
 
125
127
  ## Related Libraries
126
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/infrastructure-tools",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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": {
@@ -18,8 +18,8 @@
18
18
  "author": "OnlineApps",
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
- "@onlineapps/mq-client-core": "^1.0.24",
22
- "redis": "^4.6.0"
21
+ "@onlineapps/mq-client-core": "^1.0.25",
22
+ "@onlineapps/service-common": "^1.0.0"
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 = console
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.debug(`[InfrastructureHealth:${serviceName}] Published health check`, {
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, logger) {
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
- throw new Error('BaseClient instance must have publish() method');
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, logger) {
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
- if (!channel || channel.closed) {
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
- Buffer.from(JSON.stringify(data)),
191
- { persistent: true }
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,22 +10,30 @@
10
10
  * Business services should NOT use this library.
11
11
  */
12
12
 
13
- const { waitForInfrastructureReady } = require('./orchestration/waitForInfrastructureReady');
13
+ // Re-export infrastructure readiness utilities from service-common (no duplication)
14
+ const { waitForInfrastructureReady, waitForHealthCheckQueueReady } = require('@onlineapps/service-common');
15
+
14
16
  const { initInfrastructureQueues } = require('./orchestration/initInfrastructureQueues');
15
17
  const {
16
18
  createHealthPublisher,
17
19
  createBaseClientAdapter,
18
20
  createAmqplibAdapter
19
21
  } = require('./health/healthPublisher');
22
+ const { createLogger } = require('./utils/logger');
20
23
 
21
24
  module.exports = {
22
25
  // Orchestration utilities
26
+ // waitForInfrastructureReady and waitForHealthCheckQueueReady are re-exported from @onlineapps/service-common
23
27
  waitForInfrastructureReady,
28
+ waitForHealthCheckQueueReady,
24
29
  initInfrastructureQueues,
25
30
 
26
31
  // Health tracking utilities
27
32
  createHealthPublisher,
28
33
  createBaseClientAdapter,
29
- createAmqplibAdapter
34
+ createAmqplibAdapter,
35
+
36
+ // Logger utility (consistent with conn-infra-mq pattern)
37
+ createLogger
30
38
  };
31
39
 
@@ -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
+
@@ -1,149 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * waitForInfrastructureReady.js
5
- *
6
- * Waits for all infrastructure services to be verified as running and healthy.
7
- * Used by infrastructure services before creating queues to ensure system consistency.
8
- *
9
- * This prevents race conditions where queues are created before all services are ready.
10
- *
11
- * **How it works:**
12
- * 1. Connects to Redis (where Registry stores infrastructure health status)
13
- * 2. Checks `infrastructure:health:all` key every 5 seconds
14
- * 3. If key is `"true"` → all infrastructure services are UP → return success
15
- * 4. If key is `"false"` or missing → wait and retry
16
- * 5. If timeout reached → throw error
17
- *
18
- * **Why Redis (not HTTP):**
19
- * - Fast: In-memory lookup, no network overhead
20
- * - Reliable: Redis is already required infrastructure
21
- * - Low latency: Direct key lookup, no HTTP parsing
22
- * - Consistent: Same data source as Registry uses
23
- * - No single point of failure: Redis can be clustered
24
- */
25
-
26
- /**
27
- * Wait for all infrastructure services to be ready
28
- * @param {Object} options - Options
29
- * @param {string} [options.redisUrl] - Redis URL (default: REDIS_URL env or redis://api_node_cache:6379)
30
- * @param {number} [options.maxWait] - Maximum wait time in ms (default: INFRASTRUCTURE_HEALTH_WAIT_MAX_TIME env or 300000 = 5 minutes)
31
- * @param {number} [options.checkInterval] - Check interval in ms (default: INFRASTRUCTURE_HEALTH_WAIT_CHECK_INTERVAL env or 5000 = 5 seconds)
32
- * @param {Object} [options.logger] - Logger instance (default: console)
33
- * @returns {Promise<boolean>} - True if all infrastructure services are ready
34
- * @throws {Error} - If timeout is reached
35
- *
36
- * Note: Default values can be overridden via ENV variables or options parameter.
37
- * Registry config (config.infrastructureHealth) is the source of truth for these defaults.
38
- */
39
- async function waitForInfrastructureReady(options = {}) {
40
- const redisUrl = options.redisUrl || process.env.REDIS_URL || 'redis://api_node_cache:6379';
41
- const maxWait = options.maxWait || parseInt(process.env.INFRASTRUCTURE_HEALTH_WAIT_MAX_TIME) || 300000; // 5 minutes
42
- const checkInterval = options.checkInterval || parseInt(process.env.INFRASTRUCTURE_HEALTH_WAIT_CHECK_INTERVAL) || 5000; // 5 seconds
43
- const logger = options.logger || console;
44
-
45
- // Universal logger adapter (supports both console and winston)
46
- // Winston logger can accept string, but to be safe, we'll always use object format for winston
47
- const log = (message) => {
48
- const messageStr = String(message);
49
-
50
- // Check if logger is winston (has child method or format property)
51
- const isWinston = typeof logger.child === 'function' || logger.format !== undefined;
52
-
53
- if (typeof logger.info === 'function') {
54
- if (isWinston) {
55
- // Winston logger - always use object format to avoid property conflicts
56
- logger.info({ message: messageStr });
57
- } else {
58
- // Other logger with info method - use string
59
- logger.info(messageStr);
60
- }
61
- } else if (typeof logger.log === 'function') {
62
- // console.log or similar
63
- logger.log(messageStr);
64
- } else {
65
- console.log(messageStr);
66
- }
67
- };
68
-
69
- const startTime = Date.now();
70
- let attemptCount = 0;
71
- let redis = null;
72
-
73
- log('[InfrastructureReady] Waiting for all infrastructure services to be ready...');
74
- log(`[InfrastructureReady] Redis URL: ${redisUrl}`);
75
- log(`[InfrastructureReady] Max wait: ${maxWait}ms, Check interval: ${checkInterval}ms`);
76
-
77
- try {
78
- // Connect to Redis
79
- const { createClient } = require('redis');
80
- redis = createClient({ url: redisUrl });
81
-
82
- redis.on('error', (err) => {
83
- log(`[InfrastructureReady] Redis error: ${err.message}`);
84
- });
85
-
86
- await redis.connect();
87
- log('[InfrastructureReady] Connected to Redis');
88
-
89
- while (Date.now() - startTime < maxWait) {
90
- attemptCount++;
91
-
92
- try {
93
- // Check Redis key: infrastructure:health:all
94
- const allHealthy = await redis.get('infrastructure:health:all');
95
-
96
- if (allHealthy === 'true') {
97
- // All services are UP, we can proceed
98
- const elapsed = Date.now() - startTime;
99
- log(`[InfrastructureReady] ✓ All infrastructure services are ready (took ${elapsed}ms, ${attemptCount} attempts)`);
100
-
101
- // Optionally log individual service status
102
- // Note: Service names should match config.infrastructureServices in Registry
103
- const serviceKeys = ['gateway', 'registry', 'validator', 'delivery', 'monitoring'];
104
- const statuses = {};
105
- for (const serviceName of serviceKeys) {
106
- const status = await redis.get(`infrastructure:health:${serviceName}`);
107
- if (status) {
108
- statuses[serviceName] = JSON.parse(status);
109
- }
110
- }
111
- log(`[InfrastructureReady] Infrastructure status: ${JSON.stringify(statuses, null, 2)}`);
112
-
113
- return true;
114
- }
115
-
116
- // Not all services ready yet
117
- log(`[InfrastructureReady] Attempt ${attemptCount}: Not all services ready (allHealthy: ${allHealthy || 'null'})`);
118
- log(`[InfrastructureReady] Waiting ${checkInterval}ms before next check...`);
119
-
120
- } catch (error) {
121
- // Redis might not be ready yet, or connection issue
122
- log(`[InfrastructureReady] Attempt ${attemptCount}: Redis check failed (${error.message})`);
123
- log(`[InfrastructureReady] Waiting ${checkInterval}ms before retry...`);
124
- }
125
-
126
- // Wait before next check
127
- await new Promise(resolve => setTimeout(resolve, checkInterval));
128
- }
129
-
130
- // Timeout reached
131
- const elapsed = Date.now() - startTime;
132
- throw new Error(
133
- `Infrastructure services not ready within ${maxWait}ms (${elapsed}ms elapsed, ${attemptCount} attempts). ` +
134
- `Check Redis keys: infrastructure:health:*`
135
- );
136
-
137
- } finally {
138
- // Clean up Redis connection
139
- if (redis && redis.isReady) {
140
- await redis.quit();
141
- log('[InfrastructureReady] Redis connection closed');
142
- }
143
- }
144
- }
145
-
146
- module.exports = {
147
- waitForInfrastructureReady
148
- };
149
-