@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.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,15 +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 matchingRule = rules?.find((rule) => matchesContext(storeContext, context, rule.match));
600
- const effectiveLevel = matchingRule?.level ?? level ?? "silly";
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];
701
+ }
702
+ }
703
+ const effectiveLevel = level ?? "silly";
601
704
  if (effectiveLevel === "off") return false;
602
705
  return LOG_LEVELS[logLevel] <= LOG_LEVELS[effectiveLevel];
603
706
  }
707
+ var RESERVED_LOG_FIELDS = /* @__PURE__ */ new Set(["level", "time", "msg", "context", "__or"]);
604
708
  function formatLog(log, format, store) {
605
709
  const levelName = getLevelName(log.level);
606
710
  const timestamp = new Date(log.time).toISOString();
@@ -609,7 +713,7 @@ function formatLog(log, format, store) {
609
713
  const storeContext = store.getStore();
610
714
  const meta = {};
611
715
  for (const [key, value] of Object.entries(log)) {
612
- if (!["level", "time", "msg", "context"].includes(key)) {
716
+ if (!RESERVED_LOG_FIELDS.has(key)) {
613
717
  meta[key] = value;
614
718
  }
615
719
  }
@@ -658,7 +762,7 @@ function formatLog(log, format, store) {
658
762
  return `[${timestamp}] ${levelName}: ${message}
659
763
  `;
660
764
  }
661
- function createFormattedFilterStream(format, level, rules, store, destination) {
765
+ function createFormattedFilterStream(format, level, rules, state, destination) {
662
766
  return new Transform({
663
767
  transform(chunk, _encoding, callback) {
664
768
  const line = chunk.toString().trim();
@@ -671,24 +775,24 @@ function createFormattedFilterStream(format, level, rules, store, destination) {
671
775
  callback();
672
776
  return;
673
777
  }
674
- if (!shouldPassTransport(log, level, rules, store)) {
778
+ if (!shouldPassTransport(log, level, rules, state)) {
675
779
  callback();
676
780
  return;
677
781
  }
678
- const formatted = formatLog(log, format, store);
782
+ const formatted = formatLog(log, format, state.store);
679
783
  destination.write(formatted);
680
784
  callback();
681
785
  }
682
786
  });
683
787
  }
684
- function createStreams(config, store) {
788
+ function createStreams(config, state) {
685
789
  const streams = [];
686
790
  const transports = [];
687
791
  const consoleStream = createFormattedFilterStream(
688
792
  config.console.format,
689
793
  config.console.level,
690
794
  config.console.rules,
691
- store,
795
+ state,
692
796
  process.stdout
693
797
  );
694
798
  streams.push({
@@ -721,7 +825,7 @@ function createStreams(config, store) {
721
825
  fileConfig.format,
722
826
  fileConfig.level,
723
827
  fileConfig.rules,
724
- store,
828
+ state,
725
829
  rotatingStream
726
830
  );
727
831
  streams.push({
@@ -732,7 +836,7 @@ function createStreams(config, store) {
732
836
  for (const discordConfig of toArray(config.discord)) {
733
837
  const transport = new DiscordTransport(discordConfig);
734
838
  transports.push(transport);
735
- const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, store);
839
+ const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, state);
736
840
  streams.push({
737
841
  level: "trace",
738
842
  stream: discordStream
@@ -741,7 +845,7 @@ function createStreams(config, store) {
741
845
  for (const telegramConfig of toArray(config.telegram)) {
742
846
  const transport = new TelegramTransport(telegramConfig);
743
847
  transports.push(transport);
744
- const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, store);
848
+ const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, state);
745
849
  streams.push({
746
850
  level: "trace",
747
851
  stream: telegramStream
@@ -750,12 +854,22 @@ function createStreams(config, store) {
750
854
  for (const cloudwatchConfig of toArray(config.cloudwatch)) {
751
855
  const transport = new CloudWatchTransport(cloudwatchConfig, config.hostname);
752
856
  transports.push(transport);
753
- const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, store);
857
+ const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, state);
754
858
  streams.push({
755
859
  level: "trace",
756
860
  stream: cwStream
757
861
  });
758
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
+ }
759
873
  return {
760
874
  destination: pino.multistream(streams),
761
875
  transports
@@ -765,7 +879,7 @@ function toArray(value) {
765
879
  if (!value) return [];
766
880
  return Array.isArray(value) ? value : [value];
767
881
  }
768
- function createHttpTransportStream(transport, level, rules, store) {
882
+ function createHttpTransportStream(transport, level, rules, state) {
769
883
  return new Writable({
770
884
  write(chunk, _encoding, callback) {
771
885
  const line = chunk.toString().trim();
@@ -778,15 +892,15 @@ function createHttpTransportStream(transport, level, rules, store) {
778
892
  callback();
779
893
  return;
780
894
  }
781
- if (!shouldPassTransport(log, level, rules, store)) {
895
+ if (!shouldPassTransport(log, level, rules, state)) {
782
896
  callback();
783
897
  return;
784
898
  }
785
899
  const levelName = getLevelName(log.level);
786
- const storeContext = store.getStore();
900
+ const storeContext = state.store.getStore();
787
901
  const meta = {};
788
902
  for (const [key, value] of Object.entries(log)) {
789
- if (!["level", "time", "msg", "context"].includes(key)) {
903
+ if (!RESERVED_LOG_FIELDS.has(key)) {
790
904
  meta[key] = value;
791
905
  }
792
906
  }
@@ -835,45 +949,53 @@ function buildIndexes(overrides) {
835
949
  }
836
950
  return { contextIndex, complexRules };
837
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
+ }
838
960
  function createState(config, store) {
839
961
  const { defaultLevel, rules } = parseLevelConfig(config.level);
840
962
  const loggerStore = store ?? new LoggerStore();
841
963
  const levelOverrides = /* @__PURE__ */ new Map();
842
964
  for (const rule of rules) {
843
- const key = JSON.stringify(rule.match);
965
+ const key = canonicalMatchKey(rule.match);
844
966
  levelOverrides.set(key, rule);
845
967
  }
846
968
  const { contextIndex, complexRules } = buildIndexes(levelOverrides);
847
- 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;
848
981
  const options = {
849
982
  level: "trace",
850
983
  // Accept all, we filter in shouldLog()
851
984
  customLevels: CUSTOM_LEVELS,
852
985
  base: { hostname: config.hostname ?? hostname() }
853
986
  };
854
- const pinoLogger = pino(options, destination);
855
- let callerConfig;
987
+ state.pino = pino(options, destination);
856
988
  if (config.caller === true) {
857
- callerConfig = {};
989
+ state.callerConfig = {};
858
990
  } else if (config.caller && typeof config.caller === "object") {
859
- callerConfig = config.caller;
991
+ state.callerConfig = config.caller;
860
992
  }
861
- return {
862
- pino: pinoLogger,
863
- store: loggerStore,
864
- defaultLevel,
865
- levelOverrides,
866
- contextIndex,
867
- complexRules,
868
- callerConfig,
869
- transports
870
- };
993
+ return state;
871
994
  }
872
995
  function rebuildIndexes(state) {
873
996
  const { contextIndex, complexRules } = buildIndexes(state.levelOverrides);
874
997
  state.contextIndex = contextIndex;
875
- state.complexRules.length = 0;
876
- state.complexRules.push(...complexRules);
998
+ state.complexRules = complexRules;
877
999
  }
878
1000
  function shouldLog(state, level, context) {
879
1001
  const effectiveLevel = getEffectiveLevel(state, context);
@@ -893,10 +1015,24 @@ function getEffectiveLevel(state, loggerContext) {
893
1015
  }
894
1016
  return state.defaultLevel;
895
1017
  }
896
- function matchesContext(storeContext, loggerContext, match) {
897
- const combined = { ...storeContext, context: loggerContext };
1018
+ function matchesContext(storeContext, loggerContext, match, logMeta) {
1019
+ const combined = { ...logMeta, ...storeContext, context: loggerContext };
898
1020
  return Object.entries(match).every(([key, value]) => combined[key] === value);
899
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
+ }
900
1036
  var LogLevelSchema = z.enum([
901
1037
  "off",
902
1038
  "error",
@@ -935,7 +1071,12 @@ var HttpTransportBaseConfigSchema = z.object({
935
1071
  batchSize: z.number().int().positive().optional(),
936
1072
  flushInterval: z.number().int().positive().optional(),
937
1073
  maxRetries: z.number().int().nonnegative().optional(),
938
- 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()
939
1080
  });
940
1081
  var DiscordConfigSchema = z.object({
941
1082
  level: LogLevelSchema.optional(),
@@ -944,6 +1085,11 @@ var DiscordConfigSchema = z.object({
944
1085
  flushInterval: z.number().int().positive().optional(),
945
1086
  maxRetries: z.number().int().nonnegative().optional(),
946
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(),
947
1093
  webhookUrl: z.string().url("webhookUrl must be a valid URL"),
948
1094
  format: z.enum(["embed", "markdown"]).optional(),
949
1095
  username: z.string().optional(),
@@ -960,6 +1106,11 @@ var TelegramConfigSchema = z.object({
960
1106
  flushInterval: z.number().int().positive().optional(),
961
1107
  maxRetries: z.number().int().nonnegative().optional(),
962
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(),
963
1114
  botToken: z.string().min(1, "botToken is required"),
964
1115
  chatId: z.union([z.string(), z.number()]),
965
1116
  parseMode: z.enum(["Markdown", "MarkdownV2", "HTML"]).optional(),
@@ -992,6 +1143,11 @@ var CloudWatchConfigSchema = z.object({
992
1143
  flushInterval: z.number().int().positive().optional(),
993
1144
  maxRetries: z.number().int().nonnegative().optional(),
994
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(),
995
1151
  logGroupName: z.string().min(1, "logGroupName is required"),
996
1152
  logStreamName: LogStreamNameSchema.optional(),
997
1153
  region: z.string().min(1, "region is required"),
@@ -1000,6 +1156,14 @@ var CloudWatchConfigSchema = z.object({
1000
1156
  createLogGroup: z.boolean().optional(),
1001
1157
  createLogStream: z.boolean().optional()
1002
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
+ });
1003
1167
  var LevelConfigObjectSchema = z.object({
1004
1168
  default: LogLevelSchema,
1005
1169
  rules: z.array(LevelRuleSchema).optional()
@@ -1023,6 +1187,7 @@ var LoggerConfigSchema = z.object({
1023
1187
  discord: z.union([DiscordConfigSchema, z.array(DiscordConfigSchema)]).optional(),
1024
1188
  telegram: z.union([TelegramConfigSchema, z.array(TelegramConfigSchema)]).optional(),
1025
1189
  cloudwatch: z.union([CloudWatchConfigSchema, z.array(CloudWatchConfigSchema)]).optional(),
1190
+ relay: RelayConfigSchema.optional(),
1026
1191
  caller: z.union([z.boolean(), CallerConfigSchema]).optional(),
1027
1192
  hostname: z.string().optional(),
1028
1193
  autoShutdown: z.union([z.boolean(), AutoShutdownConfigSchema]).optional()
@@ -1270,7 +1435,10 @@ var Logger = class _Logger {
1270
1435
  this.state = state;
1271
1436
  this.context = context;
1272
1437
  }
1273
- 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;
1274
1442
  static create(config, store) {
1275
1443
  const validatedConfig = validateConfig(config);
1276
1444
  const state = createState(validatedConfig, store);
@@ -1290,12 +1458,12 @@ var Logger = class _Logger {
1290
1458
  }
1291
1459
  setLevelOverride(match, level) {
1292
1460
  assertLogLevel(level);
1293
- const key = JSON.stringify(match);
1461
+ const key = canonicalMatchKey(match);
1294
1462
  this.state.levelOverrides.set(key, { match, level });
1295
1463
  rebuildIndexes(this.state);
1296
1464
  }
1297
1465
  removeLevelOverride(match) {
1298
- const key = JSON.stringify(match);
1466
+ const key = canonicalMatchKey(match);
1299
1467
  const override = this.state.levelOverrides.get(key);
1300
1468
  if (override?.readonly) {
1301
1469
  return false;
@@ -1325,33 +1493,55 @@ var Logger = class _Logger {
1325
1493
  /**
1326
1494
  * Gracefully shutdown the logger, flushing all pending messages.
1327
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.
1328
1500
  */
1329
- async shutdown() {
1330
- 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));
1331
1503
  await Promise.all(closePromises);
1332
1504
  }
1333
1505
  // Profiling
1334
1506
  profile(id, meta) {
1335
- const existing = this.profileTimers.get(id);
1507
+ const timers = this.profileTimers ??= /* @__PURE__ */ new Map();
1508
+ const existing = timers.get(id);
1336
1509
  if (existing) {
1337
1510
  const duration = Date.now() - existing;
1338
- this.profileTimers.delete(id);
1511
+ timers.delete(id);
1339
1512
  this.info(`${id} completed`, { ...meta, durationMs: duration });
1340
1513
  } else {
1341
- this.profileTimers.set(id, Date.now());
1514
+ timers.set(id, Date.now());
1342
1515
  }
1343
1516
  }
1344
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
+ */
1345
1533
  error(errorOrMessage, messageOrMeta, meta) {
1346
1534
  if (!shouldLog(this.state, "error", this.context)) return;
1347
- if (errorOrMessage instanceof Error) {
1348
- if (typeof messageOrMeta === "string") {
1349
- this.log("error", messageOrMeta, meta, errorOrMessage);
1350
- } else {
1351
- this.log("error", errorOrMessage.message, messageOrMeta, errorOrMessage);
1352
- }
1353
- } else {
1535
+ if (typeof errorOrMessage === "string") {
1354
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);
1355
1545
  }
1356
1546
  }
1357
1547
  warn(message, meta) {
@@ -1386,6 +1576,10 @@ var Logger = class _Logger {
1386
1576
  if (storeContext) {
1387
1577
  Object.assign(logMeta, storeContext);
1388
1578
  }
1579
+ const overrideLevel = getGlobalOverrideLevel2(this.state, this.context, logMeta);
1580
+ if (overrideLevel !== void 0) {
1581
+ logMeta.__or = overrideLevel;
1582
+ }
1389
1583
  if (this.state.callerConfig) {
1390
1584
  const callerInfo = getCallerInfo(this.state.callerConfig, callerOffset);
1391
1585
  if (callerInfo) {