@onlineapps/mq-client-core 1.0.36 → 1.0.38

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/mq-client-core",
3
- "version": "1.0.36",
3
+ "version": "1.0.38",
4
4
  "description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Jednoduchý in-memory buffer pro zprávy, které čekají na bezpečné odeslání.
5
+ * - Omezený počtem položek (maxSize)
6
+ * - TTL pro jednotlivé položky (ttlMs)
7
+ *
8
+ * Primárně určený pro transient scénáře (health checky apod.).
9
+ */
10
+
11
+ class InMemoryBuffer {
12
+ /**
13
+ * @param {Object} options
14
+ * @param {number} [options.maxSize=100] - Max počet zpráv v bufferu
15
+ * @param {number} [options.ttlMs=300000] - TTL jedné zprávy v ms (default 5 minut)
16
+ * @param {Function} [options.logger] - Volitelný logger (console-like)
17
+ */
18
+ constructor(options = {}) {
19
+ this._maxSize = options.maxSize || 100;
20
+ this._ttlMs = options.ttlMs || 5 * 60 * 1000;
21
+ this._logger = options.logger || console;
22
+
23
+ /** @type {Array<{queue:string, buffer:Buffer, options:Object, createdAt:number, priority:string}>} */
24
+ this._items = [];
25
+ }
26
+
27
+ /**
28
+ * Přidá zprávu do bufferu.
29
+ * Staré/expirující zprávy jsou průběžně čištěny, při překročení kapacity odhazujeme nejstarší.
30
+ */
31
+ async add(queue, buffer, options = {}, priority = 'normal') {
32
+ const now = Date.now();
33
+ this._cleanupExpired(now);
34
+
35
+ if (this._items.length >= this._maxSize) {
36
+ const dropped = this._items.shift();
37
+ this._logger?.warn?.(
38
+ `[InMemoryBuffer] Buffer full (${this._maxSize}), dropping oldest message for queue '${dropped.queue}'`
39
+ );
40
+ }
41
+
42
+ this._items.push({
43
+ queue,
44
+ buffer,
45
+ options,
46
+ createdAt: now,
47
+ priority,
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Flushne všechny aktuálně platné zprávy do poskytnuté publish funkce (FIFO, priority-aware).
53
+ * @param {Function} flushFn - async (queue, buffer, options) => void
54
+ * @returns {Promise<number>} - počet úspěšně flushnutých zpráv
55
+ */
56
+ async flush(flushFn) {
57
+ const now = Date.now();
58
+ this._cleanupExpired(now);
59
+
60
+ if (this._items.length === 0) {
61
+ return 0;
62
+ }
63
+
64
+ // Zkopírujeme a vyprázdníme interní pole, abychom se vyhnuli reentrancy problémům
65
+ const items = this._items.slice();
66
+ this._items = [];
67
+
68
+ // Kritické zprávy (priority === 'critical') házíme dopředu
69
+ items.sort((a, b) => {
70
+ if (a.priority === b.priority) return a.createdAt - b.createdAt;
71
+ if (a.priority === 'critical') return -1;
72
+ if (b.priority === 'critical') return 1;
73
+ return a.createdAt - b.createdAt;
74
+ });
75
+
76
+ let flushed = 0;
77
+ for (const item of items) {
78
+ try {
79
+ await flushFn(item.queue, item.buffer, item.options || {});
80
+ flushed++;
81
+ } catch (err) {
82
+ // Pokud flush selže, vrátíme zprávu zpět do bufferu (na konec fronty),
83
+ // aby ji mohl zpracovat další pokus / worker.
84
+ this._logger?.warn?.(
85
+ `[InMemoryBuffer] Failed to flush message for queue '${item.queue}': ${err.message}`
86
+ );
87
+ await this.add(item.queue, item.buffer, item.options, item.priority);
88
+ }
89
+ }
90
+
91
+ return flushed;
92
+ }
93
+
94
+ /**
95
+ * Aktuální velikost bufferu.
96
+ */
97
+ size() {
98
+ this._cleanupExpired(Date.now());
99
+ return this._items.length;
100
+ }
101
+
102
+ _cleanupExpired(now) {
103
+ const before = this._items.length;
104
+ this._items = this._items.filter(
105
+ (item) => now - item.createdAt <= this._ttlMs
106
+ );
107
+ const removed = before - this._items.length;
108
+ if (removed > 0) {
109
+ this._logger?.info?.(
110
+ `[InMemoryBuffer] Removed ${removed} expired buffered messages`
111
+ );
112
+ }
113
+ }
114
+ }
115
+
116
+ module.exports = InMemoryBuffer;
117
+
118
+
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const InMemoryBuffer = require('./InMemoryBuffer');
4
+ const RedisBuffer = require('./RedisBuffer');
5
+
6
+ /**
7
+ * MessageBuffer – sjednocený interface nad in-memory a (potenciálně) persistentním bufferem.
8
+ *
9
+ * - InMemoryBuffer: pro transient zprávy (např. health checky)
10
+ * - RedisBuffer: pro kritické zprávy (workflow.completed apod.) – aktuálně jen připravený hook
11
+ */
12
+
13
+ class MessageBuffer {
14
+ /**
15
+ * @param {Object} options
16
+ * @param {Object} [options.inMemory] - In-memory buffer options
17
+ * @param {Object} [options.persistent] - Persistent buffer options (Redis)
18
+ * @param {boolean} [options.persistent.enabled=false] - Zapnutí persistentního bufferu
19
+ * @param {Object} [options.logger] - Logger (console-like)
20
+ */
21
+ constructor(options = {}) {
22
+ this._logger = options.logger || console;
23
+
24
+ this._inMemory = new InMemoryBuffer({
25
+ maxSize: options.inMemory?.maxSize,
26
+ ttlMs: options.inMemory?.ttlMs,
27
+ logger: this._logger,
28
+ });
29
+
30
+ const persistentEnabled = !!options.persistent?.enabled;
31
+ this._persistent = persistentEnabled
32
+ ? new RedisBuffer({
33
+ redisClient: options.persistent?.redisClient,
34
+ logger: this._logger,
35
+ })
36
+ : null;
37
+
38
+ if (!persistentEnabled) {
39
+ this._logger?.info?.(
40
+ '[MessageBuffer] Persistent buffer is disabled (persistent.enabled=false)'
41
+ );
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Přidá zprávu do bufferu.
47
+ * @param {string} queue
48
+ * @param {Buffer} buffer
49
+ * @param {Object} options
50
+ * @param {Object} [meta]
51
+ * @param {string} [meta.priority='normal'] - 'normal' | 'critical'
52
+ * @param {boolean} [meta.persistent=false] - zda preferovat persistentní buffer
53
+ */
54
+ async add(queue, buffer, options = {}, meta = {}) {
55
+ const priority = meta.priority || 'normal';
56
+ const wantPersistent = !!meta.persistent;
57
+
58
+ if (wantPersistent && this._persistent) {
59
+ try {
60
+ await this._persistent.add(queue, buffer, options, priority);
61
+ return;
62
+ } catch (err) {
63
+ this._logger?.warn?.(
64
+ `[MessageBuffer] Failed to add message to persistent buffer, falling back to in-memory: ${err.message}`
65
+ );
66
+ }
67
+ }
68
+
69
+ await this._inMemory.add(queue, buffer, options, priority);
70
+ }
71
+
72
+ /**
73
+ * Flushne všechny buffered zprávy přes poskytnutou publish funkci.
74
+ * @param {Function} flushFn - async (queue, buffer, options) => void
75
+ * @returns {Promise<{inMemory:number,persistent:number}>}
76
+ */
77
+ async flush(flushFn) {
78
+ let persistentFlushed = 0;
79
+ if (this._persistent) {
80
+ try {
81
+ persistentFlushed = await this._persistent.flush(flushFn);
82
+ } catch (err) {
83
+ this._logger?.error?.(
84
+ `[MessageBuffer] Failed to flush persistent buffer: ${err.message}`
85
+ );
86
+ }
87
+ }
88
+
89
+ const inMemoryFlushed = await this._inMemory.flush(flushFn);
90
+
91
+ return {
92
+ inMemory: inMemoryFlushed,
93
+ persistent: persistentFlushed,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Aktuální velikost in-memory bufferu.
99
+ */
100
+ size() {
101
+ return this._inMemory.size();
102
+ }
103
+ }
104
+
105
+ module.exports = MessageBuffer;
106
+
107
+
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Placeholder Redis buffer.
5
+ *
6
+ * Záměrně JE minimalistický: v tomto projektu zatím nemáme přímou Redis závislost v mq-client-core.
7
+ * Třída je připravená na budoucí rozšíření – aktuálně pouze no-op / deleguje na in-memory fallback.
8
+ */
9
+
10
+ class RedisBuffer {
11
+ /**
12
+ * @param {Object} options
13
+ * @param {Object} [options.redisClient] - Volitelný Redis klient (s metodami set/get/lpush/brpop atd.)
14
+ * @param {Function} [options.logger] - Logger (console-like)
15
+ */
16
+ constructor(options = {}) {
17
+ this._redis = options.redisClient || null;
18
+ this._logger = options.logger || console;
19
+
20
+ if (!this._redis) {
21
+ this._logger?.info?.(
22
+ '[RedisBuffer] No redisClient provided - RedisBuffer is effectively disabled (will not persist messages)'
23
+ );
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Přidá zprávu do persistentního bufferu.
29
+ * Aktuální implementace je no-op, pokud není k dispozici redisClient.
30
+ */
31
+ async add(/* queue, buffer, options, priority */) {
32
+ if (!this._redis) {
33
+ // No-op; fallback na InMemoryBuffer zajišťuje MessageBuffer
34
+ return;
35
+ }
36
+ // Budoucí rozšíření: implementace zápisu do Redis seznamu/streamu.
37
+ }
38
+
39
+ /**
40
+ * Flushne všechny zprávy z Redis bufferu přes poskytnutý flushFn.
41
+ * Aktuální implementace je no-op, pokud není k dispozici redisClient.
42
+ *
43
+ * @param {Function} flushFn - async (queue, buffer, options) => void
44
+ * @returns {Promise<number>} - počet flushnutých zpráv
45
+ */
46
+ async flush(flushFn) {
47
+ if (!this._redis) {
48
+ return 0;
49
+ }
50
+ // Budoucí rozšíření: čtení z Redis (např. list/stream) a volání flushFn.
51
+ return 0;
52
+ }
53
+ }
54
+
55
+ module.exports = RedisBuffer;
56
+
57
+
package/src/index.js CHANGED
@@ -19,6 +19,12 @@ const {
19
19
  ConsumeError,
20
20
  SerializationError,
21
21
  } = require('./utils/errorHandler');
22
+ const {
23
+ TransientPublishError,
24
+ PermanentPublishError,
25
+ QueueNotFoundError,
26
+ classifyPublishError,
27
+ } = require('./utils/publishErrors');
22
28
 
23
29
  // Export BaseClient as default (constructor), with additional named exports
24
30
  // NOTE: When destructuring, use: const { BaseClient } = require('@onlineapps/mq-client-core');
@@ -34,4 +40,10 @@ module.exports.errors = {
34
40
  ConsumeError,
35
41
  SerializationError,
36
42
  };
43
+ module.exports.publishErrors = {
44
+ TransientPublishError,
45
+ PermanentPublishError,
46
+ QueueNotFoundError,
47
+ classifyPublishError,
48
+ };
37
49
 
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ TransientPublishError,
5
+ PermanentPublishError,
6
+ QueueNotFoundError,
7
+ } = require('../utils/publishErrors');
8
+ const MessageBuffer = require('../buffer/MessageBuffer');
9
+
10
+ /**
11
+ * PublishLayer - čistá vrstva pro publish s retry + buffer
12
+ *
13
+ * Odděluje:
14
+ * - Retry logiku (exponential backoff)
15
+ * - Bufferování při transient chybách
16
+ * - Delegaci na RabbitMQClient._publishOnce() (čisté publish bez retry)
17
+ */
18
+ class PublishLayer {
19
+ /**
20
+ * @param {Object} options
21
+ * @param {Object} options.client - Instance RabbitMQClient
22
+ * @param {Object} [options.logger] - Logger
23
+ * @param {Object} [options.bufferConfig] - Konfigurace pro MessageBuffer
24
+ * @param {boolean} [options.retryEnabled=true] - Zapnout retry
25
+ * @param {number} [options.maxRetries=3] - Max počet pokusů
26
+ * @param {number} [options.retryBaseDelay=100] - Base delay v ms
27
+ * @param {number} [options.retryMaxDelay=5000] - Max delay v ms
28
+ * @param {number} [options.retryBackoffMultiplier=2] - Backoff multiplikátor
29
+ */
30
+ constructor(options = {}) {
31
+ if (!options.client) {
32
+ throw new Error('PublishLayer requires a client instance');
33
+ }
34
+
35
+ this._client = options.client;
36
+ this._logger = options.logger || console;
37
+
38
+ // Retry konfigurace - bere z client config nebo options
39
+ this._retryEnabled = options.retryEnabled !== undefined
40
+ ? options.retryEnabled
41
+ : (this._client._publishRetryEnabled !== false);
42
+ this._maxRetries = options.maxRetries || this._client._publishMaxRetries || 3;
43
+ this._retryBaseDelay = options.retryBaseDelay || this._client._publishRetryBaseDelay || 100;
44
+ this._retryMaxDelay = options.retryMaxDelay || this._client._publishRetryMaxDelay || 5000;
45
+ this._retryBackoffMultiplier = options.retryBackoffMultiplier || this._client._publishRetryBackoffMultiplier || 2;
46
+
47
+ // Buffer
48
+ this._buffer = new MessageBuffer({
49
+ inMemory: options.bufferConfig?.inMemory,
50
+ persistent: options.bufferConfig?.persistent,
51
+ logger: this._logger,
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Publish s retry + buffer logikou
57
+ * @param {string} queue
58
+ * @param {Buffer} buffer
59
+ * @param {Object} [options]
60
+ */
61
+ async publish(queue, buffer, options = {}) {
62
+ const priority = options.priority || 'normal';
63
+ const usePersistentBuffer =
64
+ priority === 'critical' && !!this._client._config?.persistentBufferEnabled;
65
+
66
+ if (this._retryEnabled) {
67
+ return await this._publishWithRetry(queue, buffer, options, priority, usePersistentBuffer);
68
+ } else {
69
+ return await this._publishOnce(queue, buffer, options, priority, usePersistentBuffer);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Single publish attempt - deleguje na RabbitMQClient._publishOnce()
75
+ * @private
76
+ */
77
+ async _publishOnce(queue, buffer, options, priority, usePersistentBuffer) {
78
+ try {
79
+ await this._client._publishOnce(queue, buffer, options);
80
+ } catch (err) {
81
+ return await this._handlePublishError(err, queue, buffer, options, priority, usePersistentBuffer);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Publish s retry logikou
87
+ * @private
88
+ */
89
+ async _publishWithRetry(queue, buffer, options, priority, usePersistentBuffer) {
90
+ let lastError = null;
91
+ let attempt = 0;
92
+
93
+ while (attempt < this._maxRetries) {
94
+ attempt++;
95
+
96
+ try {
97
+ // Track attempt (i při prvním pokusu pro monitoring)
98
+ this._client.emit('publish:retry', {
99
+ queue,
100
+ attempt,
101
+ maxRetries: this._maxRetries,
102
+ lastError: lastError?.message
103
+ });
104
+ if (attempt > 1) {
105
+ this._logger?.log?.(`[PublishLayer] Retry attempt ${attempt}/${this._maxRetries} for queue "${queue}"`);
106
+ }
107
+
108
+ await this._client._publishOnce(queue, buffer, options);
109
+
110
+ // Track success (always emit, even for first attempt)
111
+ this._client.emit('publish:success', {
112
+ queue,
113
+ attempt,
114
+ totalAttempts: attempt
115
+ });
116
+ if (attempt > 1) {
117
+ this._logger?.log?.(`[PublishLayer] ✓ Published successfully after ${attempt} attempts for queue "${queue}"`);
118
+ }
119
+
120
+ return; // Success
121
+
122
+ } catch (err) {
123
+ lastError = err;
124
+
125
+ // Non-retryable errors - fail immediately
126
+ if (err instanceof PermanentPublishError || err instanceof QueueNotFoundError) {
127
+ this._client.emit('publish:failed', {
128
+ queue,
129
+ attempt,
130
+ error: err.message,
131
+ retryable: false
132
+ });
133
+ throw err;
134
+ }
135
+
136
+ // Transient errors - check if we should retry or buffer
137
+ if (err instanceof TransientPublishError) {
138
+ // If we have more attempts, retry
139
+ if (attempt < this._maxRetries) {
140
+ // Wait for reconnection if in progress
141
+ if (this._client._reconnecting) {
142
+ this._logger?.log?.(`[PublishLayer] Connection reconnecting, waiting before retry ${attempt + 1}/${this._maxRetries}...`);
143
+ try {
144
+ await this._client._waitForReconnection();
145
+ this._logger?.log?.(`[PublishLayer] ✓ Reconnection completed, proceeding with retry ${attempt + 1}/${this._maxRetries}`);
146
+ continue; // Retry immediately after reconnect
147
+ } catch (reconnectErr) {
148
+ // Reconnection failed - buffer and fail
149
+ await this._bufferMessage(queue, buffer, options, priority, usePersistentBuffer, err);
150
+ this._client.emit('publish:failed', {
151
+ queue,
152
+ attempt,
153
+ error: `Reconnection failed: ${reconnectErr.message}`,
154
+ retryable: false,
155
+ reconnectFailed: true
156
+ });
157
+ throw new Error(`Publish failed: reconnection failed after ${attempt} attempts for queue "${queue}": ${reconnectErr.message}`);
158
+ }
159
+ }
160
+
161
+ // Exponential backoff
162
+ const delay = Math.min(
163
+ this._retryBaseDelay * Math.pow(this._retryBackoffMultiplier, attempt - 1),
164
+ this._retryMaxDelay
165
+ );
166
+ this._logger?.warn?.(`[PublishLayer] Retryable error for queue "${queue}" (attempt ${attempt}/${this._maxRetries}), waiting ${delay}ms before retry: ${err.message}`);
167
+ await new Promise(resolve => setTimeout(resolve, delay));
168
+ continue;
169
+ }
170
+
171
+ // Max retries exceeded - buffer and fail
172
+ await this._bufferMessage(queue, buffer, options, priority, usePersistentBuffer, err);
173
+ this._client.emit('publish:failed', {
174
+ queue,
175
+ attempt,
176
+ error: err.message,
177
+ retryable: true,
178
+ maxRetriesExceeded: true
179
+ });
180
+ throw new Error(`Publish failed after ${attempt} attempts for queue "${queue}": ${err.message}`);
181
+ }
182
+
183
+ // Unknown error - fail immediately
184
+ throw err;
185
+ }
186
+ }
187
+
188
+ throw lastError || new Error(`Publish failed for queue "${queue}" after ${attempt} attempts`);
189
+ }
190
+
191
+ /**
192
+ * Handle publish error - buffer transient errors
193
+ * @private
194
+ */
195
+ async _handlePublishError(err, queue, buffer, options, priority, usePersistentBuffer) {
196
+ if (err instanceof QueueNotFoundError) {
197
+ this._logger?.error?.(`[PublishLayer] QueueNotFoundError for '${queue}' (infra=${err.isInfrastructure}): ${err.message}`);
198
+ throw err;
199
+ }
200
+
201
+ if (err instanceof TransientPublishError) {
202
+ await this._bufferMessage(queue, buffer, options, priority, usePersistentBuffer, err);
203
+ throw err;
204
+ }
205
+
206
+ throw err;
207
+ }
208
+
209
+ /**
210
+ * Buffer message při transient error
211
+ * @private
212
+ */
213
+ async _bufferMessage(queue, buffer, options, priority, usePersistentBuffer, err) {
214
+ try {
215
+ await this._buffer.add(queue, buffer, options, {
216
+ priority,
217
+ persistent: usePersistentBuffer,
218
+ });
219
+ this._logger?.warn?.(`[PublishLayer] Buffered message for queue '${queue}' due to transient error: ${err.message}`);
220
+ this._client.emit('publish:buffered', { queue });
221
+ } catch (bufferErr) {
222
+ this._logger?.error?.(`[PublishLayer] Failed to buffer message for queue '${queue}': ${bufferErr.message}`);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Flush buffered messages po reconnectu
228
+ */
229
+ async flushBuffered(flushOptions = {}) {
230
+ const result = await this._buffer.flush(async (queue, buf, opts) => {
231
+ const mergedOptions = Object.assign({}, opts, flushOptions);
232
+ await this._client._publishOnce(queue, buf, mergedOptions);
233
+ });
234
+
235
+ this._logger?.info?.(`[PublishLayer] Flushed buffered messages (inMemory=${result.inMemory}, persistent=${result.persistent})`);
236
+ return result;
237
+ }
238
+ }
239
+
240
+ module.exports = PublishLayer;
241
+
242
+
@@ -0,0 +1,138 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PublishMonitor - tracking a metriky pro publish operace
5
+ */
6
+ class PublishMonitor {
7
+ /**
8
+ * @param {Object} options
9
+ * @param {Object} [options.logger] - Logger
10
+ */
11
+ constructor(options = {}) {
12
+ this._logger = options.logger || console;
13
+
14
+ // Metrics
15
+ this._metrics = {
16
+ attempts: 0,
17
+ successes: 0,
18
+ failures: 0,
19
+ retries: 0,
20
+ buffered: 0,
21
+ flushed: 0,
22
+ };
23
+
24
+ // Per-queue metrics
25
+ this._queueMetrics = new Map(); // queue -> { attempts, successes, failures, retries }
26
+ }
27
+
28
+ /**
29
+ * Track publish attempt
30
+ */
31
+ trackAttempt(queue) {
32
+ this._metrics.attempts++;
33
+ this._updateQueueMetrics(queue, 'attempts');
34
+ }
35
+
36
+ /**
37
+ * Track publish success
38
+ */
39
+ trackSuccess(queue, attempt = 1) {
40
+ this._metrics.successes++;
41
+ if (attempt > 1) {
42
+ this._metrics.retries += (attempt - 1);
43
+ }
44
+ this._updateQueueMetrics(queue, 'successes', attempt);
45
+ }
46
+
47
+ /**
48
+ * Track publish failure
49
+ */
50
+ trackFailure(queue, retryable = false) {
51
+ this._metrics.failures++;
52
+ this._updateQueueMetrics(queue, 'failures', 0, retryable);
53
+ }
54
+
55
+ /**
56
+ * Track buffered message
57
+ */
58
+ trackBuffered(queue) {
59
+ this._metrics.buffered++;
60
+ }
61
+
62
+ /**
63
+ * Track flushed messages
64
+ */
65
+ trackFlushed(count) {
66
+ this._metrics.flushed += count;
67
+ }
68
+
69
+ /**
70
+ * Get metrics
71
+ */
72
+ getMetrics() {
73
+ return {
74
+ ...this._metrics,
75
+ queues: Array.from(this._queueMetrics.entries()).map(([queue, metrics]) => ({
76
+ queue,
77
+ ...metrics
78
+ }))
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Get Prometheus metrics format
84
+ */
85
+ getPrometheusMetrics() {
86
+ const lines = [];
87
+ lines.push(`# HELP mq_publish_attempts Total publish attempts`);
88
+ lines.push(`# TYPE mq_publish_attempts counter`);
89
+ lines.push(`mq_publish_attempts ${this._metrics.attempts}`);
90
+
91
+ lines.push(`# HELP mq_publish_successes Total successful publishes`);
92
+ lines.push(`# TYPE mq_publish_successes counter`);
93
+ lines.push(`mq_publish_successes ${this._metrics.successes}`);
94
+
95
+ lines.push(`# HELP mq_publish_failures Total failed publishes`);
96
+ lines.push(`# TYPE mq_publish_failures counter`);
97
+ lines.push(`mq_publish_failures ${this._metrics.failures}`);
98
+
99
+ lines.push(`# HELP mq_publish_retries Total retry attempts`);
100
+ lines.push(`# TYPE mq_publish_retries counter`);
101
+ lines.push(`mq_publish_retries ${this._metrics.retries}`);
102
+
103
+ lines.push(`# HELP mq_publish_buffered Total buffered messages`);
104
+ lines.push(`# TYPE mq_publish_buffered counter`);
105
+ lines.push(`mq_publish_buffered ${this._metrics.buffered}`);
106
+
107
+ lines.push(`# HELP mq_publish_flushed Total flushed messages`);
108
+ lines.push(`# TYPE mq_publish_flushed counter`);
109
+ lines.push(`mq_publish_flushed ${this._metrics.flushed}`);
110
+
111
+ return lines.join('\n');
112
+ }
113
+
114
+ /**
115
+ * Update queue metrics
116
+ * @private
117
+ */
118
+ _updateQueueMetrics(queue, type, attempt = 1, retryable = false) {
119
+ if (!this._queueMetrics.has(queue)) {
120
+ this._queueMetrics.set(queue, {
121
+ attempts: 0,
122
+ successes: 0,
123
+ failures: 0,
124
+ retries: 0,
125
+ });
126
+ }
127
+
128
+ const metrics = this._queueMetrics.get(queue);
129
+ metrics[type]++;
130
+
131
+ if (type === 'successes' && attempt > 1) {
132
+ metrics.retries += (attempt - 1);
133
+ }
134
+ }
135
+ }
136
+
137
+ module.exports = PublishMonitor;
138
+