@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/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,21 +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 logMeta = {};
606
- for (const [key, value] of Object.entries(log)) {
607
- if (!["level", "time", "msg", "context"].includes(key)) {
608
- logMeta[key] = value;
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];
609
707
  }
610
708
  }
611
- const matchingRule = rules?.find((rule) => matchesContext(storeContext, context, rule.match, logMeta));
612
- const effectiveLevel = matchingRule?.level ?? level ?? "silly";
709
+ const effectiveLevel = level ?? "silly";
613
710
  if (effectiveLevel === "off") return false;
614
711
  return LOG_LEVELS[logLevel] <= LOG_LEVELS[effectiveLevel];
615
712
  }
713
+ var RESERVED_LOG_FIELDS = /* @__PURE__ */ new Set(["level", "time", "msg", "context", "__or"]);
616
714
  function formatLog(log, format, store) {
617
715
  const levelName = getLevelName(log.level);
618
716
  const timestamp = new Date(log.time).toISOString();
@@ -621,7 +719,7 @@ function formatLog(log, format, store) {
621
719
  const storeContext = store.getStore();
622
720
  const meta = {};
623
721
  for (const [key, value] of Object.entries(log)) {
624
- if (!["level", "time", "msg", "context"].includes(key)) {
722
+ if (!RESERVED_LOG_FIELDS.has(key)) {
625
723
  meta[key] = value;
626
724
  }
627
725
  }
@@ -670,7 +768,7 @@ function formatLog(log, format, store) {
670
768
  return `[${timestamp}] ${levelName}: ${message}
671
769
  `;
672
770
  }
673
- function createFormattedFilterStream(format, level, rules, store, destination) {
771
+ function createFormattedFilterStream(format, level, rules, state, destination) {
674
772
  return new stream.Transform({
675
773
  transform(chunk, _encoding, callback) {
676
774
  const line = chunk.toString().trim();
@@ -683,24 +781,24 @@ function createFormattedFilterStream(format, level, rules, store, destination) {
683
781
  callback();
684
782
  return;
685
783
  }
686
- if (!shouldPassTransport(log, level, rules, store)) {
784
+ if (!shouldPassTransport(log, level, rules, state)) {
687
785
  callback();
688
786
  return;
689
787
  }
690
- const formatted = formatLog(log, format, store);
788
+ const formatted = formatLog(log, format, state.store);
691
789
  destination.write(formatted);
692
790
  callback();
693
791
  }
694
792
  });
695
793
  }
696
- function createStreams(config, store) {
794
+ function createStreams(config, state) {
697
795
  const streams = [];
698
796
  const transports = [];
699
797
  const consoleStream = createFormattedFilterStream(
700
798
  config.console.format,
701
799
  config.console.level,
702
800
  config.console.rules,
703
- store,
801
+ state,
704
802
  process.stdout
705
803
  );
706
804
  streams.push({
@@ -733,7 +831,7 @@ function createStreams(config, store) {
733
831
  fileConfig.format,
734
832
  fileConfig.level,
735
833
  fileConfig.rules,
736
- store,
834
+ state,
737
835
  rotatingStream
738
836
  );
739
837
  streams.push({
@@ -744,7 +842,7 @@ function createStreams(config, store) {
744
842
  for (const discordConfig of toArray(config.discord)) {
745
843
  const transport = new DiscordTransport(discordConfig);
746
844
  transports.push(transport);
747
- const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, store);
845
+ const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, state);
748
846
  streams.push({
749
847
  level: "trace",
750
848
  stream: discordStream
@@ -753,7 +851,7 @@ function createStreams(config, store) {
753
851
  for (const telegramConfig of toArray(config.telegram)) {
754
852
  const transport = new TelegramTransport(telegramConfig);
755
853
  transports.push(transport);
756
- const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, store);
854
+ const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, state);
757
855
  streams.push({
758
856
  level: "trace",
759
857
  stream: telegramStream
@@ -762,12 +860,22 @@ function createStreams(config, store) {
762
860
  for (const cloudwatchConfig of toArray(config.cloudwatch)) {
763
861
  const transport = new CloudWatchTransport(cloudwatchConfig, config.hostname);
764
862
  transports.push(transport);
765
- const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, store);
863
+ const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, state);
766
864
  streams.push({
767
865
  level: "trace",
768
866
  stream: cwStream
769
867
  });
770
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
+ }
771
879
  return {
772
880
  destination: pino__default.default.multistream(streams),
773
881
  transports
@@ -777,7 +885,7 @@ function toArray(value) {
777
885
  if (!value) return [];
778
886
  return Array.isArray(value) ? value : [value];
779
887
  }
780
- function createHttpTransportStream(transport, level, rules, store) {
888
+ function createHttpTransportStream(transport, level, rules, state) {
781
889
  return new stream.Writable({
782
890
  write(chunk, _encoding, callback) {
783
891
  const line = chunk.toString().trim();
@@ -790,15 +898,15 @@ function createHttpTransportStream(transport, level, rules, store) {
790
898
  callback();
791
899
  return;
792
900
  }
793
- if (!shouldPassTransport(log, level, rules, store)) {
901
+ if (!shouldPassTransport(log, level, rules, state)) {
794
902
  callback();
795
903
  return;
796
904
  }
797
905
  const levelName = getLevelName(log.level);
798
- const storeContext = store.getStore();
906
+ const storeContext = state.store.getStore();
799
907
  const meta = {};
800
908
  for (const [key, value] of Object.entries(log)) {
801
- if (!["level", "time", "msg", "context"].includes(key)) {
909
+ if (!RESERVED_LOG_FIELDS.has(key)) {
802
910
  meta[key] = value;
803
911
  }
804
912
  }
@@ -847,45 +955,53 @@ function buildIndexes(overrides) {
847
955
  }
848
956
  return { contextIndex, complexRules };
849
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
+ }
850
966
  function createState(config, store) {
851
967
  const { defaultLevel, rules } = parseLevelConfig(config.level);
852
968
  const loggerStore = store ?? new LoggerStore();
853
969
  const levelOverrides = /* @__PURE__ */ new Map();
854
970
  for (const rule of rules) {
855
- const key = JSON.stringify(rule.match);
971
+ const key = canonicalMatchKey(rule.match);
856
972
  levelOverrides.set(key, rule);
857
973
  }
858
974
  const { contextIndex, complexRules } = buildIndexes(levelOverrides);
859
- 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;
860
987
  const options = {
861
988
  level: "trace",
862
989
  // Accept all, we filter in shouldLog()
863
990
  customLevels: CUSTOM_LEVELS,
864
991
  base: { hostname: config.hostname ?? os.hostname() }
865
992
  };
866
- const pinoLogger = pino__default.default(options, destination);
867
- let callerConfig;
993
+ state.pino = pino__default.default(options, destination);
868
994
  if (config.caller === true) {
869
- callerConfig = {};
995
+ state.callerConfig = {};
870
996
  } else if (config.caller && typeof config.caller === "object") {
871
- callerConfig = config.caller;
997
+ state.callerConfig = config.caller;
872
998
  }
873
- return {
874
- pino: pinoLogger,
875
- store: loggerStore,
876
- defaultLevel,
877
- levelOverrides,
878
- contextIndex,
879
- complexRules,
880
- callerConfig,
881
- transports
882
- };
999
+ return state;
883
1000
  }
884
1001
  function rebuildIndexes(state) {
885
1002
  const { contextIndex, complexRules } = buildIndexes(state.levelOverrides);
886
1003
  state.contextIndex = contextIndex;
887
- state.complexRules.length = 0;
888
- state.complexRules.push(...complexRules);
1004
+ state.complexRules = complexRules;
889
1005
  }
890
1006
  function shouldLog(state, level, context) {
891
1007
  const effectiveLevel = getEffectiveLevel(state, context);
@@ -909,6 +1025,20 @@ function matchesContext(storeContext, loggerContext, match, logMeta) {
909
1025
  const combined = { ...logMeta, ...storeContext, context: loggerContext };
910
1026
  return Object.entries(match).every(([key, value]) => combined[key] === value);
911
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
+ }
912
1042
  var LogLevelSchema = zod.z.enum([
913
1043
  "off",
914
1044
  "error",
@@ -947,7 +1077,12 @@ var HttpTransportBaseConfigSchema = zod.z.object({
947
1077
  batchSize: zod.z.number().int().positive().optional(),
948
1078
  flushInterval: zod.z.number().int().positive().optional(),
949
1079
  maxRetries: zod.z.number().int().nonnegative().optional(),
950
- 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()
951
1086
  });
952
1087
  var DiscordConfigSchema = zod.z.object({
953
1088
  level: LogLevelSchema.optional(),
@@ -956,6 +1091,11 @@ var DiscordConfigSchema = zod.z.object({
956
1091
  flushInterval: zod.z.number().int().positive().optional(),
957
1092
  maxRetries: zod.z.number().int().nonnegative().optional(),
958
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(),
959
1099
  webhookUrl: zod.z.string().url("webhookUrl must be a valid URL"),
960
1100
  format: zod.z.enum(["embed", "markdown"]).optional(),
961
1101
  username: zod.z.string().optional(),
@@ -972,6 +1112,11 @@ var TelegramConfigSchema = zod.z.object({
972
1112
  flushInterval: zod.z.number().int().positive().optional(),
973
1113
  maxRetries: zod.z.number().int().nonnegative().optional(),
974
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(),
975
1120
  botToken: zod.z.string().min(1, "botToken is required"),
976
1121
  chatId: zod.z.union([zod.z.string(), zod.z.number()]),
977
1122
  parseMode: zod.z.enum(["Markdown", "MarkdownV2", "HTML"]).optional(),
@@ -1004,6 +1149,11 @@ var CloudWatchConfigSchema = zod.z.object({
1004
1149
  flushInterval: zod.z.number().int().positive().optional(),
1005
1150
  maxRetries: zod.z.number().int().nonnegative().optional(),
1006
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(),
1007
1157
  logGroupName: zod.z.string().min(1, "logGroupName is required"),
1008
1158
  logStreamName: LogStreamNameSchema.optional(),
1009
1159
  region: zod.z.string().min(1, "region is required"),
@@ -1012,6 +1162,14 @@ var CloudWatchConfigSchema = zod.z.object({
1012
1162
  createLogGroup: zod.z.boolean().optional(),
1013
1163
  createLogStream: zod.z.boolean().optional()
1014
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
+ });
1015
1173
  var LevelConfigObjectSchema = zod.z.object({
1016
1174
  default: LogLevelSchema,
1017
1175
  rules: zod.z.array(LevelRuleSchema).optional()
@@ -1035,6 +1193,7 @@ var LoggerConfigSchema = zod.z.object({
1035
1193
  discord: zod.z.union([DiscordConfigSchema, zod.z.array(DiscordConfigSchema)]).optional(),
1036
1194
  telegram: zod.z.union([TelegramConfigSchema, zod.z.array(TelegramConfigSchema)]).optional(),
1037
1195
  cloudwatch: zod.z.union([CloudWatchConfigSchema, zod.z.array(CloudWatchConfigSchema)]).optional(),
1196
+ relay: RelayConfigSchema.optional(),
1038
1197
  caller: zod.z.union([zod.z.boolean(), CallerConfigSchema]).optional(),
1039
1198
  hostname: zod.z.string().optional(),
1040
1199
  autoShutdown: zod.z.union([zod.z.boolean(), AutoShutdownConfigSchema]).optional()
@@ -1282,7 +1441,10 @@ var Logger = class _Logger {
1282
1441
  this.state = state;
1283
1442
  this.context = context;
1284
1443
  }
1285
- 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;
1286
1448
  static create(config, store) {
1287
1449
  const validatedConfig = validateConfig(config);
1288
1450
  const state = createState(validatedConfig, store);
@@ -1302,12 +1464,12 @@ var Logger = class _Logger {
1302
1464
  }
1303
1465
  setLevelOverride(match, level) {
1304
1466
  assertLogLevel(level);
1305
- const key = JSON.stringify(match);
1467
+ const key = canonicalMatchKey(match);
1306
1468
  this.state.levelOverrides.set(key, { match, level });
1307
1469
  rebuildIndexes(this.state);
1308
1470
  }
1309
1471
  removeLevelOverride(match) {
1310
- const key = JSON.stringify(match);
1472
+ const key = canonicalMatchKey(match);
1311
1473
  const override = this.state.levelOverrides.get(key);
1312
1474
  if (override?.readonly) {
1313
1475
  return false;
@@ -1337,33 +1499,55 @@ var Logger = class _Logger {
1337
1499
  /**
1338
1500
  * Gracefully shutdown the logger, flushing all pending messages.
1339
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.
1340
1506
  */
1341
- async shutdown() {
1342
- 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));
1343
1509
  await Promise.all(closePromises);
1344
1510
  }
1345
1511
  // Profiling
1346
1512
  profile(id, meta) {
1347
- const existing = this.profileTimers.get(id);
1513
+ const timers = this.profileTimers ??= /* @__PURE__ */ new Map();
1514
+ const existing = timers.get(id);
1348
1515
  if (existing) {
1349
1516
  const duration = Date.now() - existing;
1350
- this.profileTimers.delete(id);
1517
+ timers.delete(id);
1351
1518
  this.info(`${id} completed`, { ...meta, durationMs: duration });
1352
1519
  } else {
1353
- this.profileTimers.set(id, Date.now());
1520
+ timers.set(id, Date.now());
1354
1521
  }
1355
1522
  }
1356
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
+ */
1357
1539
  error(errorOrMessage, messageOrMeta, meta) {
1358
1540
  if (!shouldLog(this.state, "error", this.context)) return;
1359
- if (errorOrMessage instanceof Error) {
1360
- if (typeof messageOrMeta === "string") {
1361
- this.log("error", messageOrMeta, meta, errorOrMessage);
1362
- } else {
1363
- this.log("error", errorOrMessage.message, messageOrMeta, errorOrMessage);
1364
- }
1365
- } else {
1541
+ if (typeof errorOrMessage === "string") {
1366
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);
1367
1551
  }
1368
1552
  }
1369
1553
  warn(message, meta) {
@@ -1398,6 +1582,10 @@ var Logger = class _Logger {
1398
1582
  if (storeContext) {
1399
1583
  Object.assign(logMeta, storeContext);
1400
1584
  }
1585
+ const overrideLevel = getGlobalOverrideLevel2(this.state, this.context, logMeta);
1586
+ if (overrideLevel !== void 0) {
1587
+ logMeta.__or = overrideLevel;
1588
+ }
1401
1589
  if (this.state.callerConfig) {
1402
1590
  const callerInfo = getCallerInfo(this.state.callerConfig, callerOffset);
1403
1591
  if (callerInfo) {