@rawnodes/logger 2.7.0 → 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/dist/index.js CHANGED
@@ -6,7 +6,6 @@ var async_hooks = require('async_hooks');
6
6
  var stream = require('stream');
7
7
  var promises = require('fs/promises');
8
8
  var rotatingFileStream = require('rotating-file-stream');
9
- var events = require('events');
10
9
  var clientCloudwatchLogs = require('@aws-sdk/client-cloudwatch-logs');
11
10
  var crypto = require('crypto');
12
11
  var zod = require('zod');
@@ -56,8 +55,24 @@ var MessageBuffer = class {
56
55
  timer = null;
57
56
  flushing = false;
58
57
  closed = false;
58
+ droppedCount = 0;
59
59
  add(message) {
60
60
  if (this.closed) return;
61
+ const limit = this.options.maxQueueSize;
62
+ if (limit !== void 0 && this.queue.length >= limit) {
63
+ const policy = this.options.dropPolicy ?? "drop-oldest";
64
+ const dropped = policy === "drop-oldest" ? [this.queue.shift()] : [message];
65
+ this.droppedCount += 1;
66
+ if (this.options.onDrop) {
67
+ try {
68
+ this.options.onDrop(dropped);
69
+ } catch {
70
+ }
71
+ }
72
+ if (policy === "drop-newest") {
73
+ return;
74
+ }
75
+ }
61
76
  this.queue.push(message);
62
77
  if (this.queue.length >= this.options.batchSize) {
63
78
  void this.flush();
@@ -81,13 +96,34 @@ var MessageBuffer = class {
81
96
  }
82
97
  }
83
98
  }
84
- async close() {
99
+ /**
100
+ * Stops accepting new messages and attempts to flush what's already buffered.
101
+ * Returns when the queue is drained OR when `timeoutMs` elapses. A timeout
102
+ * is essential during graceful shutdown — without it, a dead transport would
103
+ * block process exit indefinitely, and orchestrators like Kubernetes would
104
+ * escalate to SIGKILL after their grace period.
105
+ *
106
+ * Default timeout: 5 seconds. Pass `Infinity` to preserve legacy behaviour.
107
+ */
108
+ async close(timeoutMs = 5e3) {
85
109
  this.closed = true;
86
110
  this.clearTimer();
111
+ const deadline = timeoutMs === Infinity ? Infinity : Date.now() + timeoutMs;
87
112
  while (this.queue.length > 0) {
113
+ if (Date.now() >= deadline) {
114
+ return;
115
+ }
88
116
  await this.flush();
89
117
  }
90
118
  }
119
+ /** Current number of messages waiting in the queue. */
120
+ get size() {
121
+ return this.queue.length;
122
+ }
123
+ /** Total number of messages dropped due to queue overflow since creation. */
124
+ get droppedTotal() {
125
+ return this.droppedCount;
126
+ }
91
127
  scheduleFlush() {
92
128
  if (this.timer || this.closed) return;
93
129
  this.timer = setTimeout(() => {
@@ -128,15 +164,19 @@ var DEFAULT_OPTIONS = {
128
164
  maxRetries: 3,
129
165
  retryDelay: 1e3
130
166
  };
131
- var BaseHttpTransport = class extends events.EventEmitter {
167
+ var BaseHttpTransport = class {
132
168
  buffer;
169
+ onErrorCallback;
133
170
  constructor(opts = {}) {
134
- super();
171
+ this.onErrorCallback = opts.onError;
135
172
  this.buffer = new MessageBuffer({
136
173
  batchSize: opts.batchSize ?? DEFAULT_OPTIONS.batchSize,
137
174
  flushInterval: opts.flushInterval ?? DEFAULT_OPTIONS.flushInterval,
138
175
  maxRetries: opts.maxRetries ?? DEFAULT_OPTIONS.maxRetries,
139
176
  retryDelay: opts.retryDelay ?? DEFAULT_OPTIONS.retryDelay,
177
+ maxQueueSize: opts.maxQueueSize,
178
+ dropPolicy: opts.dropPolicy,
179
+ onDrop: opts.onDrop,
140
180
  onFlush: this.sendBatch.bind(this),
141
181
  onError: this.handleError.bind(this)
142
182
  });
@@ -146,8 +186,20 @@ var BaseHttpTransport = class extends events.EventEmitter {
146
186
  this.buffer.add(message);
147
187
  callback();
148
188
  }
149
- close() {
150
- return this.buffer.close();
189
+ /**
190
+ * Stop accepting new messages and flush what's buffered. `timeoutMs` caps
191
+ * how long the flush may take; after that the remaining queue is discarded
192
+ * so shutdown can complete. Default 5000ms.
193
+ */
194
+ close(timeoutMs) {
195
+ return this.buffer.close(timeoutMs);
196
+ }
197
+ /** Current buffered message count and total drops since creation. */
198
+ getMetrics() {
199
+ return {
200
+ queueSize: this.buffer.size,
201
+ droppedTotal: this.buffer.droppedTotal
202
+ };
151
203
  }
152
204
  transformMessage(info) {
153
205
  const { level, message, timestamp, context, ...meta } = info;
@@ -160,11 +212,21 @@ var BaseHttpTransport = class extends events.EventEmitter {
160
212
  };
161
213
  }
162
214
  handleError(error, messages) {
215
+ if (this.onErrorCallback) {
216
+ try {
217
+ this.onErrorCallback(error, messages);
218
+ return;
219
+ } catch (callbackError) {
220
+ console.error(
221
+ `[${this.constructor.name}] onError callback threw:`,
222
+ callbackError instanceof Error ? callbackError.message : callbackError
223
+ );
224
+ }
225
+ }
163
226
  console.error(
164
227
  `[${this.constructor.name}] Failed to send ${messages.length} messages:`,
165
228
  error.message
166
229
  );
167
- this.emit("error", error);
168
230
  }
169
231
  };
170
232
 
@@ -189,16 +251,23 @@ var LEVEL_EMOJI = {
189
251
  debug: "\u26AA",
190
252
  silly: "\u26AB"
191
253
  };
254
+ var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
192
255
  var DiscordTransport = class extends BaseHttpTransport {
193
256
  config;
257
+ requestTimeout;
194
258
  constructor(config) {
195
259
  super({
196
260
  batchSize: config.batchSize ?? 10,
197
261
  flushInterval: config.flushInterval ?? 2e3,
198
262
  maxRetries: config.maxRetries,
199
- retryDelay: config.retryDelay
263
+ retryDelay: config.retryDelay,
264
+ maxQueueSize: config.maxQueueSize,
265
+ dropPolicy: config.dropPolicy,
266
+ onDrop: config.onDrop,
267
+ onError: config.onError
200
268
  });
201
269
  this.config = config;
270
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
202
271
  }
203
272
  async sendBatch(messages) {
204
273
  if (this.config.format === "markdown") {
@@ -302,7 +371,8 @@ ${msg.message}`;
302
371
  const response = await fetch(this.config.webhookUrl, {
303
372
  method: "POST",
304
373
  headers: { "Content-Type": "application/json" },
305
- body: JSON.stringify(payload)
374
+ body: JSON.stringify(payload),
375
+ signal: AbortSignal.timeout(this.requestTimeout)
306
376
  });
307
377
  if (!response.ok) {
308
378
  const text = await response.text();
@@ -329,18 +399,25 @@ var LEVEL_EMOJI2 = {
329
399
  debug: "\u26AA",
330
400
  silly: "\u26AB"
331
401
  };
402
+ var DEFAULT_REQUEST_TIMEOUT_MS2 = 1e4;
332
403
  var TelegramTransport = class extends BaseHttpTransport {
333
404
  config;
334
405
  apiUrl;
406
+ requestTimeout;
335
407
  constructor(config) {
336
408
  super({
337
409
  batchSize: config.batchSize ?? 20,
338
410
  flushInterval: config.flushInterval ?? 1e3,
339
411
  maxRetries: config.maxRetries,
340
- retryDelay: config.retryDelay
412
+ retryDelay: config.retryDelay,
413
+ maxQueueSize: config.maxQueueSize,
414
+ dropPolicy: config.dropPolicy,
415
+ onDrop: config.onDrop,
416
+ onError: config.onError
341
417
  });
342
418
  this.config = config;
343
419
  this.apiUrl = `https://api.telegram.org/bot${config.botToken}`;
420
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS2;
344
421
  }
345
422
  async sendBatch(messages) {
346
423
  const text = this.formatBatchMessage(messages);
@@ -401,7 +478,8 @@ var TelegramTransport = class extends BaseHttpTransport {
401
478
  const response = await fetch(`${this.apiUrl}/sendMessage`, {
402
479
  method: "POST",
403
480
  headers: { "Content-Type": "application/json" },
404
- body: JSON.stringify(body)
481
+ body: JSON.stringify(body),
482
+ signal: AbortSignal.timeout(this.requestTimeout)
405
483
  });
406
484
  if (!response.ok) {
407
485
  const result = await response.json();
@@ -470,7 +548,11 @@ var CloudWatchTransport = class extends BaseHttpTransport {
470
548
  batchSize: config.batchSize ?? 100,
471
549
  flushInterval: config.flushInterval ?? 1e3,
472
550
  maxRetries: config.maxRetries,
473
- retryDelay: config.retryDelay
551
+ retryDelay: config.retryDelay,
552
+ maxQueueSize: config.maxQueueSize,
553
+ dropPolicy: config.dropPolicy,
554
+ onDrop: config.onDrop,
555
+ onError: config.onError
474
556
  });
475
557
  this.config = config;
476
558
  this.resolvedLogStreamName = resolveLogStreamName(config.logStreamName, configHostname);
@@ -522,7 +604,10 @@ var CloudWatchTransport = class extends BaseHttpTransport {
522
604
  async ensureInitialized() {
523
605
  if (this.initialized) return;
524
606
  if (!this.initPromise) {
525
- this.initPromise = this.initialize();
607
+ this.initPromise = this.initialize().catch((err) => {
608
+ this.initPromise = null;
609
+ throw err;
610
+ });
526
611
  }
527
612
  await this.initPromise;
528
613
  }
@@ -598,15 +683,34 @@ function getLevelName(levelNum) {
598
683
  if (levelNum >= 20) return "debug";
599
684
  return "silly";
600
685
  }
601
- function shouldPassTransport(log, level, rules, store) {
686
+ function shouldPassTransport(log, level, rules, state) {
602
687
  const logLevel = getLevelName(log.level);
688
+ const embeddedOverride = log.__or;
689
+ if (typeof embeddedOverride === "string") {
690
+ if (embeddedOverride === "off") return false;
691
+ const overrideLevel = embeddedOverride;
692
+ return LOG_LEVELS[logLevel] <= LOG_LEVELS[overrideLevel];
693
+ }
603
694
  const context = log.context;
604
- const storeContext = store.getStore();
605
- const matchingRule = rules?.find((rule) => matchesContext(storeContext, context, rule.match));
606
- const effectiveLevel = matchingRule?.level ?? level ?? "silly";
695
+ if (rules && rules.length > 0) {
696
+ const storeContext = state.store.getStore();
697
+ const logMeta = {};
698
+ for (const [key, value] of Object.entries(log)) {
699
+ if (!RESERVED_LOG_FIELDS.has(key)) {
700
+ logMeta[key] = value;
701
+ }
702
+ }
703
+ const matchingRule = rules.find((rule) => matchesContext(storeContext, context, rule.match, logMeta));
704
+ if (matchingRule) {
705
+ if (matchingRule.level === "off") return false;
706
+ return LOG_LEVELS[logLevel] <= LOG_LEVELS[matchingRule.level];
707
+ }
708
+ }
709
+ const effectiveLevel = level ?? "silly";
607
710
  if (effectiveLevel === "off") return false;
608
711
  return LOG_LEVELS[logLevel] <= LOG_LEVELS[effectiveLevel];
609
712
  }
713
+ var RESERVED_LOG_FIELDS = /* @__PURE__ */ new Set(["level", "time", "msg", "context", "__or"]);
610
714
  function formatLog(log, format, store) {
611
715
  const levelName = getLevelName(log.level);
612
716
  const timestamp = new Date(log.time).toISOString();
@@ -615,7 +719,7 @@ function formatLog(log, format, store) {
615
719
  const storeContext = store.getStore();
616
720
  const meta = {};
617
721
  for (const [key, value] of Object.entries(log)) {
618
- if (!["level", "time", "msg", "context"].includes(key)) {
722
+ if (!RESERVED_LOG_FIELDS.has(key)) {
619
723
  meta[key] = value;
620
724
  }
621
725
  }
@@ -664,7 +768,7 @@ function formatLog(log, format, store) {
664
768
  return `[${timestamp}] ${levelName}: ${message}
665
769
  `;
666
770
  }
667
- function createFormattedFilterStream(format, level, rules, store, destination) {
771
+ function createFormattedFilterStream(format, level, rules, state, destination) {
668
772
  return new stream.Transform({
669
773
  transform(chunk, _encoding, callback) {
670
774
  const line = chunk.toString().trim();
@@ -677,24 +781,24 @@ function createFormattedFilterStream(format, level, rules, store, destination) {
677
781
  callback();
678
782
  return;
679
783
  }
680
- if (!shouldPassTransport(log, level, rules, store)) {
784
+ if (!shouldPassTransport(log, level, rules, state)) {
681
785
  callback();
682
786
  return;
683
787
  }
684
- const formatted = formatLog(log, format, store);
788
+ const formatted = formatLog(log, format, state.store);
685
789
  destination.write(formatted);
686
790
  callback();
687
791
  }
688
792
  });
689
793
  }
690
- function createStreams(config, store) {
794
+ function createStreams(config, state) {
691
795
  const streams = [];
692
796
  const transports = [];
693
797
  const consoleStream = createFormattedFilterStream(
694
798
  config.console.format,
695
799
  config.console.level,
696
800
  config.console.rules,
697
- store,
801
+ state,
698
802
  process.stdout
699
803
  );
700
804
  streams.push({
@@ -727,7 +831,7 @@ function createStreams(config, store) {
727
831
  fileConfig.format,
728
832
  fileConfig.level,
729
833
  fileConfig.rules,
730
- store,
834
+ state,
731
835
  rotatingStream
732
836
  );
733
837
  streams.push({
@@ -738,7 +842,7 @@ function createStreams(config, store) {
738
842
  for (const discordConfig of toArray(config.discord)) {
739
843
  const transport = new DiscordTransport(discordConfig);
740
844
  transports.push(transport);
741
- const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, store);
845
+ const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, state);
742
846
  streams.push({
743
847
  level: "trace",
744
848
  stream: discordStream
@@ -747,7 +851,7 @@ function createStreams(config, store) {
747
851
  for (const telegramConfig of toArray(config.telegram)) {
748
852
  const transport = new TelegramTransport(telegramConfig);
749
853
  transports.push(transport);
750
- const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, store);
854
+ const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, state);
751
855
  streams.push({
752
856
  level: "trace",
753
857
  stream: telegramStream
@@ -756,12 +860,22 @@ function createStreams(config, store) {
756
860
  for (const cloudwatchConfig of toArray(config.cloudwatch)) {
757
861
  const transport = new CloudWatchTransport(cloudwatchConfig, config.hostname);
758
862
  transports.push(transport);
759
- const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, store);
863
+ const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, state);
760
864
  streams.push({
761
865
  level: "trace",
762
866
  stream: cwStream
763
867
  });
764
868
  }
869
+ if (config.relay) {
870
+ const relayStream = pino__default.default.transport({
871
+ target: "@rawnodes/logger/relay",
872
+ options: config.relay
873
+ });
874
+ streams.push({
875
+ level: "trace",
876
+ stream: relayStream
877
+ });
878
+ }
765
879
  return {
766
880
  destination: pino__default.default.multistream(streams),
767
881
  transports
@@ -771,7 +885,7 @@ function toArray(value) {
771
885
  if (!value) return [];
772
886
  return Array.isArray(value) ? value : [value];
773
887
  }
774
- function createHttpTransportStream(transport, level, rules, store) {
888
+ function createHttpTransportStream(transport, level, rules, state) {
775
889
  return new stream.Writable({
776
890
  write(chunk, _encoding, callback) {
777
891
  const line = chunk.toString().trim();
@@ -784,15 +898,15 @@ function createHttpTransportStream(transport, level, rules, store) {
784
898
  callback();
785
899
  return;
786
900
  }
787
- if (!shouldPassTransport(log, level, rules, store)) {
901
+ if (!shouldPassTransport(log, level, rules, state)) {
788
902
  callback();
789
903
  return;
790
904
  }
791
905
  const levelName = getLevelName(log.level);
792
- const storeContext = store.getStore();
906
+ const storeContext = state.store.getStore();
793
907
  const meta = {};
794
908
  for (const [key, value] of Object.entries(log)) {
795
- if (!["level", "time", "msg", "context"].includes(key)) {
909
+ if (!RESERVED_LOG_FIELDS.has(key)) {
796
910
  meta[key] = value;
797
911
  }
798
912
  }
@@ -841,45 +955,53 @@ function buildIndexes(overrides) {
841
955
  }
842
956
  return { contextIndex, complexRules };
843
957
  }
958
+ function canonicalMatchKey(match) {
959
+ const keys = Object.keys(match).sort();
960
+ const sorted = {};
961
+ for (const key of keys) {
962
+ sorted[key] = match[key];
963
+ }
964
+ return JSON.stringify(sorted);
965
+ }
844
966
  function createState(config, store) {
845
967
  const { defaultLevel, rules } = parseLevelConfig(config.level);
846
968
  const loggerStore = store ?? new LoggerStore();
847
969
  const levelOverrides = /* @__PURE__ */ new Map();
848
970
  for (const rule of rules) {
849
- const key = JSON.stringify(rule.match);
971
+ const key = canonicalMatchKey(rule.match);
850
972
  levelOverrides.set(key, rule);
851
973
  }
852
974
  const { contextIndex, complexRules } = buildIndexes(levelOverrides);
853
- const { destination, transports } = createStreams(config, loggerStore);
975
+ const state = {
976
+ pino: null,
977
+ store: loggerStore,
978
+ defaultLevel,
979
+ levelOverrides,
980
+ contextIndex,
981
+ complexRules,
982
+ callerConfig: void 0,
983
+ transports: []
984
+ };
985
+ const { destination, transports } = createStreams(config, state);
986
+ state.transports = transports;
854
987
  const options = {
855
988
  level: "trace",
856
989
  // Accept all, we filter in shouldLog()
857
990
  customLevels: CUSTOM_LEVELS,
858
991
  base: { hostname: config.hostname ?? os.hostname() }
859
992
  };
860
- const pinoLogger = pino__default.default(options, destination);
861
- let callerConfig;
993
+ state.pino = pino__default.default(options, destination);
862
994
  if (config.caller === true) {
863
- callerConfig = {};
995
+ state.callerConfig = {};
864
996
  } else if (config.caller && typeof config.caller === "object") {
865
- callerConfig = config.caller;
997
+ state.callerConfig = config.caller;
866
998
  }
867
- return {
868
- pino: pinoLogger,
869
- store: loggerStore,
870
- defaultLevel,
871
- levelOverrides,
872
- contextIndex,
873
- complexRules,
874
- callerConfig,
875
- transports
876
- };
999
+ return state;
877
1000
  }
878
1001
  function rebuildIndexes(state) {
879
1002
  const { contextIndex, complexRules } = buildIndexes(state.levelOverrides);
880
1003
  state.contextIndex = contextIndex;
881
- state.complexRules.length = 0;
882
- state.complexRules.push(...complexRules);
1004
+ state.complexRules = complexRules;
883
1005
  }
884
1006
  function shouldLog(state, level, context) {
885
1007
  const effectiveLevel = getEffectiveLevel(state, context);
@@ -899,10 +1021,24 @@ function getEffectiveLevel(state, loggerContext) {
899
1021
  }
900
1022
  return state.defaultLevel;
901
1023
  }
902
- function matchesContext(storeContext, loggerContext, match) {
903
- const combined = { ...storeContext, context: loggerContext };
1024
+ function matchesContext(storeContext, loggerContext, match, logMeta) {
1025
+ const combined = { ...logMeta, ...storeContext, context: loggerContext };
904
1026
  return Object.entries(match).every(([key, value]) => combined[key] === value);
905
1027
  }
1028
+ function getGlobalOverrideLevel2(state, loggerContext, logMeta) {
1029
+ if (loggerContext) {
1030
+ const indexed = state.contextIndex.get(loggerContext);
1031
+ if (indexed && !indexed.readonly) return indexed.level;
1032
+ }
1033
+ const storeContext = state.store.getStore();
1034
+ for (const override of state.complexRules) {
1035
+ if (override.readonly) continue;
1036
+ if (matchesContext(storeContext, loggerContext, override.match, logMeta)) {
1037
+ return override.level;
1038
+ }
1039
+ }
1040
+ return void 0;
1041
+ }
906
1042
  var LogLevelSchema = zod.z.enum([
907
1043
  "off",
908
1044
  "error",
@@ -941,7 +1077,12 @@ var HttpTransportBaseConfigSchema = zod.z.object({
941
1077
  batchSize: zod.z.number().int().positive().optional(),
942
1078
  flushInterval: zod.z.number().int().positive().optional(),
943
1079
  maxRetries: zod.z.number().int().nonnegative().optional(),
944
- retryDelay: zod.z.number().int().positive().optional()
1080
+ retryDelay: zod.z.number().int().positive().optional(),
1081
+ onError: zod.z.function().optional(),
1082
+ onDrop: zod.z.function().optional(),
1083
+ maxQueueSize: zod.z.number().int().positive().optional(),
1084
+ dropPolicy: zod.z.enum(["drop-oldest", "drop-newest"]).optional(),
1085
+ requestTimeout: zod.z.number().int().positive().optional()
945
1086
  });
946
1087
  var DiscordConfigSchema = zod.z.object({
947
1088
  level: LogLevelSchema.optional(),
@@ -950,6 +1091,11 @@ var DiscordConfigSchema = zod.z.object({
950
1091
  flushInterval: zod.z.number().int().positive().optional(),
951
1092
  maxRetries: zod.z.number().int().nonnegative().optional(),
952
1093
  retryDelay: zod.z.number().int().positive().optional(),
1094
+ onError: zod.z.function().optional(),
1095
+ onDrop: zod.z.function().optional(),
1096
+ maxQueueSize: zod.z.number().int().positive().optional(),
1097
+ dropPolicy: zod.z.enum(["drop-oldest", "drop-newest"]).optional(),
1098
+ requestTimeout: zod.z.number().int().positive().optional(),
953
1099
  webhookUrl: zod.z.string().url("webhookUrl must be a valid URL"),
954
1100
  format: zod.z.enum(["embed", "markdown"]).optional(),
955
1101
  username: zod.z.string().optional(),
@@ -966,6 +1112,11 @@ var TelegramConfigSchema = zod.z.object({
966
1112
  flushInterval: zod.z.number().int().positive().optional(),
967
1113
  maxRetries: zod.z.number().int().nonnegative().optional(),
968
1114
  retryDelay: zod.z.number().int().positive().optional(),
1115
+ onError: zod.z.function().optional(),
1116
+ onDrop: zod.z.function().optional(),
1117
+ maxQueueSize: zod.z.number().int().positive().optional(),
1118
+ dropPolicy: zod.z.enum(["drop-oldest", "drop-newest"]).optional(),
1119
+ requestTimeout: zod.z.number().int().positive().optional(),
969
1120
  botToken: zod.z.string().min(1, "botToken is required"),
970
1121
  chatId: zod.z.union([zod.z.string(), zod.z.number()]),
971
1122
  parseMode: zod.z.enum(["Markdown", "MarkdownV2", "HTML"]).optional(),
@@ -998,6 +1149,11 @@ var CloudWatchConfigSchema = zod.z.object({
998
1149
  flushInterval: zod.z.number().int().positive().optional(),
999
1150
  maxRetries: zod.z.number().int().nonnegative().optional(),
1000
1151
  retryDelay: zod.z.number().int().positive().optional(),
1152
+ onError: zod.z.function().optional(),
1153
+ onDrop: zod.z.function().optional(),
1154
+ maxQueueSize: zod.z.number().int().positive().optional(),
1155
+ dropPolicy: zod.z.enum(["drop-oldest", "drop-newest"]).optional(),
1156
+ requestTimeout: zod.z.number().int().positive().optional(),
1001
1157
  logGroupName: zod.z.string().min(1, "logGroupName is required"),
1002
1158
  logStreamName: LogStreamNameSchema.optional(),
1003
1159
  region: zod.z.string().min(1, "region is required"),
@@ -1006,6 +1162,14 @@ var CloudWatchConfigSchema = zod.z.object({
1006
1162
  createLogGroup: zod.z.boolean().optional(),
1007
1163
  createLogStream: zod.z.boolean().optional()
1008
1164
  });
1165
+ var RelayConfigSchema = zod.z.object({
1166
+ apiUrl: zod.z.string().url("apiUrl must be a valid URL"),
1167
+ token: zod.z.string().min(1, "token is required"),
1168
+ pollInterval: zod.z.number().int().positive().optional(),
1169
+ bufferSize: zod.z.number().int().positive().optional(),
1170
+ reconnectDelay: zod.z.number().int().positive().optional(),
1171
+ maxReconnectDelay: zod.z.number().int().positive().optional()
1172
+ });
1009
1173
  var LevelConfigObjectSchema = zod.z.object({
1010
1174
  default: LogLevelSchema,
1011
1175
  rules: zod.z.array(LevelRuleSchema).optional()
@@ -1029,6 +1193,7 @@ var LoggerConfigSchema = zod.z.object({
1029
1193
  discord: zod.z.union([DiscordConfigSchema, zod.z.array(DiscordConfigSchema)]).optional(),
1030
1194
  telegram: zod.z.union([TelegramConfigSchema, zod.z.array(TelegramConfigSchema)]).optional(),
1031
1195
  cloudwatch: zod.z.union([CloudWatchConfigSchema, zod.z.array(CloudWatchConfigSchema)]).optional(),
1196
+ relay: RelayConfigSchema.optional(),
1032
1197
  caller: zod.z.union([zod.z.boolean(), CallerConfigSchema]).optional(),
1033
1198
  hostname: zod.z.string().optional(),
1034
1199
  autoShutdown: zod.z.union([zod.z.boolean(), AutoShutdownConfigSchema]).optional()
@@ -1276,7 +1441,10 @@ var Logger = class _Logger {
1276
1441
  this.state = state;
1277
1442
  this.context = context;
1278
1443
  }
1279
- profileTimers = /* @__PURE__ */ new Map();
1444
+ // Lazy: `.for()` creates many child loggers (hot path), most never call
1445
+ // `profile()`. Allocating a fresh Map per child wastes ~200 bytes each and
1446
+ // caused OOM in benchmarks creating millions of children.
1447
+ profileTimers;
1280
1448
  static create(config, store) {
1281
1449
  const validatedConfig = validateConfig(config);
1282
1450
  const state = createState(validatedConfig, store);
@@ -1296,12 +1464,12 @@ var Logger = class _Logger {
1296
1464
  }
1297
1465
  setLevelOverride(match, level) {
1298
1466
  assertLogLevel(level);
1299
- const key = JSON.stringify(match);
1467
+ const key = canonicalMatchKey(match);
1300
1468
  this.state.levelOverrides.set(key, { match, level });
1301
1469
  rebuildIndexes(this.state);
1302
1470
  }
1303
1471
  removeLevelOverride(match) {
1304
- const key = JSON.stringify(match);
1472
+ const key = canonicalMatchKey(match);
1305
1473
  const override = this.state.levelOverrides.get(key);
1306
1474
  if (override?.readonly) {
1307
1475
  return false;
@@ -1331,33 +1499,55 @@ var Logger = class _Logger {
1331
1499
  /**
1332
1500
  * Gracefully shutdown the logger, flushing all pending messages.
1333
1501
  * Should be called before process exit to ensure no logs are lost.
1502
+ *
1503
+ * `timeoutMs` is forwarded to each transport's `close()` as a per-transport
1504
+ * cap; it prevents a dead transport from blocking the whole shutdown.
1505
+ * Default: 5000ms. Pass `Infinity` for legacy unbounded behaviour.
1334
1506
  */
1335
- async shutdown() {
1336
- const closePromises = this.state.transports.map((transport) => transport.close());
1507
+ async shutdown(timeoutMs = 5e3) {
1508
+ const closePromises = this.state.transports.map((transport) => transport.close(timeoutMs));
1337
1509
  await Promise.all(closePromises);
1338
1510
  }
1339
1511
  // Profiling
1340
1512
  profile(id, meta) {
1341
- const existing = this.profileTimers.get(id);
1513
+ const timers = this.profileTimers ??= /* @__PURE__ */ new Map();
1514
+ const existing = timers.get(id);
1342
1515
  if (existing) {
1343
1516
  const duration = Date.now() - existing;
1344
- this.profileTimers.delete(id);
1517
+ timers.delete(id);
1345
1518
  this.info(`${id} completed`, { ...meta, durationMs: duration });
1346
1519
  } else {
1347
- this.profileTimers.set(id, Date.now());
1520
+ timers.set(id, Date.now());
1348
1521
  }
1349
1522
  }
1350
1523
  // Logging methods
1524
+ /**
1525
+ * Log an error. Supported shapes:
1526
+ * error(message: string)
1527
+ * error(message: string, meta)
1528
+ * error(error: Error | unknown)
1529
+ * error(error: Error | unknown, message: string)
1530
+ * error(error: Error | unknown, meta)
1531
+ * error(error: Error | unknown, message: string, meta)
1532
+ *
1533
+ * The first argument is treated as an error value whenever it is not a string.
1534
+ * Non-Error values (plain objects, numbers, etc. — e.g. anything caught by
1535
+ * TypeScript's `catch (err)` clause, which is typed as `unknown`) are passed
1536
+ * through `serializeError` so they produce a sensible `errorMessage` and, when
1537
+ * possible, a `stack` / HTTP diagnostic payload.
1538
+ */
1351
1539
  error(errorOrMessage, messageOrMeta, meta) {
1352
1540
  if (!shouldLog(this.state, "error", this.context)) return;
1353
- if (errorOrMessage instanceof Error) {
1354
- if (typeof messageOrMeta === "string") {
1355
- this.log("error", messageOrMeta, meta, errorOrMessage);
1356
- } else {
1357
- this.log("error", errorOrMessage.message, messageOrMeta, errorOrMessage);
1358
- }
1359
- } else {
1541
+ if (typeof errorOrMessage === "string") {
1360
1542
  this.log("error", errorOrMessage, messageOrMeta);
1543
+ return;
1544
+ }
1545
+ const errorValue = errorOrMessage;
1546
+ if (typeof messageOrMeta === "string") {
1547
+ this.log("error", messageOrMeta, meta, errorValue);
1548
+ } else {
1549
+ const fallbackMessage = errorValue instanceof Error ? errorValue.message : serializeError(errorValue).errorMessage;
1550
+ this.log("error", fallbackMessage, messageOrMeta, errorValue);
1361
1551
  }
1362
1552
  }
1363
1553
  warn(message, meta) {
@@ -1392,6 +1582,10 @@ var Logger = class _Logger {
1392
1582
  if (storeContext) {
1393
1583
  Object.assign(logMeta, storeContext);
1394
1584
  }
1585
+ const overrideLevel = getGlobalOverrideLevel2(this.state, this.context, logMeta);
1586
+ if (overrideLevel !== void 0) {
1587
+ logMeta.__or = overrideLevel;
1588
+ }
1395
1589
  if (this.state.callerConfig) {
1396
1590
  const callerInfo = getCallerInfo(this.state.callerConfig, callerOffset);
1397
1591
  if (callerInfo) {