@rawnodes/logger 2.7.1 → 2.7.2

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
@@ -134,6 +134,19 @@ const logger = Logger.create({
134
134
 
135
135
  This is useful for sending only critical errors to alerting systems while keeping verbose logs in console.
136
136
 
137
+ #### Level precedence
138
+
139
+ When deciding whether to emit a log entry, the following order applies — **most specific wins**:
140
+
141
+ 1. **Global override** from `setLevelOverride(...)` or the top-level `level.rules` array
142
+ 2. **Per-transport rule** (a matching entry in `console.rules` / `file.rules` / etc.)
143
+ 3. **Per-transport level** (`console.level`, `file.level`, …)
144
+ 4. **Global default level** (`level` or `level.default`)
145
+
146
+ Practically: a matching `setLevelOverride({ userId: 123 }, 'debug')` will surface debug logs to **every** transport, even one whose own `level` is set to `info`. This is intentional — dynamic overrides are for targeted troubleshooting, and they wouldn't be useful if a downstream transport could silently swallow the messages they just enabled.
147
+
148
+ If you need a transport to stay narrow while overrides are active, use `rules` on that transport (level 2), not the plain `level` field.
149
+
137
150
  ### Per-Transport Rules
138
151
 
139
152
  Each transport can have its own filtering rules. Use `level: 'off'` with rules to create a whitelist pattern:
@@ -635,6 +648,43 @@ const masker = createMasker({
635
648
 
636
649
  **Default masked patterns:** `password`, `secret`, `token`, `apikey`, `api_key`, `api-key`, `auth`, `credential`, `private`
637
650
 
651
+ ## Error logging
652
+
653
+ `error()` accepts an error value as the **first** argument. This keeps call sites short in the common case — you forward whatever `catch` produced without pre-normalizing it.
654
+
655
+ ```typescript
656
+ try {
657
+ await doWork();
658
+ } catch (err) {
659
+ // err: unknown — passes straight through, no cast needed
660
+ logger.error(err, 'doWork failed', { userId });
661
+ }
662
+ ```
663
+
664
+ Supported shapes:
665
+
666
+ ```typescript
667
+ logger.error('something broke'); // message only
668
+ logger.error('something broke', { retries: 3 }); // message + meta
669
+ logger.error(err); // error only (uses err.message)
670
+ logger.error(err, 'doWork failed'); // error + message
671
+ logger.error(err, { userId }); // error + meta
672
+ logger.error(err, 'doWork failed', { userId }); // error + message + meta
673
+ ```
674
+
675
+ The first argument is interpreted as an error whenever it is **not a string**. That includes anything TypeScript's `catch` clause can produce (`Error`, plain objects, primitives, `null`, `undefined`). Non-`Error` values are normalized via `serializeError`:
676
+
677
+ | Input | Log fields extracted |
678
+ |---------------------------------------|-------------------------------------------------------|
679
+ | `new Error('boom')` | `errorMessage`, `stack`, `errorName` (if not `Error`) |
680
+ | `'string value'` | `errorMessage: 'string value'` |
681
+ | axios error | `errorMessage`, `stack`, `http: { status, url, … }` |
682
+ | fetch-style `{ response, config }` | Same, extracted heuristically |
683
+ | `{ message: 'x', code: 'ECONN' }` | `errorMessage`, `errorCode` |
684
+ | `null` / `undefined` | `errorMessage: 'Unknown error'` |
685
+
686
+ String-as-first-arg is always a message, never an error. If you want to log a string *as* an error, wrap it: `logger.error(new Error(str))`.
687
+
638
688
  ## Logfmt Utilities
639
689
 
640
690
  Helper functions for logfmt format:
@@ -661,7 +711,11 @@ class Logger<TContext> {
661
711
  getStore(): LoggerStore<TContext>;
662
712
 
663
713
  // Logging
664
- error(message: string, error?: Error, meta?: Meta): void;
714
+ // error() accepts an error value as the first argument — including `unknown`
715
+ // (what `catch (err)` produces under TypeScript's strict mode). Non-Error
716
+ // values are normalized via `serializeError` so logs still get a sensible
717
+ // `errorMessage` / `stack` / HTTP payload. See "Error logging" below.
718
+ error(errorOrMessage: Error | string | unknown, messageOrMeta?: string | Meta, meta?: Meta): void;
665
719
  warn(message: string, meta?: Meta): void;
666
720
  info(message: string, meta?: Meta): void;
667
721
  http(message: string, meta?: Meta): void;
package/dist/index.d.mts CHANGED
@@ -1,4 +1,3 @@
1
- import { EventEmitter } from 'events';
2
1
  import { z } from 'zod';
3
2
 
4
3
  declare const LOG_LEVELS: {
@@ -45,6 +44,47 @@ interface HttpTransportBaseConfig {
45
44
  flushInterval?: number;
46
45
  maxRetries?: number;
47
46
  retryDelay?: number;
47
+ /**
48
+ * Maximum number of messages the in-memory queue will hold before dropping.
49
+ * Provides a hard upper bound on memory usage when a transport is degraded
50
+ * or offline — without this, a failing transport + steady log volume grows
51
+ * the queue until OOM. Default: unbounded.
52
+ *
53
+ * Recommended for production: `10_000` — enough to absorb transient blips,
54
+ * small enough to avoid runaway memory.
55
+ */
56
+ maxQueueSize?: number;
57
+ /**
58
+ * Policy applied when the queue is full.
59
+ * - `'drop-oldest'` (default): prefers keeping recent events — during an
60
+ * outage old logs are usually stale.
61
+ * - `'drop-newest'`: prefers preserving historical context — useful if you
62
+ * care more about the events leading up to the outage than the noise
63
+ * happening during it.
64
+ */
65
+ dropPolicy?: 'drop-oldest' | 'drop-newest';
66
+ /**
67
+ * Called when messages are dropped due to queue overflow. Receives only the
68
+ * messages that were discarded. If not provided, drops are silent.
69
+ *
70
+ * Callback MUST NOT throw; if it does, the exception is swallowed.
71
+ */
72
+ onDrop?: (droppedMessages: unknown[]) => void;
73
+ /**
74
+ * Called when a batch fails after all retries and messages are dropped.
75
+ * If not provided, the error is logged to stderr.
76
+ *
77
+ * The callback MUST NOT throw; if it does, the failure is swallowed and a
78
+ * stderr fallback is used. This is intentional — a logger must never
79
+ * propagate its own transport errors back into the host application.
80
+ */
81
+ onError?: (error: Error, droppedMessages: unknown[]) => void;
82
+ /**
83
+ * Timeout in milliseconds for a single outbound HTTP request. Default:
84
+ * `10_000`. Applies to Discord/Telegram `fetch`. CloudWatch uses the AWS
85
+ * SDK's own timeout configuration.
86
+ */
87
+ requestTimeout?: number;
48
88
  }
49
89
  interface DiscordConfig extends HttpTransportBaseConfig {
50
90
  webhookUrl: string;
@@ -73,6 +113,20 @@ interface LogStreamTemplateConfig {
73
113
  template: string;
74
114
  }
75
115
  type LogStreamName = string | LogStreamPatternConfig | LogStreamTemplateConfig;
116
+ interface RelayConfig {
117
+ /** URL of the relay API (e.g., https://relay.example.com) */
118
+ apiUrl: string;
119
+ /** Authentication token for the relay server */
120
+ token: string;
121
+ /** Polling interval in ms (default: 30000) */
122
+ pollInterval?: number;
123
+ /** Ring buffer capacity - max logs held during reconnect (default: 1000) */
124
+ bufferSize?: number;
125
+ /** WebSocket reconnect base delay in ms (default: 1000) */
126
+ reconnectDelay?: number;
127
+ /** Max reconnect delay in ms (default: 30000) */
128
+ maxReconnectDelay?: number;
129
+ }
76
130
  interface CloudWatchConfig extends HttpTransportBaseConfig {
77
131
  logGroupName: string;
78
132
  /** Log stream name - string, pattern config, or template config. Defaults to hostname pattern */
@@ -113,6 +167,8 @@ interface LoggerConfig {
113
167
  discord?: DiscordConfig | DiscordConfig[];
114
168
  telegram?: TelegramConfig | TelegramConfig[];
115
169
  cloudwatch?: CloudWatchConfig | CloudWatchConfig[];
170
+ /** Relay transport config — streams logs to a remote server via WebSocket (runs in worker thread) */
171
+ relay?: RelayConfig;
116
172
  /** Enable caller info (file:line) in logs. Pass true for defaults or CallerConfig for options */
117
173
  caller?: boolean | CallerConfig;
118
174
  /** Hostname to include in all log entries. Defaults to os.hostname() if not specified */
@@ -140,7 +196,7 @@ declare class LoggerStore<TContext extends LoggerContext = LoggerContext> {
140
196
  declare class Logger<TContext extends LoggerContext = LoggerContext> {
141
197
  private state;
142
198
  private context;
143
- private profileTimers;
199
+ private profileTimers?;
144
200
  private constructor();
145
201
  static create<TContext extends LoggerContext = LoggerContext>(config: LoggerConfig, store?: LoggerStore<TContext>): Logger<TContext>;
146
202
  for(context: string): Logger<TContext>;
@@ -152,10 +208,29 @@ declare class Logger<TContext extends LoggerContext = LoggerContext> {
152
208
  /**
153
209
  * Gracefully shutdown the logger, flushing all pending messages.
154
210
  * Should be called before process exit to ensure no logs are lost.
211
+ *
212
+ * `timeoutMs` is forwarded to each transport's `close()` as a per-transport
213
+ * cap; it prevents a dead transport from blocking the whole shutdown.
214
+ * Default: 5000ms. Pass `Infinity` for legacy unbounded behaviour.
155
215
  */
156
- shutdown(): Promise<void>;
216
+ shutdown(timeoutMs?: number): Promise<void>;
157
217
  profile(id: string, meta?: object): void;
158
- error(errorOrMessage: Error | string, messageOrMeta?: string | Meta, meta?: Meta): void;
218
+ /**
219
+ * Log an error. Supported shapes:
220
+ * error(message: string)
221
+ * error(message: string, meta)
222
+ * error(error: Error | unknown)
223
+ * error(error: Error | unknown, message: string)
224
+ * error(error: Error | unknown, meta)
225
+ * error(error: Error | unknown, message: string, meta)
226
+ *
227
+ * The first argument is treated as an error value whenever it is not a string.
228
+ * Non-Error values (plain objects, numbers, etc. — e.g. anything caught by
229
+ * TypeScript's `catch (err)` clause, which is typed as `unknown`) are passed
230
+ * through `serializeError` so they produce a sensible `errorMessage` and, when
231
+ * possible, a `stack` / HTTP diagnostic payload.
232
+ */
233
+ error(errorOrMessage: Error | string | unknown, messageOrMeta?: string | Meta, meta?: Meta): void;
159
234
  warn(message: string, meta?: Meta): void;
160
235
  info(message: string, meta?: Meta): void;
161
236
  http(message: string, meta?: Meta): void;
@@ -185,6 +260,7 @@ interface BufferedMessage {
185
260
  context?: string;
186
261
  meta?: Record<string, unknown>;
187
262
  }
263
+ type DropPolicy = 'drop-oldest' | 'drop-newest';
188
264
  interface BufferOptions {
189
265
  batchSize: number;
190
266
  flushInterval: number;
@@ -192,6 +268,25 @@ interface BufferOptions {
192
268
  retryDelay: number;
193
269
  onFlush: (messages: BufferedMessage[]) => Promise<void>;
194
270
  onError?: (error: Error, messages: BufferedMessage[]) => void;
271
+ /**
272
+ * Maximum number of messages the queue will hold before dropping. When the
273
+ * queue is full, behaviour follows `dropPolicy`. Default: unbounded.
274
+ */
275
+ maxQueueSize?: number;
276
+ /**
277
+ * Policy applied when the queue is at `maxQueueSize` and a new message
278
+ * arrives. Default: `'drop-oldest'` — prefers keeping recent events, since
279
+ * during an outage the old ones are usually stale anyway.
280
+ */
281
+ dropPolicy?: DropPolicy;
282
+ /**
283
+ * Called when messages are dropped due to queue overflow. Receives the
284
+ * messages that were discarded (not the remaining queue). If not provided,
285
+ * the drop is silent — consumers can still track it via buffer metrics.
286
+ *
287
+ * Callback MUST NOT throw; if it does, the exception is swallowed.
288
+ */
289
+ onDrop?: (droppedMessages: BufferedMessage[]) => void;
195
290
  }
196
291
  declare class MessageBuffer {
197
292
  private options;
@@ -199,10 +294,24 @@ declare class MessageBuffer {
199
294
  private timer;
200
295
  private flushing;
201
296
  private closed;
297
+ private droppedCount;
202
298
  constructor(options: BufferOptions);
203
299
  add(message: BufferedMessage): void;
204
300
  flush(): Promise<void>;
205
- close(): Promise<void>;
301
+ /**
302
+ * Stops accepting new messages and attempts to flush what's already buffered.
303
+ * Returns when the queue is drained OR when `timeoutMs` elapses. A timeout
304
+ * is essential during graceful shutdown — without it, a dead transport would
305
+ * block process exit indefinitely, and orchestrators like Kubernetes would
306
+ * escalate to SIGKILL after their grace period.
307
+ *
308
+ * Default timeout: 5 seconds. Pass `Infinity` to preserve legacy behaviour.
309
+ */
310
+ close(timeoutMs?: number): Promise<void>;
311
+ /** Current number of messages waiting in the queue. */
312
+ get size(): number;
313
+ /** Total number of messages dropped due to queue overflow since creation. */
314
+ get droppedTotal(): number;
206
315
  private scheduleFlush;
207
316
  private clearTimer;
208
317
  private sendWithRetry;
@@ -214,12 +323,35 @@ interface BaseHttpTransportOptions {
214
323
  flushInterval?: number;
215
324
  maxRetries?: number;
216
325
  retryDelay?: number;
326
+ maxQueueSize?: number;
327
+ dropPolicy?: DropPolicy;
328
+ onDrop?: (droppedMessages: BufferedMessage[]) => void;
329
+ /**
330
+ * Called when a batch fails after all retries and messages are dropped.
331
+ * If not provided, the error is logged to stderr.
332
+ *
333
+ * The callback MUST NOT throw; if it does, the failure is swallowed and a
334
+ * stderr fallback is used. A logger must never propagate transport errors
335
+ * back into the host application.
336
+ */
337
+ onError?: (error: Error, droppedMessages: BufferedMessage[]) => void;
217
338
  }
218
- declare abstract class BaseHttpTransport extends EventEmitter {
339
+ declare abstract class BaseHttpTransport {
219
340
  protected buffer: MessageBuffer;
341
+ private onErrorCallback?;
220
342
  constructor(opts?: BaseHttpTransportOptions);
221
343
  log(info: Record<string, unknown>, callback: () => void): void;
222
- close(): Promise<void>;
344
+ /**
345
+ * Stop accepting new messages and flush what's buffered. `timeoutMs` caps
346
+ * how long the flush may take; after that the remaining queue is discarded
347
+ * so shutdown can complete. Default 5000ms.
348
+ */
349
+ close(timeoutMs?: number): Promise<void>;
350
+ /** Current buffered message count and total drops since creation. */
351
+ getMetrics(): {
352
+ queueSize: number;
353
+ droppedTotal: number;
354
+ };
223
355
  protected transformMessage(info: Record<string, unknown>): BufferedMessage;
224
356
  protected handleError(error: Error, messages: BufferedMessage[]): void;
225
357
  protected abstract sendBatch(messages: BufferedMessage[]): Promise<void>;
@@ -231,6 +363,7 @@ declare function matchesContext(storeContext: LoggerContext | undefined, loggerC
231
363
 
232
364
  declare class DiscordTransport extends BaseHttpTransport {
233
365
  private config;
366
+ private requestTimeout;
234
367
  constructor(config: DiscordConfig);
235
368
  protected sendBatch(messages: BufferedMessage[]): Promise<void>;
236
369
  private sendEmbedBatch;
@@ -246,6 +379,7 @@ declare class DiscordTransport extends BaseHttpTransport {
246
379
  declare class TelegramTransport extends BaseHttpTransport {
247
380
  private config;
248
381
  private apiUrl;
382
+ private requestTimeout;
249
383
  constructor(config: TelegramConfig);
250
384
  protected sendBatch(messages: BufferedMessage[]): Promise<void>;
251
385
  private formatBatchMessage;
@@ -412,6 +546,7 @@ declare const LoggerConfigSchema: z.ZodObject<{
412
546
  discord: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<DiscordConfig, unknown, z.core.$ZodTypeInternals<DiscordConfig, unknown>>, z.ZodArray<z.ZodType<DiscordConfig, unknown, z.core.$ZodTypeInternals<DiscordConfig, unknown>>>]>>;
413
547
  telegram: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<TelegramConfig, unknown, z.core.$ZodTypeInternals<TelegramConfig, unknown>>, z.ZodArray<z.ZodType<TelegramConfig, unknown, z.core.$ZodTypeInternals<TelegramConfig, unknown>>>]>>;
414
548
  cloudwatch: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<CloudWatchConfig, unknown, z.core.$ZodTypeInternals<CloudWatchConfig, unknown>>, z.ZodArray<z.ZodType<CloudWatchConfig, unknown, z.core.$ZodTypeInternals<CloudWatchConfig, unknown>>>]>>;
549
+ relay: z.ZodOptional<z.ZodType<RelayConfig, unknown, z.core.$ZodTypeInternals<RelayConfig, unknown>>>;
415
550
  caller: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodType<CallerConfig, unknown, z.core.$ZodTypeInternals<CallerConfig, unknown>>]>>;
416
551
  hostname: z.ZodOptional<z.ZodString>;
417
552
  autoShutdown: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodType<AutoShutdownConfig, unknown, z.core.$ZodTypeInternals<AutoShutdownConfig, unknown>>]>>;
@@ -424,6 +559,7 @@ declare function safeValidateConfig(config: unknown): z.ZodSafeParseResult<{
424
559
  discord?: DiscordConfig | DiscordConfig[] | undefined;
425
560
  telegram?: TelegramConfig | TelegramConfig[] | undefined;
426
561
  cloudwatch?: CloudWatchConfig | CloudWatchConfig[] | undefined;
562
+ relay?: RelayConfig | undefined;
427
563
  caller?: boolean | CallerConfig | undefined;
428
564
  hostname?: string | undefined;
429
565
  autoShutdown?: boolean | AutoShutdownConfig | undefined;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { EventEmitter } from 'events';
2
1
  import { z } from 'zod';
3
2
 
4
3
  declare const LOG_LEVELS: {
@@ -45,6 +44,47 @@ interface HttpTransportBaseConfig {
45
44
  flushInterval?: number;
46
45
  maxRetries?: number;
47
46
  retryDelay?: number;
47
+ /**
48
+ * Maximum number of messages the in-memory queue will hold before dropping.
49
+ * Provides a hard upper bound on memory usage when a transport is degraded
50
+ * or offline — without this, a failing transport + steady log volume grows
51
+ * the queue until OOM. Default: unbounded.
52
+ *
53
+ * Recommended for production: `10_000` — enough to absorb transient blips,
54
+ * small enough to avoid runaway memory.
55
+ */
56
+ maxQueueSize?: number;
57
+ /**
58
+ * Policy applied when the queue is full.
59
+ * - `'drop-oldest'` (default): prefers keeping recent events — during an
60
+ * outage old logs are usually stale.
61
+ * - `'drop-newest'`: prefers preserving historical context — useful if you
62
+ * care more about the events leading up to the outage than the noise
63
+ * happening during it.
64
+ */
65
+ dropPolicy?: 'drop-oldest' | 'drop-newest';
66
+ /**
67
+ * Called when messages are dropped due to queue overflow. Receives only the
68
+ * messages that were discarded. If not provided, drops are silent.
69
+ *
70
+ * Callback MUST NOT throw; if it does, the exception is swallowed.
71
+ */
72
+ onDrop?: (droppedMessages: unknown[]) => void;
73
+ /**
74
+ * Called when a batch fails after all retries and messages are dropped.
75
+ * If not provided, the error is logged to stderr.
76
+ *
77
+ * The callback MUST NOT throw; if it does, the failure is swallowed and a
78
+ * stderr fallback is used. This is intentional — a logger must never
79
+ * propagate its own transport errors back into the host application.
80
+ */
81
+ onError?: (error: Error, droppedMessages: unknown[]) => void;
82
+ /**
83
+ * Timeout in milliseconds for a single outbound HTTP request. Default:
84
+ * `10_000`. Applies to Discord/Telegram `fetch`. CloudWatch uses the AWS
85
+ * SDK's own timeout configuration.
86
+ */
87
+ requestTimeout?: number;
48
88
  }
49
89
  interface DiscordConfig extends HttpTransportBaseConfig {
50
90
  webhookUrl: string;
@@ -73,6 +113,20 @@ interface LogStreamTemplateConfig {
73
113
  template: string;
74
114
  }
75
115
  type LogStreamName = string | LogStreamPatternConfig | LogStreamTemplateConfig;
116
+ interface RelayConfig {
117
+ /** URL of the relay API (e.g., https://relay.example.com) */
118
+ apiUrl: string;
119
+ /** Authentication token for the relay server */
120
+ token: string;
121
+ /** Polling interval in ms (default: 30000) */
122
+ pollInterval?: number;
123
+ /** Ring buffer capacity - max logs held during reconnect (default: 1000) */
124
+ bufferSize?: number;
125
+ /** WebSocket reconnect base delay in ms (default: 1000) */
126
+ reconnectDelay?: number;
127
+ /** Max reconnect delay in ms (default: 30000) */
128
+ maxReconnectDelay?: number;
129
+ }
76
130
  interface CloudWatchConfig extends HttpTransportBaseConfig {
77
131
  logGroupName: string;
78
132
  /** Log stream name - string, pattern config, or template config. Defaults to hostname pattern */
@@ -113,6 +167,8 @@ interface LoggerConfig {
113
167
  discord?: DiscordConfig | DiscordConfig[];
114
168
  telegram?: TelegramConfig | TelegramConfig[];
115
169
  cloudwatch?: CloudWatchConfig | CloudWatchConfig[];
170
+ /** Relay transport config — streams logs to a remote server via WebSocket (runs in worker thread) */
171
+ relay?: RelayConfig;
116
172
  /** Enable caller info (file:line) in logs. Pass true for defaults or CallerConfig for options */
117
173
  caller?: boolean | CallerConfig;
118
174
  /** Hostname to include in all log entries. Defaults to os.hostname() if not specified */
@@ -140,7 +196,7 @@ declare class LoggerStore<TContext extends LoggerContext = LoggerContext> {
140
196
  declare class Logger<TContext extends LoggerContext = LoggerContext> {
141
197
  private state;
142
198
  private context;
143
- private profileTimers;
199
+ private profileTimers?;
144
200
  private constructor();
145
201
  static create<TContext extends LoggerContext = LoggerContext>(config: LoggerConfig, store?: LoggerStore<TContext>): Logger<TContext>;
146
202
  for(context: string): Logger<TContext>;
@@ -152,10 +208,29 @@ declare class Logger<TContext extends LoggerContext = LoggerContext> {
152
208
  /**
153
209
  * Gracefully shutdown the logger, flushing all pending messages.
154
210
  * Should be called before process exit to ensure no logs are lost.
211
+ *
212
+ * `timeoutMs` is forwarded to each transport's `close()` as a per-transport
213
+ * cap; it prevents a dead transport from blocking the whole shutdown.
214
+ * Default: 5000ms. Pass `Infinity` for legacy unbounded behaviour.
155
215
  */
156
- shutdown(): Promise<void>;
216
+ shutdown(timeoutMs?: number): Promise<void>;
157
217
  profile(id: string, meta?: object): void;
158
- error(errorOrMessage: Error | string, messageOrMeta?: string | Meta, meta?: Meta): void;
218
+ /**
219
+ * Log an error. Supported shapes:
220
+ * error(message: string)
221
+ * error(message: string, meta)
222
+ * error(error: Error | unknown)
223
+ * error(error: Error | unknown, message: string)
224
+ * error(error: Error | unknown, meta)
225
+ * error(error: Error | unknown, message: string, meta)
226
+ *
227
+ * The first argument is treated as an error value whenever it is not a string.
228
+ * Non-Error values (plain objects, numbers, etc. — e.g. anything caught by
229
+ * TypeScript's `catch (err)` clause, which is typed as `unknown`) are passed
230
+ * through `serializeError` so they produce a sensible `errorMessage` and, when
231
+ * possible, a `stack` / HTTP diagnostic payload.
232
+ */
233
+ error(errorOrMessage: Error | string | unknown, messageOrMeta?: string | Meta, meta?: Meta): void;
159
234
  warn(message: string, meta?: Meta): void;
160
235
  info(message: string, meta?: Meta): void;
161
236
  http(message: string, meta?: Meta): void;
@@ -185,6 +260,7 @@ interface BufferedMessage {
185
260
  context?: string;
186
261
  meta?: Record<string, unknown>;
187
262
  }
263
+ type DropPolicy = 'drop-oldest' | 'drop-newest';
188
264
  interface BufferOptions {
189
265
  batchSize: number;
190
266
  flushInterval: number;
@@ -192,6 +268,25 @@ interface BufferOptions {
192
268
  retryDelay: number;
193
269
  onFlush: (messages: BufferedMessage[]) => Promise<void>;
194
270
  onError?: (error: Error, messages: BufferedMessage[]) => void;
271
+ /**
272
+ * Maximum number of messages the queue will hold before dropping. When the
273
+ * queue is full, behaviour follows `dropPolicy`. Default: unbounded.
274
+ */
275
+ maxQueueSize?: number;
276
+ /**
277
+ * Policy applied when the queue is at `maxQueueSize` and a new message
278
+ * arrives. Default: `'drop-oldest'` — prefers keeping recent events, since
279
+ * during an outage the old ones are usually stale anyway.
280
+ */
281
+ dropPolicy?: DropPolicy;
282
+ /**
283
+ * Called when messages are dropped due to queue overflow. Receives the
284
+ * messages that were discarded (not the remaining queue). If not provided,
285
+ * the drop is silent — consumers can still track it via buffer metrics.
286
+ *
287
+ * Callback MUST NOT throw; if it does, the exception is swallowed.
288
+ */
289
+ onDrop?: (droppedMessages: BufferedMessage[]) => void;
195
290
  }
196
291
  declare class MessageBuffer {
197
292
  private options;
@@ -199,10 +294,24 @@ declare class MessageBuffer {
199
294
  private timer;
200
295
  private flushing;
201
296
  private closed;
297
+ private droppedCount;
202
298
  constructor(options: BufferOptions);
203
299
  add(message: BufferedMessage): void;
204
300
  flush(): Promise<void>;
205
- close(): Promise<void>;
301
+ /**
302
+ * Stops accepting new messages and attempts to flush what's already buffered.
303
+ * Returns when the queue is drained OR when `timeoutMs` elapses. A timeout
304
+ * is essential during graceful shutdown — without it, a dead transport would
305
+ * block process exit indefinitely, and orchestrators like Kubernetes would
306
+ * escalate to SIGKILL after their grace period.
307
+ *
308
+ * Default timeout: 5 seconds. Pass `Infinity` to preserve legacy behaviour.
309
+ */
310
+ close(timeoutMs?: number): Promise<void>;
311
+ /** Current number of messages waiting in the queue. */
312
+ get size(): number;
313
+ /** Total number of messages dropped due to queue overflow since creation. */
314
+ get droppedTotal(): number;
206
315
  private scheduleFlush;
207
316
  private clearTimer;
208
317
  private sendWithRetry;
@@ -214,12 +323,35 @@ interface BaseHttpTransportOptions {
214
323
  flushInterval?: number;
215
324
  maxRetries?: number;
216
325
  retryDelay?: number;
326
+ maxQueueSize?: number;
327
+ dropPolicy?: DropPolicy;
328
+ onDrop?: (droppedMessages: BufferedMessage[]) => void;
329
+ /**
330
+ * Called when a batch fails after all retries and messages are dropped.
331
+ * If not provided, the error is logged to stderr.
332
+ *
333
+ * The callback MUST NOT throw; if it does, the failure is swallowed and a
334
+ * stderr fallback is used. A logger must never propagate transport errors
335
+ * back into the host application.
336
+ */
337
+ onError?: (error: Error, droppedMessages: BufferedMessage[]) => void;
217
338
  }
218
- declare abstract class BaseHttpTransport extends EventEmitter {
339
+ declare abstract class BaseHttpTransport {
219
340
  protected buffer: MessageBuffer;
341
+ private onErrorCallback?;
220
342
  constructor(opts?: BaseHttpTransportOptions);
221
343
  log(info: Record<string, unknown>, callback: () => void): void;
222
- close(): Promise<void>;
344
+ /**
345
+ * Stop accepting new messages and flush what's buffered. `timeoutMs` caps
346
+ * how long the flush may take; after that the remaining queue is discarded
347
+ * so shutdown can complete. Default 5000ms.
348
+ */
349
+ close(timeoutMs?: number): Promise<void>;
350
+ /** Current buffered message count and total drops since creation. */
351
+ getMetrics(): {
352
+ queueSize: number;
353
+ droppedTotal: number;
354
+ };
223
355
  protected transformMessage(info: Record<string, unknown>): BufferedMessage;
224
356
  protected handleError(error: Error, messages: BufferedMessage[]): void;
225
357
  protected abstract sendBatch(messages: BufferedMessage[]): Promise<void>;
@@ -231,6 +363,7 @@ declare function matchesContext(storeContext: LoggerContext | undefined, loggerC
231
363
 
232
364
  declare class DiscordTransport extends BaseHttpTransport {
233
365
  private config;
366
+ private requestTimeout;
234
367
  constructor(config: DiscordConfig);
235
368
  protected sendBatch(messages: BufferedMessage[]): Promise<void>;
236
369
  private sendEmbedBatch;
@@ -246,6 +379,7 @@ declare class DiscordTransport extends BaseHttpTransport {
246
379
  declare class TelegramTransport extends BaseHttpTransport {
247
380
  private config;
248
381
  private apiUrl;
382
+ private requestTimeout;
249
383
  constructor(config: TelegramConfig);
250
384
  protected sendBatch(messages: BufferedMessage[]): Promise<void>;
251
385
  private formatBatchMessage;
@@ -412,6 +546,7 @@ declare const LoggerConfigSchema: z.ZodObject<{
412
546
  discord: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<DiscordConfig, unknown, z.core.$ZodTypeInternals<DiscordConfig, unknown>>, z.ZodArray<z.ZodType<DiscordConfig, unknown, z.core.$ZodTypeInternals<DiscordConfig, unknown>>>]>>;
413
547
  telegram: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<TelegramConfig, unknown, z.core.$ZodTypeInternals<TelegramConfig, unknown>>, z.ZodArray<z.ZodType<TelegramConfig, unknown, z.core.$ZodTypeInternals<TelegramConfig, unknown>>>]>>;
414
548
  cloudwatch: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<CloudWatchConfig, unknown, z.core.$ZodTypeInternals<CloudWatchConfig, unknown>>, z.ZodArray<z.ZodType<CloudWatchConfig, unknown, z.core.$ZodTypeInternals<CloudWatchConfig, unknown>>>]>>;
549
+ relay: z.ZodOptional<z.ZodType<RelayConfig, unknown, z.core.$ZodTypeInternals<RelayConfig, unknown>>>;
415
550
  caller: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodType<CallerConfig, unknown, z.core.$ZodTypeInternals<CallerConfig, unknown>>]>>;
416
551
  hostname: z.ZodOptional<z.ZodString>;
417
552
  autoShutdown: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodType<AutoShutdownConfig, unknown, z.core.$ZodTypeInternals<AutoShutdownConfig, unknown>>]>>;
@@ -424,6 +559,7 @@ declare function safeValidateConfig(config: unknown): z.ZodSafeParseResult<{
424
559
  discord?: DiscordConfig | DiscordConfig[] | undefined;
425
560
  telegram?: TelegramConfig | TelegramConfig[] | undefined;
426
561
  cloudwatch?: CloudWatchConfig | CloudWatchConfig[] | undefined;
562
+ relay?: RelayConfig | undefined;
427
563
  caller?: boolean | CallerConfig | undefined;
428
564
  hostname?: string | undefined;
429
565
  autoShutdown?: boolean | AutoShutdownConfig | undefined;