@onlineapps/mq-client-core 2.0.1 → 3.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.
- package/CHANGELOG.md +153 -0
- package/README.md +491 -8
- package/package.json +12 -8
- package/src/BaseClient.js +953 -80
- package/src/buffer/InMemoryBuffer.js +50 -9
- package/src/buffer/MessageBuffer.js +20 -52
- package/src/config/composeConfig.js +60 -0
- package/src/config/configSchema.js +398 -17
- package/src/config/defaultConfig.js +169 -15
- package/src/config/deliveryPolicy.js +165 -0
- package/src/config/queueConfig.js +738 -64
- package/src/config.js +29 -0
- package/src/defaults.js +43 -0
- package/src/index.js +91 -2
- package/src/layers/PublishLayer.js +83 -37
- package/src/monitoring/PublishMonitor.js +11 -4
- package/src/monitoring-publish.js +81 -54
- package/src/transports/rabbitmqClient.js +2698 -738
- package/src/transports/transportFactory.js +9 -2
- package/src/utils/errorHandler.js +83 -4
- package/src/utils/nearestKey.js +101 -0
- package/src/utils/publishErrors.js +95 -10
- package/src/utils/redactCredentials.js +106 -0
- package/src/utils/serializer.js +12 -2
- package/src/workers/RecoveryWorker.js +58 -81
- package/src/buffer/RedisBuffer.js +0 -57
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* Provides resilient, fail-safe publishing to monitoring queues (monitoring.workflow, monitoring.services).
|
|
5
5
|
*
|
|
6
6
|
* Principles:
|
|
7
|
-
* - Fail-safe:
|
|
7
|
+
* - Fail-safe about PUBLISHING: a broker that is down or a queue that does not
|
|
8
|
+
* exist yet returns `false` and is reported, never thrown. The single
|
|
9
|
+
* exception is a missing or incomplete `logger`, which is a programming error
|
|
10
|
+
* in the caller: without it the "always logs failures" half of this promise
|
|
11
|
+
* cannot hold, so the call refuses instead of dropping events in silence.
|
|
8
12
|
* - Resilient: Handles queue unavailability gracefully (queue doesn't exist yet, RabbitMQ down)
|
|
9
13
|
* - Unified: Single API for all monitoring publish operations across all services
|
|
10
14
|
* - Encapsulated: All monitoring publish logic in one place, no duplication
|
|
@@ -15,13 +19,30 @@
|
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
21
|
const { PublishError, ConnectionError } = require('./utils/errorHandler');
|
|
22
|
+
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
18
23
|
const { QueueNotFoundError, classifyPublishError } = require('./utils/publishErrors');
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* Check if error indicates queue doesn't exist or RabbitMQ is unavailable
|
|
27
|
+
*
|
|
28
|
+
* The queue is named because the last question it asks —
|
|
29
|
+
* `classifyPublishError()` — builds a `QueueNotFoundError`, and that error carries
|
|
30
|
+
* a queue name it must be told (d.435); the caller has had it all along.
|
|
31
|
+
*
|
|
32
|
+
* @param {Error} error - The error a publish to `queueName` produced.
|
|
33
|
+
* @param {string} queueName - The queue that publish was aimed at.
|
|
22
34
|
* @private
|
|
23
35
|
*/
|
|
24
|
-
function isQueueUnavailableError(error) {
|
|
36
|
+
function isQueueUnavailableError(error, queueName) {
|
|
37
|
+
if (typeof queueName !== 'string' || queueName === '') {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'[monitoring-publish] isQueueUnavailableError requires the queue name - '
|
|
40
|
+
+ 'Expected: the caller names the queue whose publish failed, so the classification it asks '
|
|
41
|
+
+ 'for carries a real name. Fix: pass it as the second argument, e.g. '
|
|
42
|
+
+ 'isQueueUnavailableError(error, queueName).'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
if (!error) return false;
|
|
26
47
|
|
|
27
48
|
const errorMessage = error.message || String(error);
|
|
@@ -50,7 +71,7 @@ function isQueueUnavailableError(error) {
|
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
// Classify publish errors
|
|
53
|
-
const classified = classifyPublishError(error);
|
|
74
|
+
const classified = classifyPublishError(error, queueName);
|
|
54
75
|
if (classified instanceof QueueNotFoundError) {
|
|
55
76
|
return true;
|
|
56
77
|
}
|
|
@@ -64,91 +85,97 @@ function isQueueUnavailableError(error) {
|
|
|
64
85
|
* @param {Object} mqClient - BaseClient instance (must be connected)
|
|
65
86
|
* @param {string} queueName - Queue name ('monitoring.workflow' or 'monitoring.services')
|
|
66
87
|
* @param {Object} message - Message to publish (must include event_type)
|
|
67
|
-
* @param {Object}
|
|
88
|
+
* @param {Object} logger - Logger with info/warn/error/debug (REQUIRED)
|
|
68
89
|
* @param {Object} [context] - Additional context for logging (optional)
|
|
69
90
|
* @returns {Promise<boolean>} - true if published successfully, false otherwise (never throws)
|
|
91
|
+
* @throws {Error} If the logger is missing or incomplete — the ONLY throw here.
|
|
70
92
|
*/
|
|
71
|
-
async function publishToMonitoringResilient(mqClient, queueName, message, logger
|
|
93
|
+
async function publishToMonitoringResilient(mqClient, queueName, message, logger, context = {}) {
|
|
94
|
+
// BREAKING (2026-09-07): the logger is required and validated up front. This
|
|
95
|
+
// function is fail-safe by design — every failure path below returns `false`
|
|
96
|
+
// and says why through the logger — so a missing logger turned it into a
|
|
97
|
+
// silent no-op: the caller got `false` and nobody ever learned that monitoring
|
|
98
|
+
// events were being dropped. Owner confirmation
|
|
99
|
+
// `docs/governance/confirmations/connector-logger-contract.md` 001/002.
|
|
100
|
+
assertLogger(
|
|
101
|
+
'publishToMonitoringResilient',
|
|
102
|
+
logger,
|
|
103
|
+
'every skipped or failed monitoring publish is reported instead of silently returning false',
|
|
104
|
+
'pass the logger your service already built as the fourth argument'
|
|
105
|
+
);
|
|
106
|
+
|
|
72
107
|
// Validate inputs
|
|
73
108
|
if (!mqClient) {
|
|
74
|
-
|
|
75
|
-
logger.warn('Monitoring publish skipped: mqClient not provided', context);
|
|
76
|
-
}
|
|
109
|
+
logger.warn('Monitoring publish skipped: mqClient not provided', context);
|
|
77
110
|
return false;
|
|
78
111
|
}
|
|
79
112
|
|
|
80
113
|
if (!mqClient.isConnected || typeof mqClient.isConnected !== 'function' || !mqClient.isConnected()) {
|
|
81
|
-
|
|
82
|
-
logger.warn(`Monitoring publish skipped: mqClient not connected (queue: ${queueName})`, context);
|
|
83
|
-
}
|
|
114
|
+
logger.warn(`Monitoring publish skipped: mqClient not connected (queue: ${queueName})`, context);
|
|
84
115
|
return false;
|
|
85
116
|
}
|
|
86
117
|
|
|
87
118
|
if (!message || !message.event_type) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
93
|
-
}
|
|
119
|
+
logger.warn(`Monitoring publish skipped: message missing event_type (queue: ${queueName})`, {
|
|
120
|
+
...context,
|
|
121
|
+
hasMessage: !!message
|
|
122
|
+
});
|
|
94
123
|
return false;
|
|
95
124
|
}
|
|
96
125
|
|
|
97
126
|
// Validate queue name
|
|
98
127
|
if (queueName !== 'monitoring.workflow' && queueName !== 'monitoring.services') {
|
|
99
|
-
|
|
100
|
-
logger.warn(`Monitoring publish skipped: invalid queue name (expected monitoring.workflow or monitoring.services, got: ${queueName})`, context);
|
|
101
|
-
}
|
|
128
|
+
logger.warn(`Monitoring publish skipped: invalid queue name (expected monitoring.workflow or monitoring.services, got: ${queueName})`, context);
|
|
102
129
|
return false;
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
try {
|
|
106
133
|
const WORKFLOW_FANOUT = 'monitoring.workflow.fanout';
|
|
134
|
+
// The exchange is NAMED, not declared: it is a fanout owned by the monitoring
|
|
135
|
+
// consumer, which asserts it from the declaration in `config/queueConfig.js`
|
|
136
|
+
// ('monitoring' → 'workflow.fanout'). `exchangeType` stood here for the
|
|
137
|
+
// per-publish `assertExchange()` the publish path made until d.342, and left
|
|
138
|
+
// with it — a key nothing reads is not kept lying around
|
|
139
|
+
// (`change-discipline.md` § Removing something removes its declaration).
|
|
107
140
|
const publishOptions = queueName === 'monitoring.workflow'
|
|
108
|
-
? { exchange: WORKFLOW_FANOUT,
|
|
141
|
+
? { exchange: WORKFLOW_FANOUT, routingKey: '' }
|
|
109
142
|
: {};
|
|
110
143
|
await mqClient.publish(queueName, message, publishOptions);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
144
|
+
|
|
145
|
+
logger.debug(`Published to ${queueName}`, {
|
|
146
|
+
...context,
|
|
147
|
+
event_type: message.event_type,
|
|
148
|
+
workflow_id: message.workflow_id,
|
|
149
|
+
service_name: message.service_name
|
|
150
|
+
});
|
|
151
|
+
|
|
121
152
|
return true;
|
|
122
153
|
} catch (error) {
|
|
123
154
|
// Check if this is a queue unavailable error
|
|
124
|
-
if (isQueueUnavailableError(error)) {
|
|
155
|
+
if (isQueueUnavailableError(error, queueName)) {
|
|
125
156
|
// Queue doesn't exist yet or RabbitMQ is unavailable - log but don't fail
|
|
126
|
-
|
|
127
|
-
logger.warn(`Monitoring queue ${queueName} not available (queue may not exist yet or RabbitMQ unavailable)`, {
|
|
128
|
-
...context,
|
|
129
|
-
error: error.message,
|
|
130
|
-
errorCode: error.code,
|
|
131
|
-
event_type: message.event_type,
|
|
132
|
-
workflow_id: message.workflow_id,
|
|
133
|
-
service_name: message.service_name
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Other errors - log as warning but don't fail
|
|
140
|
-
if (logger && logger.warn) {
|
|
141
|
-
logger.warn(`Failed to publish to monitoring queue ${queueName}`, {
|
|
157
|
+
logger.warn(`Monitoring queue ${queueName} not available (queue may not exist yet or RabbitMQ unavailable)`, {
|
|
142
158
|
...context,
|
|
143
159
|
error: error.message,
|
|
144
160
|
errorCode: error.code,
|
|
145
|
-
errorName: error.name,
|
|
146
161
|
event_type: message.event_type,
|
|
147
162
|
workflow_id: message.workflow_id,
|
|
148
163
|
service_name: message.service_name
|
|
149
164
|
});
|
|
165
|
+
return false;
|
|
150
166
|
}
|
|
151
|
-
|
|
167
|
+
|
|
168
|
+
// Other errors - log as warning but don't fail
|
|
169
|
+
logger.warn(`Failed to publish to monitoring queue ${queueName}`, {
|
|
170
|
+
...context,
|
|
171
|
+
error: error.message,
|
|
172
|
+
errorCode: error.code,
|
|
173
|
+
errorName: error.name,
|
|
174
|
+
event_type: message.event_type,
|
|
175
|
+
workflow_id: message.workflow_id,
|
|
176
|
+
service_name: message.service_name
|
|
177
|
+
});
|
|
178
|
+
|
|
152
179
|
return false;
|
|
153
180
|
}
|
|
154
181
|
}
|
|
@@ -158,7 +185,7 @@ async function publishToMonitoringResilient(mqClient, queueName, message, logger
|
|
|
158
185
|
*
|
|
159
186
|
* @param {Object} mqClient - BaseClient instance (must be connected)
|
|
160
187
|
* @param {Object} message - Message with event_type ('completed' | 'failed' | 'progress'), workflow_id, etc.
|
|
161
|
-
* @param {Object}
|
|
188
|
+
* @param {Object} logger - Logger with info/warn/error/debug (REQUIRED)
|
|
162
189
|
* @param {Object} [context] - Additional context for logging (optional)
|
|
163
190
|
* @returns {Promise<boolean>} - true if published successfully, false otherwise (never throws)
|
|
164
191
|
*
|
|
@@ -172,7 +199,7 @@ async function publishToMonitoringResilient(mqClient, queueName, message, logger
|
|
|
172
199
|
* timestamp: new Date().toISOString()
|
|
173
200
|
* }, logger);
|
|
174
201
|
*/
|
|
175
|
-
async function publishToMonitoringWorkflow(mqClient, message, logger
|
|
202
|
+
async function publishToMonitoringWorkflow(mqClient, message, logger, context = {}) {
|
|
176
203
|
return publishToMonitoringResilient(mqClient, 'monitoring.workflow', message, logger, context);
|
|
177
204
|
}
|
|
178
205
|
|
|
@@ -181,7 +208,7 @@ async function publishToMonitoringWorkflow(mqClient, message, logger = null, con
|
|
|
181
208
|
*
|
|
182
209
|
* @param {Object} mqClient - BaseClient instance (must be connected)
|
|
183
210
|
* @param {Object} message - Message with event_type ('service.registered' | 'service.validation.completed' | etc.), service_name, etc.
|
|
184
|
-
* @param {Object}
|
|
211
|
+
* @param {Object} logger - Logger with info/warn/error/debug (REQUIRED)
|
|
185
212
|
* @param {Object} [context] - Additional context for logging (optional)
|
|
186
213
|
* @returns {Promise<boolean>} - true if published successfully, false otherwise (never throws)
|
|
187
214
|
*
|
|
@@ -194,7 +221,7 @@ async function publishToMonitoringWorkflow(mqClient, message, logger = null, con
|
|
|
194
221
|
* timestamp: new Date().toISOString()
|
|
195
222
|
* }, logger);
|
|
196
223
|
*/
|
|
197
|
-
async function publishToMonitoringServices(mqClient, message, logger
|
|
224
|
+
async function publishToMonitoringServices(mqClient, message, logger, context = {}) {
|
|
198
225
|
return publishToMonitoringResilient(mqClient, 'monitoring.services', message, logger, context);
|
|
199
226
|
}
|
|
200
227
|
|