@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.mjs CHANGED
@@ -4,7 +4,6 @@ import { AsyncLocalStorage } from 'async_hooks';
4
4
  import { Transform, Writable } from 'stream';
5
5
  import { mkdir } from 'fs/promises';
6
6
  import { createStream } from 'rotating-file-stream';
7
- import { EventEmitter } from 'events';
8
7
  import { CloudWatchLogsClient, PutLogEventsCommand, CreateLogGroupCommand, CreateLogStreamCommand, DescribeLogStreamsCommand } from '@aws-sdk/client-cloudwatch-logs';
9
8
  import { randomUUID } from 'crypto';
10
9
  import { z } from 'zod';
@@ -50,8 +49,24 @@ var MessageBuffer = class {
50
49
  timer = null;
51
50
  flushing = false;
52
51
  closed = false;
52
+ droppedCount = 0;
53
53
  add(message) {
54
54
  if (this.closed) return;
55
+ const limit = this.options.maxQueueSize;
56
+ if (limit !== void 0 && this.queue.length >= limit) {
57
+ const policy = this.options.dropPolicy ?? "drop-oldest";
58
+ const dropped = policy === "drop-oldest" ? [this.queue.shift()] : [message];
59
+ this.droppedCount += 1;
60
+ if (this.options.onDrop) {
61
+ try {
62
+ this.options.onDrop(dropped);
63
+ } catch {
64
+ }
65
+ }
66
+ if (policy === "drop-newest") {
67
+ return;
68
+ }
69
+ }
55
70
  this.queue.push(message);
56
71
  if (this.queue.length >= this.options.batchSize) {
57
72
  void this.flush();
@@ -75,13 +90,34 @@ var MessageBuffer = class {
75
90
  }
76
91
  }
77
92
  }
78
- async close() {
93
+ /**
94
+ * Stops accepting new messages and attempts to flush what's already buffered.
95
+ * Returns when the queue is drained OR when `timeoutMs` elapses. A timeout
96
+ * is essential during graceful shutdown — without it, a dead transport would
97
+ * block process exit indefinitely, and orchestrators like Kubernetes would
98
+ * escalate to SIGKILL after their grace period.
99
+ *
100
+ * Default timeout: 5 seconds. Pass `Infinity` to preserve legacy behaviour.
101
+ */
102
+ async close(timeoutMs = 5e3) {
79
103
  this.closed = true;
80
104
  this.clearTimer();
105
+ const deadline = timeoutMs === Infinity ? Infinity : Date.now() + timeoutMs;
81
106
  while (this.queue.length > 0) {
107
+ if (Date.now() >= deadline) {
108
+ return;
109
+ }
82
110
  await this.flush();
83
111
  }
84
112
  }
113
+ /** Current number of messages waiting in the queue. */
114
+ get size() {
115
+ return this.queue.length;
116
+ }
117
+ /** Total number of messages dropped due to queue overflow since creation. */
118
+ get droppedTotal() {
119
+ return this.droppedCount;
120
+ }
85
121
  scheduleFlush() {
86
122
  if (this.timer || this.closed) return;
87
123
  this.timer = setTimeout(() => {
@@ -122,15 +158,19 @@ var DEFAULT_OPTIONS = {
122
158
  maxRetries: 3,
123
159
  retryDelay: 1e3
124
160
  };
125
- var BaseHttpTransport = class extends EventEmitter {
161
+ var BaseHttpTransport = class {
126
162
  buffer;
163
+ onErrorCallback;
127
164
  constructor(opts = {}) {
128
- super();
165
+ this.onErrorCallback = opts.onError;
129
166
  this.buffer = new MessageBuffer({
130
167
  batchSize: opts.batchSize ?? DEFAULT_OPTIONS.batchSize,
131
168
  flushInterval: opts.flushInterval ?? DEFAULT_OPTIONS.flushInterval,
132
169
  maxRetries: opts.maxRetries ?? DEFAULT_OPTIONS.maxRetries,
133
170
  retryDelay: opts.retryDelay ?? DEFAULT_OPTIONS.retryDelay,
171
+ maxQueueSize: opts.maxQueueSize,
172
+ dropPolicy: opts.dropPolicy,
173
+ onDrop: opts.onDrop,
134
174
  onFlush: this.sendBatch.bind(this),
135
175
  onError: this.handleError.bind(this)
136
176
  });
@@ -140,8 +180,20 @@ var BaseHttpTransport = class extends EventEmitter {
140
180
  this.buffer.add(message);
141
181
  callback();
142
182
  }
143
- close() {
144
- return this.buffer.close();
183
+ /**
184
+ * Stop accepting new messages and flush what's buffered. `timeoutMs` caps
185
+ * how long the flush may take; after that the remaining queue is discarded
186
+ * so shutdown can complete. Default 5000ms.
187
+ */
188
+ close(timeoutMs) {
189
+ return this.buffer.close(timeoutMs);
190
+ }
191
+ /** Current buffered message count and total drops since creation. */
192
+ getMetrics() {
193
+ return {
194
+ queueSize: this.buffer.size,
195
+ droppedTotal: this.buffer.droppedTotal
196
+ };
145
197
  }
146
198
  transformMessage(info) {
147
199
  const { level, message, timestamp, context, ...meta } = info;
@@ -154,11 +206,21 @@ var BaseHttpTransport = class extends EventEmitter {
154
206
  };
155
207
  }
156
208
  handleError(error, messages) {
209
+ if (this.onErrorCallback) {
210
+ try {
211
+ this.onErrorCallback(error, messages);
212
+ return;
213
+ } catch (callbackError) {
214
+ console.error(
215
+ `[${this.constructor.name}] onError callback threw:`,
216
+ callbackError instanceof Error ? callbackError.message : callbackError
217
+ );
218
+ }
219
+ }
157
220
  console.error(
158
221
  `[${this.constructor.name}] Failed to send ${messages.length} messages:`,
159
222
  error.message
160
223
  );
161
- this.emit("error", error);
162
224
  }
163
225
  };
164
226
 
@@ -183,16 +245,23 @@ var LEVEL_EMOJI = {
183
245
  debug: "\u26AA",
184
246
  silly: "\u26AB"
185
247
  };
248
+ var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
186
249
  var DiscordTransport = class extends BaseHttpTransport {
187
250
  config;
251
+ requestTimeout;
188
252
  constructor(config) {
189
253
  super({
190
254
  batchSize: config.batchSize ?? 10,
191
255
  flushInterval: config.flushInterval ?? 2e3,
192
256
  maxRetries: config.maxRetries,
193
- retryDelay: config.retryDelay
257
+ retryDelay: config.retryDelay,
258
+ maxQueueSize: config.maxQueueSize,
259
+ dropPolicy: config.dropPolicy,
260
+ onDrop: config.onDrop,
261
+ onError: config.onError
194
262
  });
195
263
  this.config = config;
264
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
196
265
  }
197
266
  async sendBatch(messages) {
198
267
  if (this.config.format === "markdown") {
@@ -296,7 +365,8 @@ ${msg.message}`;
296
365
  const response = await fetch(this.config.webhookUrl, {
297
366
  method: "POST",
298
367
  headers: { "Content-Type": "application/json" },
299
- body: JSON.stringify(payload)
368
+ body: JSON.stringify(payload),
369
+ signal: AbortSignal.timeout(this.requestTimeout)
300
370
  });
301
371
  if (!response.ok) {
302
372
  const text = await response.text();
@@ -323,18 +393,25 @@ var LEVEL_EMOJI2 = {
323
393
  debug: "\u26AA",
324
394
  silly: "\u26AB"
325
395
  };
396
+ var DEFAULT_REQUEST_TIMEOUT_MS2 = 1e4;
326
397
  var TelegramTransport = class extends BaseHttpTransport {
327
398
  config;
328
399
  apiUrl;
400
+ requestTimeout;
329
401
  constructor(config) {
330
402
  super({
331
403
  batchSize: config.batchSize ?? 20,
332
404
  flushInterval: config.flushInterval ?? 1e3,
333
405
  maxRetries: config.maxRetries,
334
- retryDelay: config.retryDelay
406
+ retryDelay: config.retryDelay,
407
+ maxQueueSize: config.maxQueueSize,
408
+ dropPolicy: config.dropPolicy,
409
+ onDrop: config.onDrop,
410
+ onError: config.onError
335
411
  });
336
412
  this.config = config;
337
413
  this.apiUrl = `https://api.telegram.org/bot${config.botToken}`;
414
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS2;
338
415
  }
339
416
  async sendBatch(messages) {
340
417
  const text = this.formatBatchMessage(messages);
@@ -395,7 +472,8 @@ var TelegramTransport = class extends BaseHttpTransport {
395
472
  const response = await fetch(`${this.apiUrl}/sendMessage`, {
396
473
  method: "POST",
397
474
  headers: { "Content-Type": "application/json" },
398
- body: JSON.stringify(body)
475
+ body: JSON.stringify(body),
476
+ signal: AbortSignal.timeout(this.requestTimeout)
399
477
  });
400
478
  if (!response.ok) {
401
479
  const result = await response.json();
@@ -464,7 +542,11 @@ var CloudWatchTransport = class extends BaseHttpTransport {
464
542
  batchSize: config.batchSize ?? 100,
465
543
  flushInterval: config.flushInterval ?? 1e3,
466
544
  maxRetries: config.maxRetries,
467
- retryDelay: config.retryDelay
545
+ retryDelay: config.retryDelay,
546
+ maxQueueSize: config.maxQueueSize,
547
+ dropPolicy: config.dropPolicy,
548
+ onDrop: config.onDrop,
549
+ onError: config.onError
468
550
  });
469
551
  this.config = config;
470
552
  this.resolvedLogStreamName = resolveLogStreamName(config.logStreamName, configHostname);
@@ -516,7 +598,10 @@ var CloudWatchTransport = class extends BaseHttpTransport {
516
598
  async ensureInitialized() {
517
599
  if (this.initialized) return;
518
600
  if (!this.initPromise) {
519
- this.initPromise = this.initialize();
601
+ this.initPromise = this.initialize().catch((err) => {
602
+ this.initPromise = null;
603
+ throw err;
604
+ });
520
605
  }
521
606
  await this.initPromise;
522
607
  }
@@ -592,21 +677,34 @@ function getLevelName(levelNum) {
592
677
  if (levelNum >= 20) return "debug";
593
678
  return "silly";
594
679
  }
595
- function shouldPassTransport(log, level, rules, store) {
680
+ function shouldPassTransport(log, level, rules, state) {
596
681
  const logLevel = getLevelName(log.level);
682
+ const embeddedOverride = log.__or;
683
+ if (typeof embeddedOverride === "string") {
684
+ if (embeddedOverride === "off") return false;
685
+ const overrideLevel = embeddedOverride;
686
+ return LOG_LEVELS[logLevel] <= LOG_LEVELS[overrideLevel];
687
+ }
597
688
  const context = log.context;
598
- const storeContext = store.getStore();
599
- const logMeta = {};
600
- for (const [key, value] of Object.entries(log)) {
601
- if (!["level", "time", "msg", "context"].includes(key)) {
602
- logMeta[key] = value;
689
+ if (rules && rules.length > 0) {
690
+ const storeContext = state.store.getStore();
691
+ const logMeta = {};
692
+ for (const [key, value] of Object.entries(log)) {
693
+ if (!RESERVED_LOG_FIELDS.has(key)) {
694
+ logMeta[key] = value;
695
+ }
696
+ }
697
+ const matchingRule = rules.find((rule) => matchesContext(storeContext, context, rule.match, logMeta));
698
+ if (matchingRule) {
699
+ if (matchingRule.level === "off") return false;
700
+ return LOG_LEVELS[logLevel] <= LOG_LEVELS[matchingRule.level];
603
701
  }
604
702
  }
605
- const matchingRule = rules?.find((rule) => matchesContext(storeContext, context, rule.match, logMeta));
606
- const effectiveLevel = matchingRule?.level ?? level ?? "silly";
703
+ const effectiveLevel = level ?? "silly";
607
704
  if (effectiveLevel === "off") return false;
608
705
  return LOG_LEVELS[logLevel] <= LOG_LEVELS[effectiveLevel];
609
706
  }
707
+ var RESERVED_LOG_FIELDS = /* @__PURE__ */ new Set(["level", "time", "msg", "context", "__or"]);
610
708
  function formatLog(log, format, store) {
611
709
  const levelName = getLevelName(log.level);
612
710
  const timestamp = new Date(log.time).toISOString();
@@ -615,7 +713,7 @@ function formatLog(log, format, store) {
615
713
  const storeContext = store.getStore();
616
714
  const meta = {};
617
715
  for (const [key, value] of Object.entries(log)) {
618
- if (!["level", "time", "msg", "context"].includes(key)) {
716
+ if (!RESERVED_LOG_FIELDS.has(key)) {
619
717
  meta[key] = value;
620
718
  }
621
719
  }
@@ -664,7 +762,7 @@ function formatLog(log, format, store) {
664
762
  return `[${timestamp}] ${levelName}: ${message}
665
763
  `;
666
764
  }
667
- function createFormattedFilterStream(format, level, rules, store, destination) {
765
+ function createFormattedFilterStream(format, level, rules, state, destination) {
668
766
  return new Transform({
669
767
  transform(chunk, _encoding, callback) {
670
768
  const line = chunk.toString().trim();
@@ -677,24 +775,24 @@ function createFormattedFilterStream(format, level, rules, store, destination) {
677
775
  callback();
678
776
  return;
679
777
  }
680
- if (!shouldPassTransport(log, level, rules, store)) {
778
+ if (!shouldPassTransport(log, level, rules, state)) {
681
779
  callback();
682
780
  return;
683
781
  }
684
- const formatted = formatLog(log, format, store);
782
+ const formatted = formatLog(log, format, state.store);
685
783
  destination.write(formatted);
686
784
  callback();
687
785
  }
688
786
  });
689
787
  }
690
- function createStreams(config, store) {
788
+ function createStreams(config, state) {
691
789
  const streams = [];
692
790
  const transports = [];
693
791
  const consoleStream = createFormattedFilterStream(
694
792
  config.console.format,
695
793
  config.console.level,
696
794
  config.console.rules,
697
- store,
795
+ state,
698
796
  process.stdout
699
797
  );
700
798
  streams.push({
@@ -727,7 +825,7 @@ function createStreams(config, store) {
727
825
  fileConfig.format,
728
826
  fileConfig.level,
729
827
  fileConfig.rules,
730
- store,
828
+ state,
731
829
  rotatingStream
732
830
  );
733
831
  streams.push({
@@ -738,7 +836,7 @@ function createStreams(config, store) {
738
836
  for (const discordConfig of toArray(config.discord)) {
739
837
  const transport = new DiscordTransport(discordConfig);
740
838
  transports.push(transport);
741
- const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, store);
839
+ const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, state);
742
840
  streams.push({
743
841
  level: "trace",
744
842
  stream: discordStream
@@ -747,7 +845,7 @@ function createStreams(config, store) {
747
845
  for (const telegramConfig of toArray(config.telegram)) {
748
846
  const transport = new TelegramTransport(telegramConfig);
749
847
  transports.push(transport);
750
- const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, store);
848
+ const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, state);
751
849
  streams.push({
752
850
  level: "trace",
753
851
  stream: telegramStream
@@ -756,12 +854,22 @@ function createStreams(config, store) {
756
854
  for (const cloudwatchConfig of toArray(config.cloudwatch)) {
757
855
  const transport = new CloudWatchTransport(cloudwatchConfig, config.hostname);
758
856
  transports.push(transport);
759
- const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, store);
857
+ const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, state);
760
858
  streams.push({
761
859
  level: "trace",
762
860
  stream: cwStream
763
861
  });
764
862
  }
863
+ if (config.relay) {
864
+ const relayStream = pino.transport({
865
+ target: "@rawnodes/logger/relay",
866
+ options: config.relay
867
+ });
868
+ streams.push({
869
+ level: "trace",
870
+ stream: relayStream
871
+ });
872
+ }
765
873
  return {
766
874
  destination: pino.multistream(streams),
767
875
  transports
@@ -771,7 +879,7 @@ function toArray(value) {
771
879
  if (!value) return [];
772
880
  return Array.isArray(value) ? value : [value];
773
881
  }
774
- function createHttpTransportStream(transport, level, rules, store) {
882
+ function createHttpTransportStream(transport, level, rules, state) {
775
883
  return new Writable({
776
884
  write(chunk, _encoding, callback) {
777
885
  const line = chunk.toString().trim();
@@ -784,15 +892,15 @@ function createHttpTransportStream(transport, level, rules, store) {
784
892
  callback();
785
893
  return;
786
894
  }
787
- if (!shouldPassTransport(log, level, rules, store)) {
895
+ if (!shouldPassTransport(log, level, rules, state)) {
788
896
  callback();
789
897
  return;
790
898
  }
791
899
  const levelName = getLevelName(log.level);
792
- const storeContext = store.getStore();
900
+ const storeContext = state.store.getStore();
793
901
  const meta = {};
794
902
  for (const [key, value] of Object.entries(log)) {
795
- if (!["level", "time", "msg", "context"].includes(key)) {
903
+ if (!RESERVED_LOG_FIELDS.has(key)) {
796
904
  meta[key] = value;
797
905
  }
798
906
  }
@@ -841,45 +949,53 @@ function buildIndexes(overrides) {
841
949
  }
842
950
  return { contextIndex, complexRules };
843
951
  }
952
+ function canonicalMatchKey(match) {
953
+ const keys = Object.keys(match).sort();
954
+ const sorted = {};
955
+ for (const key of keys) {
956
+ sorted[key] = match[key];
957
+ }
958
+ return JSON.stringify(sorted);
959
+ }
844
960
  function createState(config, store) {
845
961
  const { defaultLevel, rules } = parseLevelConfig(config.level);
846
962
  const loggerStore = store ?? new LoggerStore();
847
963
  const levelOverrides = /* @__PURE__ */ new Map();
848
964
  for (const rule of rules) {
849
- const key = JSON.stringify(rule.match);
965
+ const key = canonicalMatchKey(rule.match);
850
966
  levelOverrides.set(key, rule);
851
967
  }
852
968
  const { contextIndex, complexRules } = buildIndexes(levelOverrides);
853
- const { destination, transports } = createStreams(config, loggerStore);
969
+ const state = {
970
+ pino: null,
971
+ store: loggerStore,
972
+ defaultLevel,
973
+ levelOverrides,
974
+ contextIndex,
975
+ complexRules,
976
+ callerConfig: void 0,
977
+ transports: []
978
+ };
979
+ const { destination, transports } = createStreams(config, state);
980
+ state.transports = transports;
854
981
  const options = {
855
982
  level: "trace",
856
983
  // Accept all, we filter in shouldLog()
857
984
  customLevels: CUSTOM_LEVELS,
858
985
  base: { hostname: config.hostname ?? hostname() }
859
986
  };
860
- const pinoLogger = pino(options, destination);
861
- let callerConfig;
987
+ state.pino = pino(options, destination);
862
988
  if (config.caller === true) {
863
- callerConfig = {};
989
+ state.callerConfig = {};
864
990
  } else if (config.caller && typeof config.caller === "object") {
865
- callerConfig = config.caller;
991
+ state.callerConfig = config.caller;
866
992
  }
867
- return {
868
- pino: pinoLogger,
869
- store: loggerStore,
870
- defaultLevel,
871
- levelOverrides,
872
- contextIndex,
873
- complexRules,
874
- callerConfig,
875
- transports
876
- };
993
+ return state;
877
994
  }
878
995
  function rebuildIndexes(state) {
879
996
  const { contextIndex, complexRules } = buildIndexes(state.levelOverrides);
880
997
  state.contextIndex = contextIndex;
881
- state.complexRules.length = 0;
882
- state.complexRules.push(...complexRules);
998
+ state.complexRules = complexRules;
883
999
  }
884
1000
  function shouldLog(state, level, context) {
885
1001
  const effectiveLevel = getEffectiveLevel(state, context);
@@ -903,6 +1019,20 @@ function matchesContext(storeContext, loggerContext, match, logMeta) {
903
1019
  const combined = { ...logMeta, ...storeContext, context: loggerContext };
904
1020
  return Object.entries(match).every(([key, value]) => combined[key] === value);
905
1021
  }
1022
+ function getGlobalOverrideLevel2(state, loggerContext, logMeta) {
1023
+ if (loggerContext) {
1024
+ const indexed = state.contextIndex.get(loggerContext);
1025
+ if (indexed && !indexed.readonly) return indexed.level;
1026
+ }
1027
+ const storeContext = state.store.getStore();
1028
+ for (const override of state.complexRules) {
1029
+ if (override.readonly) continue;
1030
+ if (matchesContext(storeContext, loggerContext, override.match, logMeta)) {
1031
+ return override.level;
1032
+ }
1033
+ }
1034
+ return void 0;
1035
+ }
906
1036
  var LogLevelSchema = z.enum([
907
1037
  "off",
908
1038
  "error",
@@ -941,7 +1071,12 @@ var HttpTransportBaseConfigSchema = z.object({
941
1071
  batchSize: z.number().int().positive().optional(),
942
1072
  flushInterval: z.number().int().positive().optional(),
943
1073
  maxRetries: z.number().int().nonnegative().optional(),
944
- retryDelay: z.number().int().positive().optional()
1074
+ retryDelay: z.number().int().positive().optional(),
1075
+ onError: z.function().optional(),
1076
+ onDrop: z.function().optional(),
1077
+ maxQueueSize: z.number().int().positive().optional(),
1078
+ dropPolicy: z.enum(["drop-oldest", "drop-newest"]).optional(),
1079
+ requestTimeout: z.number().int().positive().optional()
945
1080
  });
946
1081
  var DiscordConfigSchema = z.object({
947
1082
  level: LogLevelSchema.optional(),
@@ -950,6 +1085,11 @@ var DiscordConfigSchema = z.object({
950
1085
  flushInterval: z.number().int().positive().optional(),
951
1086
  maxRetries: z.number().int().nonnegative().optional(),
952
1087
  retryDelay: z.number().int().positive().optional(),
1088
+ onError: z.function().optional(),
1089
+ onDrop: z.function().optional(),
1090
+ maxQueueSize: z.number().int().positive().optional(),
1091
+ dropPolicy: z.enum(["drop-oldest", "drop-newest"]).optional(),
1092
+ requestTimeout: z.number().int().positive().optional(),
953
1093
  webhookUrl: z.string().url("webhookUrl must be a valid URL"),
954
1094
  format: z.enum(["embed", "markdown"]).optional(),
955
1095
  username: z.string().optional(),
@@ -966,6 +1106,11 @@ var TelegramConfigSchema = z.object({
966
1106
  flushInterval: z.number().int().positive().optional(),
967
1107
  maxRetries: z.number().int().nonnegative().optional(),
968
1108
  retryDelay: z.number().int().positive().optional(),
1109
+ onError: z.function().optional(),
1110
+ onDrop: z.function().optional(),
1111
+ maxQueueSize: z.number().int().positive().optional(),
1112
+ dropPolicy: z.enum(["drop-oldest", "drop-newest"]).optional(),
1113
+ requestTimeout: z.number().int().positive().optional(),
969
1114
  botToken: z.string().min(1, "botToken is required"),
970
1115
  chatId: z.union([z.string(), z.number()]),
971
1116
  parseMode: z.enum(["Markdown", "MarkdownV2", "HTML"]).optional(),
@@ -998,6 +1143,11 @@ var CloudWatchConfigSchema = z.object({
998
1143
  flushInterval: z.number().int().positive().optional(),
999
1144
  maxRetries: z.number().int().nonnegative().optional(),
1000
1145
  retryDelay: z.number().int().positive().optional(),
1146
+ onError: z.function().optional(),
1147
+ onDrop: z.function().optional(),
1148
+ maxQueueSize: z.number().int().positive().optional(),
1149
+ dropPolicy: z.enum(["drop-oldest", "drop-newest"]).optional(),
1150
+ requestTimeout: z.number().int().positive().optional(),
1001
1151
  logGroupName: z.string().min(1, "logGroupName is required"),
1002
1152
  logStreamName: LogStreamNameSchema.optional(),
1003
1153
  region: z.string().min(1, "region is required"),
@@ -1006,6 +1156,14 @@ var CloudWatchConfigSchema = z.object({
1006
1156
  createLogGroup: z.boolean().optional(),
1007
1157
  createLogStream: z.boolean().optional()
1008
1158
  });
1159
+ var RelayConfigSchema = z.object({
1160
+ apiUrl: z.string().url("apiUrl must be a valid URL"),
1161
+ token: z.string().min(1, "token is required"),
1162
+ pollInterval: z.number().int().positive().optional(),
1163
+ bufferSize: z.number().int().positive().optional(),
1164
+ reconnectDelay: z.number().int().positive().optional(),
1165
+ maxReconnectDelay: z.number().int().positive().optional()
1166
+ });
1009
1167
  var LevelConfigObjectSchema = z.object({
1010
1168
  default: LogLevelSchema,
1011
1169
  rules: z.array(LevelRuleSchema).optional()
@@ -1029,6 +1187,7 @@ var LoggerConfigSchema = z.object({
1029
1187
  discord: z.union([DiscordConfigSchema, z.array(DiscordConfigSchema)]).optional(),
1030
1188
  telegram: z.union([TelegramConfigSchema, z.array(TelegramConfigSchema)]).optional(),
1031
1189
  cloudwatch: z.union([CloudWatchConfigSchema, z.array(CloudWatchConfigSchema)]).optional(),
1190
+ relay: RelayConfigSchema.optional(),
1032
1191
  caller: z.union([z.boolean(), CallerConfigSchema]).optional(),
1033
1192
  hostname: z.string().optional(),
1034
1193
  autoShutdown: z.union([z.boolean(), AutoShutdownConfigSchema]).optional()
@@ -1276,7 +1435,10 @@ var Logger = class _Logger {
1276
1435
  this.state = state;
1277
1436
  this.context = context;
1278
1437
  }
1279
- profileTimers = /* @__PURE__ */ new Map();
1438
+ // Lazy: `.for()` creates many child loggers (hot path), most never call
1439
+ // `profile()`. Allocating a fresh Map per child wastes ~200 bytes each and
1440
+ // caused OOM in benchmarks creating millions of children.
1441
+ profileTimers;
1280
1442
  static create(config, store) {
1281
1443
  const validatedConfig = validateConfig(config);
1282
1444
  const state = createState(validatedConfig, store);
@@ -1296,12 +1458,12 @@ var Logger = class _Logger {
1296
1458
  }
1297
1459
  setLevelOverride(match, level) {
1298
1460
  assertLogLevel(level);
1299
- const key = JSON.stringify(match);
1461
+ const key = canonicalMatchKey(match);
1300
1462
  this.state.levelOverrides.set(key, { match, level });
1301
1463
  rebuildIndexes(this.state);
1302
1464
  }
1303
1465
  removeLevelOverride(match) {
1304
- const key = JSON.stringify(match);
1466
+ const key = canonicalMatchKey(match);
1305
1467
  const override = this.state.levelOverrides.get(key);
1306
1468
  if (override?.readonly) {
1307
1469
  return false;
@@ -1331,33 +1493,55 @@ var Logger = class _Logger {
1331
1493
  /**
1332
1494
  * Gracefully shutdown the logger, flushing all pending messages.
1333
1495
  * Should be called before process exit to ensure no logs are lost.
1496
+ *
1497
+ * `timeoutMs` is forwarded to each transport's `close()` as a per-transport
1498
+ * cap; it prevents a dead transport from blocking the whole shutdown.
1499
+ * Default: 5000ms. Pass `Infinity` for legacy unbounded behaviour.
1334
1500
  */
1335
- async shutdown() {
1336
- const closePromises = this.state.transports.map((transport) => transport.close());
1501
+ async shutdown(timeoutMs = 5e3) {
1502
+ const closePromises = this.state.transports.map((transport) => transport.close(timeoutMs));
1337
1503
  await Promise.all(closePromises);
1338
1504
  }
1339
1505
  // Profiling
1340
1506
  profile(id, meta) {
1341
- const existing = this.profileTimers.get(id);
1507
+ const timers = this.profileTimers ??= /* @__PURE__ */ new Map();
1508
+ const existing = timers.get(id);
1342
1509
  if (existing) {
1343
1510
  const duration = Date.now() - existing;
1344
- this.profileTimers.delete(id);
1511
+ timers.delete(id);
1345
1512
  this.info(`${id} completed`, { ...meta, durationMs: duration });
1346
1513
  } else {
1347
- this.profileTimers.set(id, Date.now());
1514
+ timers.set(id, Date.now());
1348
1515
  }
1349
1516
  }
1350
1517
  // Logging methods
1518
+ /**
1519
+ * Log an error. Supported shapes:
1520
+ * error(message: string)
1521
+ * error(message: string, meta)
1522
+ * error(error: Error | unknown)
1523
+ * error(error: Error | unknown, message: string)
1524
+ * error(error: Error | unknown, meta)
1525
+ * error(error: Error | unknown, message: string, meta)
1526
+ *
1527
+ * The first argument is treated as an error value whenever it is not a string.
1528
+ * Non-Error values (plain objects, numbers, etc. — e.g. anything caught by
1529
+ * TypeScript's `catch (err)` clause, which is typed as `unknown`) are passed
1530
+ * through `serializeError` so they produce a sensible `errorMessage` and, when
1531
+ * possible, a `stack` / HTTP diagnostic payload.
1532
+ */
1351
1533
  error(errorOrMessage, messageOrMeta, meta) {
1352
1534
  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 {
1535
+ if (typeof errorOrMessage === "string") {
1360
1536
  this.log("error", errorOrMessage, messageOrMeta);
1537
+ return;
1538
+ }
1539
+ const errorValue = errorOrMessage;
1540
+ if (typeof messageOrMeta === "string") {
1541
+ this.log("error", messageOrMeta, meta, errorValue);
1542
+ } else {
1543
+ const fallbackMessage = errorValue instanceof Error ? errorValue.message : serializeError(errorValue).errorMessage;
1544
+ this.log("error", fallbackMessage, messageOrMeta, errorValue);
1361
1545
  }
1362
1546
  }
1363
1547
  warn(message, meta) {
@@ -1392,6 +1576,10 @@ var Logger = class _Logger {
1392
1576
  if (storeContext) {
1393
1577
  Object.assign(logMeta, storeContext);
1394
1578
  }
1579
+ const overrideLevel = getGlobalOverrideLevel2(this.state, this.context, logMeta);
1580
+ if (overrideLevel !== void 0) {
1581
+ logMeta.__or = overrideLevel;
1582
+ }
1395
1583
  if (this.state.callerConfig) {
1396
1584
  const callerInfo = getCallerInfo(this.state.callerConfig, callerOffset);
1397
1585
  if (callerInfo) {