@onlineapps/service-common 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/package.json +1 -1
- package/src/index.js +25 -1
- package/src/monitoring-publish.js +133 -0
- package/src/postgresClient.js +104 -0
- package/src/redisClient.js +16 -5
- package/src/runtime-config.js +34 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -17,15 +17,27 @@ const {
|
|
|
17
17
|
createRedisClient,
|
|
18
18
|
connectRedis
|
|
19
19
|
} = require('./redisClient');
|
|
20
|
+
const {
|
|
21
|
+
buildPostgresUrl,
|
|
22
|
+
createPostgresPool,
|
|
23
|
+
connectPostgres
|
|
24
|
+
} = require('./postgresClient');
|
|
20
25
|
const {
|
|
21
26
|
requireEnv,
|
|
22
27
|
optionalEnv,
|
|
23
28
|
optionalNumberEnv,
|
|
24
29
|
getCriticalConfig,
|
|
30
|
+
getCriticalConfigWithFallbacks,
|
|
25
31
|
getOptionalInfraConfig,
|
|
26
32
|
getServiceConfig,
|
|
27
33
|
getInfrastructureHealthConfig
|
|
28
34
|
} = require('./runtime-config');
|
|
35
|
+
const {
|
|
36
|
+
publishToMonitoringResilient,
|
|
37
|
+
publishToMonitoringWorkflow,
|
|
38
|
+
publishToMonitoringServices,
|
|
39
|
+
isQueueUnavailableError
|
|
40
|
+
} = require('./monitoring-publish');
|
|
29
41
|
|
|
30
42
|
module.exports = {
|
|
31
43
|
// Infrastructure readiness utilities (used by both infrastructure and business services)
|
|
@@ -37,17 +49,29 @@ module.exports = {
|
|
|
37
49
|
createRedisClient,
|
|
38
50
|
connectRedis,
|
|
39
51
|
|
|
52
|
+
// PostgreSQL utilities (shared between services and tests)
|
|
53
|
+
buildPostgresUrl,
|
|
54
|
+
createPostgresPool,
|
|
55
|
+
connectPostgres,
|
|
56
|
+
|
|
40
57
|
// Configuration helpers (NO FALLBACKS for critical infrastructure)
|
|
41
58
|
requireEnv,
|
|
42
59
|
optionalEnv,
|
|
43
60
|
optionalNumberEnv,
|
|
44
61
|
getCriticalConfig,
|
|
62
|
+
getCriticalConfigWithFallbacks, // EXCEPTION: Fallbacks allowed ONLY for infrastructure clients
|
|
45
63
|
getOptionalInfraConfig,
|
|
46
64
|
getServiceConfig,
|
|
47
65
|
getInfrastructureHealthConfig,
|
|
48
66
|
|
|
49
67
|
// Reporting utilities
|
|
50
|
-
sendMonitoringFailFallbackEmail
|
|
68
|
+
sendMonitoringFailFallbackEmail,
|
|
69
|
+
|
|
70
|
+
// Resilient monitoring publish utilities
|
|
71
|
+
publishToMonitoringResilient,
|
|
72
|
+
publishToMonitoringWorkflow,
|
|
73
|
+
publishToMonitoringServices,
|
|
74
|
+
isQueueUnavailableError
|
|
51
75
|
};
|
|
52
76
|
|
|
53
77
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilient monitoring publish helper
|
|
3
|
+
*
|
|
4
|
+
* Publishes to monitoring queues (monitoring.workflow, monitoring.services) with fail-safe behavior.
|
|
5
|
+
* If the queue doesn't exist or RabbitMQ is unavailable, logs the error and continues without failing.
|
|
6
|
+
*
|
|
7
|
+
* This ensures services can start and operate even if monitoring queues aren't ready yet.
|
|
8
|
+
* Monitoring queues are created when health:all is true, so this is a temporary state during startup.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if error indicates queue doesn't exist or RabbitMQ is unavailable
|
|
13
|
+
*/
|
|
14
|
+
function isQueueUnavailableError(error) {
|
|
15
|
+
if (!error) return false;
|
|
16
|
+
|
|
17
|
+
const errorMessage = error.message || String(error);
|
|
18
|
+
const errorCode = error.code || '';
|
|
19
|
+
|
|
20
|
+
// RabbitMQ connection errors
|
|
21
|
+
if (errorCode === 'ECONNREFUSED' || errorCode === 'ENOTFOUND' || errorCode === 'ETIMEDOUT') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Queue doesn't exist errors (from RabbitMQ)
|
|
26
|
+
if (errorMessage.includes('NOT_FOUND') ||
|
|
27
|
+
errorMessage.includes('404') ||
|
|
28
|
+
errorMessage.includes('no queue') ||
|
|
29
|
+
errorMessage.includes('queue does not exist') ||
|
|
30
|
+
errorMessage.includes('Channel closed') ||
|
|
31
|
+
errorMessage.includes('Connection closed')) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resilient publish to monitoring queue
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} mqClient - MQ client instance (must have publish method)
|
|
42
|
+
* @param {string} queueName - Queue name (e.g., 'monitoring.workflow', 'monitoring.services')
|
|
43
|
+
* @param {Object} message - Message to publish
|
|
44
|
+
* @param {Object} logger - Logger instance (optional, for logging)
|
|
45
|
+
* @param {Object} context - Additional context for logging (optional)
|
|
46
|
+
* @returns {Promise<boolean>} - true if published successfully, false otherwise
|
|
47
|
+
*/
|
|
48
|
+
async function publishToMonitoringResilient(mqClient, queueName, message, logger = null, context = {}) {
|
|
49
|
+
if (!mqClient || typeof mqClient.publish !== 'function') {
|
|
50
|
+
if (logger) {
|
|
51
|
+
logger.warn('Monitoring publish skipped: mqClient not available', context);
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await mqClient.publish(queueName, message);
|
|
58
|
+
|
|
59
|
+
if (logger) {
|
|
60
|
+
logger.debug(`Published to ${queueName}`, {
|
|
61
|
+
...context,
|
|
62
|
+
event_type: message.event_type,
|
|
63
|
+
workflow_id: message.workflow_id,
|
|
64
|
+
service_name: message.service_name
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// Check if this is a queue unavailable error
|
|
71
|
+
if (isQueueUnavailableError(error)) {
|
|
72
|
+
// Queue doesn't exist yet or RabbitMQ is unavailable - log but don't fail
|
|
73
|
+
if (logger) {
|
|
74
|
+
logger.warn(`Monitoring queue ${queueName} not available (queue may not exist yet or RabbitMQ unavailable)`, {
|
|
75
|
+
...context,
|
|
76
|
+
error: error.message,
|
|
77
|
+
event_type: message.event_type,
|
|
78
|
+
workflow_id: message.workflow_id,
|
|
79
|
+
service_name: message.service_name
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Other errors - log as warning but don't fail
|
|
86
|
+
if (logger) {
|
|
87
|
+
logger.warn(`Failed to publish to monitoring queue ${queueName}`, {
|
|
88
|
+
...context,
|
|
89
|
+
error: error.message,
|
|
90
|
+
errorCode: error.code,
|
|
91
|
+
event_type: message.event_type,
|
|
92
|
+
workflow_id: message.workflow_id,
|
|
93
|
+
service_name: message.service_name
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resilient publish to monitoring.workflow queue
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} mqClient - MQ client instance
|
|
105
|
+
* @param {Object} message - Message with event_type, workflow_id, etc.
|
|
106
|
+
* @param {Object} logger - Logger instance (optional)
|
|
107
|
+
* @param {Object} context - Additional context for logging (optional)
|
|
108
|
+
* @returns {Promise<boolean>}
|
|
109
|
+
*/
|
|
110
|
+
async function publishToMonitoringWorkflow(mqClient, message, logger = null, context = {}) {
|
|
111
|
+
return publishToMonitoringResilient(mqClient, 'monitoring.workflow', message, logger, context);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resilient publish to monitoring.services queue
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} mqClient - MQ client instance
|
|
118
|
+
* @param {Object} message - Message with event_type, service_name, etc.
|
|
119
|
+
* @param {Object} logger - Logger instance (optional)
|
|
120
|
+
* @param {Object} context - Additional context for logging (optional)
|
|
121
|
+
* @returns {Promise<boolean>}
|
|
122
|
+
*/
|
|
123
|
+
async function publishToMonitoringServices(mqClient, message, logger = null, context = {}) {
|
|
124
|
+
return publishToMonitoringResilient(mqClient, 'monitoring.services', message, logger, context);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
publishToMonitoringResilient,
|
|
129
|
+
publishToMonitoringWorkflow,
|
|
130
|
+
publishToMonitoringServices,
|
|
131
|
+
isQueueUnavailableError
|
|
132
|
+
};
|
|
133
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PostgreSQL Client Utilities
|
|
5
|
+
*
|
|
6
|
+
* Provides helper functions for building PostgreSQL connection URLs
|
|
7
|
+
* with fallback support for infrastructure client configuration.
|
|
8
|
+
*
|
|
9
|
+
* EXCEPTION: Fallbacks are allowed ONLY for infrastructure client configuration.
|
|
10
|
+
* This is the ONLY place where fallbacks are acceptable.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a PostgreSQL connection URL from environment.
|
|
15
|
+
*
|
|
16
|
+
* EXCEPTION: Fallbacks are allowed ONLY for infrastructure client configuration.
|
|
17
|
+
* This is the ONLY place where fallbacks are acceptable.
|
|
18
|
+
*
|
|
19
|
+
* Environment variable precedence (checked in order):
|
|
20
|
+
* 1. POSTGRES_URL (full URL, e.g. postgres://monitoring:monitoring_pass@api_monitoring_postgres:5432/monitoring) - ENV variable name
|
|
21
|
+
* 2. POSTGRES_HOST + POSTGRES_PORT + POSTGRES_USER + POSTGRES_PASSWORD + POSTGRES_DB (if POSTGRES_URL not set)
|
|
22
|
+
* 3. Fallback: postgres://monitoring:monitoring_pass@api_monitoring_postgres:5432/monitoring (docker-compose default)
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} options - Options
|
|
25
|
+
* @param {Object} [options.env=process.env] - Environment variables
|
|
26
|
+
* @param {Object} [options.defaults] - Default host/port/user/password/db overrides
|
|
27
|
+
* @returns {string} PostgreSQL connection URL
|
|
28
|
+
*/
|
|
29
|
+
function buildPostgresUrl(options = {}) {
|
|
30
|
+
const env = options.env || process.env || {};
|
|
31
|
+
const defaults = options.defaults || {};
|
|
32
|
+
|
|
33
|
+
// Priority 1: POSTGRES_URL from environment (ENV variable name: POSTGRES_URL)
|
|
34
|
+
if (env.POSTGRES_URL && typeof env.POSTGRES_URL === 'string' && env.POSTGRES_URL.length > 0) {
|
|
35
|
+
return env.POSTGRES_URL;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Priority 2: POSTGRES_HOST + POSTGRES_PORT + POSTGRES_USER + POSTGRES_PASSWORD + POSTGRES_DB (if POSTGRES_URL not set)
|
|
39
|
+
// Fallback for docker-compose environment (defined in library, not in docker-compose.yml)
|
|
40
|
+
const host = env.POSTGRES_HOST || defaults.host || 'api_monitoring_postgres';
|
|
41
|
+
const port = env.POSTGRES_PORT || defaults.port || '5432';
|
|
42
|
+
const user = env.POSTGRES_USER || defaults.user || 'monitoring';
|
|
43
|
+
const password = env.POSTGRES_PASSWORD || defaults.password || 'monitoring_pass';
|
|
44
|
+
const database = env.POSTGRES_DB || defaults.database || 'monitoring';
|
|
45
|
+
|
|
46
|
+
return `postgres://${user}:${password}@${host}:${port}/${database}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a PostgreSQL connection pool
|
|
51
|
+
*
|
|
52
|
+
* @param {Object} options - Options
|
|
53
|
+
* @param {Object} [options.env=process.env] - Environment variables
|
|
54
|
+
* @param {Object} [options.defaults] - Default connection parameters
|
|
55
|
+
* @param {Object} [options.poolConfig] - Additional pool configuration (max, idleTimeoutMillis, etc.)
|
|
56
|
+
* @returns {Object} PostgreSQL Pool instance
|
|
57
|
+
*/
|
|
58
|
+
function createPostgresPool(options = {}) {
|
|
59
|
+
const { Pool } = require('pg');
|
|
60
|
+
|
|
61
|
+
const connectionString = buildPostgresUrl({
|
|
62
|
+
env: options.env,
|
|
63
|
+
defaults: options.defaults
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const poolConfig = {
|
|
67
|
+
connectionString,
|
|
68
|
+
max: options.poolConfig?.max || 10,
|
|
69
|
+
idleTimeoutMillis: options.poolConfig?.idleTimeoutMillis || 30000,
|
|
70
|
+
connectionTimeoutMillis: options.poolConfig?.connectionTimeoutMillis || 2000,
|
|
71
|
+
...options.poolConfig
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return new Pool(poolConfig);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Connect to PostgreSQL and test connection
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} pool - PostgreSQL Pool instance
|
|
81
|
+
* @param {Object} logger - Logger object with info/error methods
|
|
82
|
+
* @returns {Promise<void>}
|
|
83
|
+
*/
|
|
84
|
+
async function connectPostgres(pool, logger = console) {
|
|
85
|
+
try {
|
|
86
|
+
await pool.query('SELECT NOW()');
|
|
87
|
+
if (logger && logger.info) {
|
|
88
|
+
logger.info('[PostgreSQL] Connected');
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (logger && logger.error) {
|
|
93
|
+
logger.error('[PostgreSQL] Connection failed:', error);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
buildPostgresUrl,
|
|
101
|
+
createPostgresPool,
|
|
102
|
+
connectPostgres
|
|
103
|
+
};
|
|
104
|
+
|
package/src/redisClient.js
CHANGED
|
@@ -17,20 +17,31 @@ const { createClient } = require('redis');
|
|
|
17
17
|
/**
|
|
18
18
|
* Build a Redis connection URL from environment.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* EXCEPTION: Fallbacks are allowed ONLY for infrastructure client configuration.
|
|
21
|
+
* This is the ONLY place where fallbacks are acceptable.
|
|
22
|
+
*
|
|
23
|
+
* Environment variable precedence (checked in order):
|
|
24
|
+
* 1. REDIS_URL (full URL, e.g. redis://api_node_cache:6379) - ENV variable name
|
|
25
|
+
* 2. REDIS_HOST + REDIS_PORT (if REDIS_URL not set)
|
|
26
|
+
* 3. Fallback: redis://api_node_cache:6379 (docker-compose default)
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} options - Options
|
|
29
|
+
* @param {Object} [options.env=process.env] - Environment variables
|
|
30
|
+
* @param {Object} [options.defaults] - Default host/port overrides
|
|
31
|
+
* @returns {string} Redis connection URL
|
|
24
32
|
*/
|
|
25
33
|
function buildRedisUrl(options = {}) {
|
|
26
34
|
const env = options.env || process.env || {};
|
|
27
35
|
const defaults = options.defaults || {};
|
|
28
36
|
|
|
37
|
+
// Priority 1: REDIS_URL from environment (ENV variable name: REDIS_URL)
|
|
29
38
|
if (env.REDIS_URL && typeof env.REDIS_URL === 'string' && env.REDIS_URL.length > 0) {
|
|
30
39
|
return env.REDIS_URL;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
// Priority 2: REDIS_HOST + REDIS_PORT (if REDIS_URL not set)
|
|
43
|
+
// Fallback for docker-compose environment (defined in library, not in docker-compose.yml)
|
|
44
|
+
const host = env.REDIS_HOST || defaults.host || 'api_node_cache';
|
|
34
45
|
const port = env.REDIS_PORT || defaults.port || '6379';
|
|
35
46
|
|
|
36
47
|
return `redis://${host}:${port}`;
|
package/src/runtime-config.js
CHANGED
|
@@ -63,8 +63,12 @@ function optionalNumberEnv(name, defaultValue) {
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* Get critical infrastructure configuration
|
|
66
|
-
*
|
|
66
|
+
*
|
|
67
|
+
* NOTE: This function is DEPRECATED - use getCriticalConfigWithFallbacks() instead.
|
|
68
|
+
* This function requires ENV variables and fails fast (NO FALLBACKS).
|
|
69
|
+
*
|
|
67
70
|
* @returns {Object} Critical infrastructure config
|
|
71
|
+
* @throws {Error} If REDIS_URL or RABBITMQ_URL are not set
|
|
68
72
|
*/
|
|
69
73
|
function getCriticalConfig() {
|
|
70
74
|
return {
|
|
@@ -73,6 +77,34 @@ function getCriticalConfig() {
|
|
|
73
77
|
};
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Get critical infrastructure configuration with fallbacks
|
|
82
|
+
*
|
|
83
|
+
* EXCEPTION: Fallbacks are allowed ONLY for infrastructure client configuration.
|
|
84
|
+
* This is the ONLY place where fallbacks are acceptable.
|
|
85
|
+
*
|
|
86
|
+
* Environment variables (checked in order):
|
|
87
|
+
* - REDIS_URL: Full Redis URL (e.g., redis://api_node_cache:6379)
|
|
88
|
+
* - RABBITMQ_URL: Full RabbitMQ URL (e.g., amqp://guest:guest@api_services_queuer:5672)
|
|
89
|
+
*
|
|
90
|
+
* Fallbacks (used only if ENV is not set):
|
|
91
|
+
* - Redis: redis://api_node_cache:6379 (docker-compose default)
|
|
92
|
+
* - RabbitMQ: amqp://guest:guest@api_services_queuer:5672 (docker-compose default)
|
|
93
|
+
*
|
|
94
|
+
* @returns {Object} Critical infrastructure config with fallbacks
|
|
95
|
+
*/
|
|
96
|
+
function getCriticalConfigWithFallbacks() {
|
|
97
|
+
// Fallback URLs for docker-compose environment (defined in library, not in docker-compose.yml)
|
|
98
|
+
// These are used ONLY if ENV variables are not set
|
|
99
|
+
const redisFallback = 'redis://api_node_cache:6379';
|
|
100
|
+
const rabbitmqFallback = 'amqp://guest:guest@api_services_queuer:5672';
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
REDIS_URL: optionalEnv('REDIS_URL', redisFallback),
|
|
104
|
+
RABBITMQ_URL: optionalEnv('RABBITMQ_URL', rabbitmqFallback)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
76
108
|
/**
|
|
77
109
|
* Get optional infrastructure configuration
|
|
78
110
|
* These have sensible defaults for development
|
|
@@ -127,6 +159,7 @@ module.exports = {
|
|
|
127
159
|
optionalEnv,
|
|
128
160
|
optionalNumberEnv,
|
|
129
161
|
getCriticalConfig,
|
|
162
|
+
getCriticalConfigWithFallbacks,
|
|
130
163
|
getOptionalInfraConfig,
|
|
131
164
|
getServiceConfig,
|
|
132
165
|
getInfrastructureHealthConfig
|