queen-mq 0.12.3 → 0.13.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.
@@ -2,9 +2,64 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- The Queen Client V2 includes comprehensive operation logging that captures every significant action performed by the client. Logging is **disabled by default** and can be enabled via the `QUEEN_CLIENT_LOG` environment variable.
5
+ The Queen Client V2 includes comprehensive operation logging that captures every significant action performed by the client. You can either:
6
6
 
7
- ## Enabling Logging
7
+ 1. **Inject a custom logger** (pino, winston, bunyan, etc.) via the `logger` config option -- recommended for production.
8
+ 2. **Use the built-in console logger** gated by the `QUEEN_CLIENT_LOG` environment variable -- handy for quick debugging.
9
+
10
+ ## Custom Logger (Recommended)
11
+
12
+ Pass any logger instance that implements `info()`, `warn()`, and `error()` methods. When a custom logger is configured, it is always active (level filtering is controlled by your logger, not by `QUEEN_CLIENT_LOG`).
13
+
14
+ ### Pino
15
+
16
+ ```javascript
17
+ import pino from 'pino'
18
+ import { Queen } from '@punkish/queen'
19
+
20
+ const queen = new Queen({
21
+ urls: ['http://localhost:6632'],
22
+ logger: pino()
23
+ })
24
+ ```
25
+
26
+ ### Winston
27
+
28
+ ```javascript
29
+ import winston from 'winston'
30
+ import { Queen } from '@punkish/queen'
31
+
32
+ const winstonLogger = winston.createLogger({
33
+ level: 'info',
34
+ transports: [new winston.transports.Console()]
35
+ })
36
+
37
+ const queen = new Queen({
38
+ urls: ['http://localhost:6632'],
39
+ logger: winstonLogger
40
+ })
41
+ ```
42
+
43
+ ### Custom implementation
44
+
45
+ Any object with `info`, `warn`, and `error` methods works:
46
+
47
+ ```javascript
48
+ const queen = new Queen({
49
+ urls: ['http://localhost:6632'],
50
+ logger: {
51
+ info: (msg) => myLogSink('INFO', msg),
52
+ warn: (msg) => myLogSink('WARN', msg),
53
+ error: (msg) => myLogSink('ERROR', msg)
54
+ }
55
+ })
56
+ ```
57
+
58
+ The `debug()` method is optional. If your logger provides it, verbose internal details (buffer flush progress, server responses, etc.) will be emitted at `debug` level. Otherwise they fall back to `info`.
59
+
60
+ ## Built-in Console Logger
61
+
62
+ If no custom logger is provided, the built-in console logger can be enabled via environment variable:
8
63
 
9
64
  ```bash
10
65
  export QUEEN_CLIENT_LOG=true
@@ -158,11 +213,13 @@ pm2 restart my-app
158
213
 
159
214
  ## Performance Impact
160
215
 
161
- - **Disabled (default)**: Zero performance impact - all logging calls are no-ops
162
- - **Enabled**: Minimal impact - logging is asynchronous and uses `console.log/warn/error`
216
+ - **No logger configured + `QUEEN_CLIENT_LOG` unset (default)**: Zero performance impact - all logging calls are no-ops
217
+ - **Built-in console logger enabled**: Minimal impact
218
+ - **Custom logger**: Depends on the logger implementation; pino in async mode has negligible overhead
163
219
 
164
220
  ## Log Levels
165
221
 
222
+ - **DEBUG**: Verbose operational details (buffer flush progress, extracted message counts, server responses)
166
223
  - **INFO**: Normal operations (most logs)
167
224
  - **WARN**: Recoverable issues (retries, failover, network errors)
168
225
  - **ERROR**: Operation failures (push failed, ack failed, etc.)
@@ -191,31 +248,7 @@ QUEEN_CLIENT_LOG=true node app.js 2>&1 | grep "pop"
191
248
 
192
249
  ## Integration with Log Aggregation
193
250
 
194
- The structured JSON format makes it easy to integrate with log aggregation tools:
195
-
196
- ### Winston
197
- ```javascript
198
- import winston from 'winston'
199
-
200
- // Redirect console.log to Winston
201
- console.log = winston.info
202
- console.error = winston.error
203
- console.warn = winston.warn
204
- ```
205
-
206
- ### Pino
207
- ```javascript
208
- import pino from 'pino'
209
- const logger = pino()
210
-
211
- console.log = (msg) => logger.info(msg)
212
- console.error = (msg) => logger.error(msg)
213
- console.warn = (msg) => logger.warn(msg)
214
- ```
215
-
216
- ### Datadog, CloudWatch, etc.
217
-
218
- The ISO 8601 timestamps and JSON format are compatible with most log aggregation services.
251
+ When using a custom logger, integration with log aggregation services (Datadog, CloudWatch, Elastic, etc.) depends on your logger's configuration, not on Queen. For example, pino outputs NDJSON by default which is compatible with most log aggregation pipelines out of the box.
219
252
 
220
253
  ## Best Practices
221
254
 
@@ -236,5 +269,5 @@ The logging system is implemented in `utils/logger.js` and imported by all major
236
269
  - `QueueBuilder.js` - Queue operations
237
270
  - `TransactionBuilder.js` - Atomic transactions
238
271
 
239
- All logging calls check the `QUEEN_CLIENT_LOG` environment variable and are no-ops when disabled, ensuring zero performance impact by default.
272
+ When a custom logger is configured via `new Queen({ logger })`, all internal logging is routed through it. When no custom logger is provided, logging falls back to the built-in console logger gated by `QUEEN_CLIENT_LOG`.
240
273
 
@@ -23,6 +23,11 @@ export class Queen {
23
23
  #admin = null
24
24
 
25
25
  constructor(config = {}) {
26
+ // Configure custom logger before anything else
27
+ if (config && typeof config === 'object' && !Array.isArray(config) && config.logger) {
28
+ logger.configure(config.logger)
29
+ }
30
+
26
31
  logger.log('Queen.constructor', { config: typeof config === 'object' && !Array.isArray(config) ? { ...config, urls: config.urls?.length || 0 } : { type: typeof config } })
27
32
 
28
33
  // Normalize config
@@ -117,17 +122,17 @@ export class Queen {
117
122
 
118
123
  let signalReceivedCount = 0;
119
124
  const shutdown = async (signal) => {
120
- console.log(`\nReceived ${signal}, shutting down gracefully...`)
125
+ logger.log('Queen.shutdown', { signal })
121
126
  try {
122
127
  signalReceivedCount++;
123
128
  if (signalReceivedCount > 1) {
124
- console.log('Received multiple shutdown signals, exiting immediately')
129
+ logger.warn('Queen.shutdown', 'Received multiple shutdown signals, exiting immediately')
125
130
  process.exit(1)
126
131
  }
127
132
  await this.close()
128
133
  process.exit(0)
129
134
  } catch (error) {
130
- console.error('Error during shutdown:', error)
135
+ logger.error('Queen.shutdown', { error: error.message })
131
136
  process.exit(1)
132
137
  }
133
138
  }
@@ -480,16 +485,13 @@ export class Queen {
480
485
 
481
486
  async close() {
482
487
  logger.log('Queen.close', 'Starting shutdown')
483
- console.log('Closing Queen client...')
484
488
 
485
489
  // Flush all buffers
486
490
  try {
487
491
  await this.#bufferManager.flushAllBuffers()
488
492
  logger.log('Queen.close', 'All buffers flushed')
489
- console.log('All buffers flushed')
490
493
  } catch (error) {
491
494
  logger.error('Queen.close', { error: error.message, phase: 'buffer-flush' })
492
- console.warn('Error flushing buffers:', error)
493
495
  }
494
496
 
495
497
  // Cleanup buffer manager
@@ -502,7 +504,6 @@ export class Queen {
502
504
  this.#shutdownHandlers = []
503
505
 
504
506
  logger.log('Queen.close', 'Client closed successfully')
505
- console.log('Queen client closed')
506
507
  }
507
508
  }
508
509
 
@@ -36,13 +36,11 @@ export class BufferManager {
36
36
  async #flushBuffer(queueAddress) {
37
37
  const buffer = this.#buffers.get(queueAddress)
38
38
  if (!buffer || buffer.messageCount === 0) {
39
- logger.log('BufferManager.flushBuffer', { queueAddress, status: 'empty' })
40
- console.log(`No buffer or empty buffer for ${queueAddress}`)
39
+ logger.debug('BufferManager.flushBuffer', { queueAddress, status: 'empty' })
41
40
  return
42
41
  }
43
42
 
44
43
  logger.log('BufferManager.flushBuffer', { queueAddress, messageCount: buffer.messageCount })
45
- console.log(`Flushing ${buffer.messageCount} messages for ${queueAddress}`)
46
44
  buffer.setFlushing(true)
47
45
 
48
46
  // Create a promise for this flush and track it
@@ -50,13 +48,13 @@ export class BufferManager {
50
48
  try {
51
49
  const messages = buffer.extractMessages()
52
50
 
53
- console.log(`Extracted ${messages.length} messages, sending to server...`)
51
+ logger.debug('BufferManager.flushBuffer', { queueAddress, extracted: messages.length })
54
52
 
55
53
  if (messages.length === 0) return
56
54
 
57
55
  // Send to server
58
56
  const result = await this.#httpClient.post('/api/v1/push', { items: messages })
59
- console.log(`Server responded:`, result ? `${result.length || 'N/A'} items` : 'null')
57
+ logger.debug('BufferManager.flushBuffer', { queueAddress, serverResponse: result ? `${result.length || 'N/A'} items` : 'null' })
60
58
 
61
59
  this.#flushCount++
62
60
  logger.log('BufferManager.flushBuffer', { queueAddress, status: 'success', messagesSent: messages.length })
@@ -66,7 +64,6 @@ export class BufferManager {
66
64
 
67
65
  } catch (error) {
68
66
  logger.error('BufferManager.flushBuffer', { queueAddress, error: error.message })
69
- console.error(`Flush error for ${queueAddress}:`, error.message)
70
67
  buffer.setFlushing(false)
71
68
  throw error
72
69
  } finally {
@@ -94,13 +91,13 @@ export class BufferManager {
94
91
  try {
95
92
  const messages = buffer.extractMessages(batchSize)
96
93
 
97
- console.log(`Extracted ${messages.length} messages, sending to server...`)
94
+ logger.debug('BufferManager.flushBufferBatch', { queueAddress, extracted: messages.length })
98
95
 
99
96
  if (messages.length === 0) return
100
97
 
101
98
  // Send to server
102
99
  const result = await this.#httpClient.post('/api/v1/push', { items: messages })
103
- console.log(`Server responded:`, result ? `${result.length || 'N/A'} items` : 'null')
100
+ logger.debug('BufferManager.flushBufferBatch', { queueAddress, serverResponse: result ? `${result.length || 'N/A'} items` : 'null' })
104
101
 
105
102
  this.#flushCount++
106
103
 
@@ -112,7 +109,7 @@ export class BufferManager {
112
109
  }
113
110
 
114
111
  } catch (error) {
115
- console.error(`Flush error for ${queueAddress}:`, error.message)
112
+ logger.error('BufferManager.flushBufferBatch', { queueAddress, error: error.message })
116
113
  buffer.setFlushing(false)
117
114
  throw error
118
115
  } finally {
@@ -129,14 +126,10 @@ export class BufferManager {
129
126
 
130
127
  async flushBuffer(queueAddress) {
131
128
  logger.log('BufferManager.flushBuffer', { queueAddress, activeBuffers: this.#buffers.size, pendingFlushes: this.#pendingFlushes.size })
132
- console.log(`flushBuffer called for address: ${queueAddress}`)
133
- console.log(`Active buffers:`, Array.from(this.#buffers.keys()))
134
- console.log(`Pending flushes:`, this.#pendingFlushes.size)
135
129
 
136
130
  const buffer = this.#buffers.get(queueAddress)
137
131
  if (!buffer) {
138
- logger.log('BufferManager.flushBuffer', { queueAddress, status: 'not-found' })
139
- console.log(`No buffer found for ${queueAddress}`)
132
+ logger.debug('BufferManager.flushBuffer', { queueAddress, status: 'not-found' })
140
133
  await this.#waitForPendingFlushes()
141
134
  return
142
135
  }
@@ -149,22 +142,20 @@ export class BufferManager {
149
142
 
150
143
  // Flush all messages in batches
151
144
  while (buffer.messageCount > 0) {
152
- console.log(`Flushing batch of up to ${batchSize} messages (${buffer.messageCount} remaining)`)
145
+ logger.debug('BufferManager.flushBuffer', { queueAddress, batchSize, remaining: buffer.messageCount })
153
146
  await this.#flushBufferBatch(queueAddress, batchSize)
154
147
  }
155
148
 
156
149
  // Wait for all pending flushes to complete
157
150
  await this.#waitForPendingFlushes()
158
151
 
159
- console.log(`flushBuffer completed for ${queueAddress}`)
152
+ logger.debug('BufferManager.flushBuffer', { queueAddress, status: 'completed' })
160
153
  }
161
154
 
162
155
  async flushAllBuffers() {
156
+ // Get all queue addresses that have buffers
163
157
  const queueAddresses = Array.from(this.#buffers.keys())
164
158
  logger.log('BufferManager.flushAllBuffers', { bufferCount: queueAddresses.length, pendingFlushes: this.#pendingFlushes.size })
165
- console.log(`flushAllBuffers called, pending flushes: ${this.#pendingFlushes.size}`)
166
-
167
- // Get all queue addresses that have buffers
168
159
 
169
160
  // Flush each buffer in batches
170
161
  for (const queueAddress of queueAddresses) {
@@ -172,15 +163,14 @@ export class BufferManager {
172
163
  }
173
164
 
174
165
  logger.log('BufferManager.flushAllBuffers', { status: 'completed' })
175
- console.log(`flushAllBuffers completed`)
176
166
  }
177
167
 
178
168
  async #waitForPendingFlushes() {
179
169
  if (this.#pendingFlushes.size === 0) return
180
170
 
181
- console.log(`Waiting for ${this.#pendingFlushes.size} pending flushes...`)
171
+ logger.debug('BufferManager.waitForPendingFlushes', { count: this.#pendingFlushes.size })
182
172
  await Promise.all(Array.from(this.#pendingFlushes))
183
- console.log(`All pending flushes completed`)
173
+ logger.debug('BufferManager.waitForPendingFlushes', { status: 'completed' })
184
174
  }
185
175
 
186
176
  getStats() {
@@ -212,4 +202,3 @@ export class BufferManager {
212
202
  this.#buffers.clear()
213
203
  }
214
204
  }
215
-
@@ -306,7 +306,6 @@ export class QueueBuilder {
306
306
  } catch (error) {
307
307
  // Return empty array on error instead of throwing
308
308
  logger.error('QueueBuilder.pop', { error: error.message })
309
- console.warn('Pop failed:', error.message)
310
309
  return []
311
310
  }
312
311
  }
@@ -745,7 +744,6 @@ class DLQBuilder {
745
744
  return result || { messages: [], total: 0 }
746
745
  } catch (error) {
747
746
  logger.error('DLQBuilder.get', { error: error.message })
748
- console.warn('DLQ query failed:', error.message)
749
747
  return { messages: [], total: 0 }
750
748
  }
751
749
  }
@@ -222,7 +222,6 @@ export class ConsumerManager {
222
222
 
223
223
  if (isNetworkError) {
224
224
  logger.warn('ConsumerManager.worker', { workerId, error: 'network', message: error.message })
225
- console.warn(`Worker ${workerId}: Network error - ${error.message}`)
226
225
  // Wait before retry
227
226
  await new Promise(resolve => setTimeout(resolve, 1000))
228
227
  continue
@@ -296,7 +295,7 @@ export class ConsumerManager {
296
295
  try {
297
296
  await this.#queen.renew(messages)
298
297
  } catch (error) {
299
- console.error('Lease renewal failed:', error)
298
+ logger.error('ConsumerManager.leaseRenewal', { error: error.message })
300
299
  }
301
300
  }, intervalMillis)
302
301
  }
@@ -367,7 +366,7 @@ export class ConsumerManager {
367
366
  error: error.message,
368
367
  phase: 'trace-failed'
369
368
  })
370
- console.warn(`[TRACE FAILED] ${message.transactionId}: ${error.message}`)
369
+ logger.warn('ConsumerManager.trace', { transactionId: message.transactionId, error: error.message })
371
370
 
372
371
  return { success: false, error: error.message }
373
372
  }
@@ -196,7 +196,6 @@ export class HttpClient {
196
196
  }
197
197
 
198
198
  logger.warn('HttpClient.failover', { url, method, path, error: error.message })
199
- console.warn(`Request failed for ${url}: ${method} ${path} - ${error.message}`)
200
199
 
201
200
  // Don't retry on client errors (4xx)
202
201
  if (error.status && error.status >= 400 && error.status < 500) {
@@ -16,7 +16,8 @@ export const CLIENT_DEFAULTS = {
16
16
  healthRetryAfterMillis: 5000, // Retry unhealthy backends after 5 seconds
17
17
  bearerToken: null, // Bearer token for proxy authentication
18
18
  headers: {}, // Custom headers to include in every request
19
- handleSignals: true // Register SIGINT/SIGTERM handlers (disable when used as a library)
19
+ handleSignals: true, // Register SIGINT/SIGTERM handlers (disable when used as a library)
20
+ logger: null // Custom logger instance (must implement info/warn/error)
20
21
  }
21
22
 
22
23
  export const QUEUE_DEFAULTS = {
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Logger utility for Queen Client v2
3
- * Controlled by QUEEN_CLIENT_LOG environment variable (Node.js)
4
- * or window.QUEEN_CLIENT_LOG (Browser)
3
+ *
4
+ * Supports pluggable logger backends (pino, winston, bunyan, etc.).
5
+ * When no custom logger is configured, falls back to console-based logging
6
+ * gated by the QUEEN_CLIENT_LOG environment variable (Node.js)
7
+ * or window.QUEEN_CLIENT_LOG (Browser).
5
8
  */
6
9
 
7
10
  const LOG_ENABLED = (() => {
@@ -16,9 +19,8 @@ const LOG_ENABLED = (() => {
16
19
  return false
17
20
  })()
18
21
 
19
- /**
20
- * Get formatted timestamp
21
- */
22
+ let customLogger = null
23
+
22
24
  function getTimestamp() {
23
25
  return new Date().toISOString()
24
26
  }
@@ -33,25 +35,56 @@ function formatLog(operation, details, level = 'INFO') {
33
35
  }
34
36
 
35
37
  /**
36
- * Log an operation
38
+ * Configure a custom logger backend.
39
+ * The logger must implement: info(msg), warn(msg), error(msg).
40
+ * debug(msg) is optional and falls back to info(msg) if missing.
41
+ * When a custom logger is set, it is always active (no env var gating).
42
+ * @param {object} logger - Logger instance (e.g. pino(), winston.createLogger())
37
43
  */
44
+ export function configure(logger) {
45
+ if (logger && typeof logger.info !== 'function') {
46
+ throw new Error('Custom logger must implement info(), warn(), and error() methods')
47
+ }
48
+ customLogger = logger
49
+ }
50
+
38
51
  export function log(operation, details) {
52
+ if (customLogger) {
53
+ const detailsStr = typeof details === 'object' ? JSON.stringify(details) : details
54
+ customLogger.info(`[${operation}] ${detailsStr}`)
55
+ return
56
+ }
39
57
  if (!LOG_ENABLED) return
40
58
  console.log(formatLog(operation, details))
41
59
  }
42
60
 
43
- /**
44
- * Log a warning
45
- */
61
+ export function debug(operation, details) {
62
+ if (customLogger) {
63
+ const detailsStr = typeof details === 'object' ? JSON.stringify(details) : details
64
+ const fn = customLogger.debug || customLogger.info
65
+ fn.call(customLogger, `[${operation}] ${detailsStr}`)
66
+ return
67
+ }
68
+ if (!LOG_ENABLED) return
69
+ console.log(formatLog(operation, details, 'DEBUG'))
70
+ }
71
+
46
72
  export function warn(operation, details) {
73
+ if (customLogger) {
74
+ const detailsStr = typeof details === 'object' ? JSON.stringify(details) : details
75
+ customLogger.warn(`[${operation}] ${detailsStr}`)
76
+ return
77
+ }
47
78
  if (!LOG_ENABLED) return
48
79
  console.warn(formatLog(operation, details, 'WARN'))
49
80
  }
50
81
 
51
- /**
52
- * Log an error
53
- */
54
82
  export function error(operation, details) {
83
+ if (customLogger) {
84
+ const detailsStr = typeof details === 'object' ? JSON.stringify(details) : details
85
+ customLogger.error(`[${operation}] ${detailsStr}`)
86
+ return
87
+ }
55
88
  if (!LOG_ENABLED) return
56
89
  console.error(formatLog(operation, details, 'ERROR'))
57
90
  }
@@ -60,6 +93,5 @@ export function error(operation, details) {
60
93
  * Check if logging is enabled
61
94
  */
62
95
  export function isEnabled() {
63
- return LOG_ENABLED
96
+ return customLogger != null || LOG_ENABLED
64
97
  }
65
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "queen-mq",
3
- "version": "0.12.3",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "High-performance C++ message queue backed by PostgreSQL",
6
6
  "main": "client-v2/index.js",