@rawnodes/logger 2.7.1 → 2.8.0

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();
@@ -416,6 +494,112 @@ var TelegramTransport = class extends BaseHttpTransport {
416
494
  return text.replace(/[&<>"']/g, (c) => entities[c] || c);
417
495
  }
418
496
  };
497
+
498
+ // src/transports/zoho-cliq.ts
499
+ var LEVEL_EMOJI3 = {
500
+ off: "",
501
+ error: "\u{1F534}",
502
+ warn: "\u{1F7E1}",
503
+ info: "\u{1F7E2}",
504
+ http: "\u{1F535}",
505
+ verbose: "\u{1F7E3}",
506
+ debug: "\u26AA",
507
+ silly: "\u26AB"
508
+ };
509
+ var DEFAULT_REQUEST_TIMEOUT_MS3 = 1e4;
510
+ var MAX_MESSAGE_LENGTH = 9500;
511
+ var ZohoCliqTransport = class extends BaseHttpTransport {
512
+ config;
513
+ endpoint;
514
+ requestTimeout;
515
+ constructor(config) {
516
+ super({
517
+ batchSize: config.batchSize ?? 20,
518
+ flushInterval: config.flushInterval ?? 2e3,
519
+ maxRetries: config.maxRetries,
520
+ retryDelay: config.retryDelay,
521
+ maxQueueSize: config.maxQueueSize,
522
+ dropPolicy: config.dropPolicy,
523
+ onDrop: config.onDrop,
524
+ onError: config.onError
525
+ });
526
+ this.config = config;
527
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS3;
528
+ this.endpoint = this.buildEndpoint();
529
+ }
530
+ buildEndpoint() {
531
+ if (this.config.webhookUrl) {
532
+ return this.config.webhookUrl;
533
+ }
534
+ const region = this.config.region ?? "eu";
535
+ const companyId = this.config.companyId;
536
+ const channel = encodeURIComponent(this.config.channel);
537
+ return `https://cliq.zoho.${region}/company/${companyId}/api/v2/channelsbyname/${channel}/message`;
538
+ }
539
+ async sendBatch(messages) {
540
+ const text = messages.map((msg) => this.formatMessage(msg)).join("\n\n---\n\n");
541
+ for (const chunk of this.splitContent(text, MAX_MESSAGE_LENGTH)) {
542
+ await this.post(chunk);
543
+ }
544
+ }
545
+ formatMessage(msg) {
546
+ const emoji = LEVEL_EMOJI3[msg.level];
547
+ const level = msg.level.toUpperCase();
548
+ const context = msg.context || "APP";
549
+ const timestamp = this.config.includeTimestamp !== false ? ` ${msg.timestamp.toISOString()}` : "";
550
+ let text = `${emoji} *${level}* [${context}]${timestamp}
551
+ ${msg.message}`;
552
+ if (this.config.includeMeta !== false && msg.meta && Object.keys(msg.meta).length > 0) {
553
+ text += "\n```\n" + JSON.stringify(msg.meta, null, 2) + "\n```";
554
+ }
555
+ return text;
556
+ }
557
+ splitContent(content, maxLength) {
558
+ if (content.length <= maxLength) return [content];
559
+ const chunks = [];
560
+ let current = content;
561
+ while (current.length > 0) {
562
+ if (current.length <= maxLength) {
563
+ chunks.push(current);
564
+ break;
565
+ }
566
+ let splitAt = current.lastIndexOf("\n", maxLength);
567
+ if (splitAt === -1 || splitAt < maxLength / 2) {
568
+ splitAt = current.lastIndexOf(" ", maxLength);
569
+ }
570
+ if (splitAt === -1 || splitAt < maxLength / 2) {
571
+ splitAt = maxLength;
572
+ }
573
+ chunks.push(current.slice(0, splitAt));
574
+ current = current.slice(splitAt).trimStart();
575
+ }
576
+ return chunks;
577
+ }
578
+ async post(text) {
579
+ const url = new URL(this.endpoint);
580
+ url.searchParams.set("zapikey", this.config.apiKey);
581
+ if (this.config.bot?.name) {
582
+ url.searchParams.set("bot_unique_name", this.config.bot.name);
583
+ }
584
+ const body = {
585
+ text,
586
+ broadcast: this.config.broadcast ?? true
587
+ };
588
+ if (this.config.bot) {
589
+ body.bot = this.config.bot;
590
+ }
591
+ const response = await fetch(url.toString(), {
592
+ method: "POST",
593
+ headers: { "Content-Type": "application/json" },
594
+ body: JSON.stringify(body),
595
+ signal: AbortSignal.timeout(this.requestTimeout)
596
+ });
597
+ if (!response.ok && response.status !== 204) {
598
+ const respText = await response.text().catch(() => "");
599
+ throw new Error(`Zoho Cliq API failed: ${response.status} ${respText}`);
600
+ }
601
+ }
602
+ };
419
603
  var instanceUuid = randomUUID().slice(0, 8);
420
604
  function formatDate() {
421
605
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -464,7 +648,11 @@ var CloudWatchTransport = class extends BaseHttpTransport {
464
648
  batchSize: config.batchSize ?? 100,
465
649
  flushInterval: config.flushInterval ?? 1e3,
466
650
  maxRetries: config.maxRetries,
467
- retryDelay: config.retryDelay
651
+ retryDelay: config.retryDelay,
652
+ maxQueueSize: config.maxQueueSize,
653
+ dropPolicy: config.dropPolicy,
654
+ onDrop: config.onDrop,
655
+ onError: config.onError
468
656
  });
469
657
  this.config = config;
470
658
  this.resolvedLogStreamName = resolveLogStreamName(config.logStreamName, configHostname);
@@ -516,7 +704,10 @@ var CloudWatchTransport = class extends BaseHttpTransport {
516
704
  async ensureInitialized() {
517
705
  if (this.initialized) return;
518
706
  if (!this.initPromise) {
519
- this.initPromise = this.initialize();
707
+ this.initPromise = this.initialize().catch((err) => {
708
+ this.initPromise = null;
709
+ throw err;
710
+ });
520
711
  }
521
712
  await this.initPromise;
522
713
  }
@@ -592,21 +783,34 @@ function getLevelName(levelNum) {
592
783
  if (levelNum >= 20) return "debug";
593
784
  return "silly";
594
785
  }
595
- function shouldPassTransport(log, level, rules, store) {
786
+ function shouldPassTransport(log, level, rules, state) {
596
787
  const logLevel = getLevelName(log.level);
788
+ const embeddedOverride = log.__or;
789
+ if (typeof embeddedOverride === "string") {
790
+ if (embeddedOverride === "off") return false;
791
+ const overrideLevel = embeddedOverride;
792
+ return LOG_LEVELS[logLevel] <= LOG_LEVELS[overrideLevel];
793
+ }
597
794
  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;
795
+ if (rules && rules.length > 0) {
796
+ const storeContext = state.store.getStore();
797
+ const logMeta = {};
798
+ for (const [key, value] of Object.entries(log)) {
799
+ if (!RESERVED_LOG_FIELDS.has(key)) {
800
+ logMeta[key] = value;
801
+ }
802
+ }
803
+ const matchingRule = rules.find((rule) => matchesContext(storeContext, context, rule.match, logMeta));
804
+ if (matchingRule) {
805
+ if (matchingRule.level === "off") return false;
806
+ return LOG_LEVELS[logLevel] <= LOG_LEVELS[matchingRule.level];
603
807
  }
604
808
  }
605
- const matchingRule = rules?.find((rule) => matchesContext(storeContext, context, rule.match, logMeta));
606
- const effectiveLevel = matchingRule?.level ?? level ?? "silly";
809
+ const effectiveLevel = level ?? "silly";
607
810
  if (effectiveLevel === "off") return false;
608
811
  return LOG_LEVELS[logLevel] <= LOG_LEVELS[effectiveLevel];
609
812
  }
813
+ var RESERVED_LOG_FIELDS = /* @__PURE__ */ new Set(["level", "time", "msg", "context", "__or"]);
610
814
  function formatLog(log, format, store) {
611
815
  const levelName = getLevelName(log.level);
612
816
  const timestamp = new Date(log.time).toISOString();
@@ -615,7 +819,7 @@ function formatLog(log, format, store) {
615
819
  const storeContext = store.getStore();
616
820
  const meta = {};
617
821
  for (const [key, value] of Object.entries(log)) {
618
- if (!["level", "time", "msg", "context"].includes(key)) {
822
+ if (!RESERVED_LOG_FIELDS.has(key)) {
619
823
  meta[key] = value;
620
824
  }
621
825
  }
@@ -664,7 +868,7 @@ function formatLog(log, format, store) {
664
868
  return `[${timestamp}] ${levelName}: ${message}
665
869
  `;
666
870
  }
667
- function createFormattedFilterStream(format, level, rules, store, destination) {
871
+ function createFormattedFilterStream(format, level, rules, state, destination) {
668
872
  return new Transform({
669
873
  transform(chunk, _encoding, callback) {
670
874
  const line = chunk.toString().trim();
@@ -677,24 +881,24 @@ function createFormattedFilterStream(format, level, rules, store, destination) {
677
881
  callback();
678
882
  return;
679
883
  }
680
- if (!shouldPassTransport(log, level, rules, store)) {
884
+ if (!shouldPassTransport(log, level, rules, state)) {
681
885
  callback();
682
886
  return;
683
887
  }
684
- const formatted = formatLog(log, format, store);
888
+ const formatted = formatLog(log, format, state.store);
685
889
  destination.write(formatted);
686
890
  callback();
687
891
  }
688
892
  });
689
893
  }
690
- function createStreams(config, store) {
894
+ function createStreams(config, state) {
691
895
  const streams = [];
692
896
  const transports = [];
693
897
  const consoleStream = createFormattedFilterStream(
694
898
  config.console.format,
695
899
  config.console.level,
696
900
  config.console.rules,
697
- store,
901
+ state,
698
902
  process.stdout
699
903
  );
700
904
  streams.push({
@@ -727,7 +931,7 @@ function createStreams(config, store) {
727
931
  fileConfig.format,
728
932
  fileConfig.level,
729
933
  fileConfig.rules,
730
- store,
934
+ state,
731
935
  rotatingStream
732
936
  );
733
937
  streams.push({
@@ -738,7 +942,7 @@ function createStreams(config, store) {
738
942
  for (const discordConfig of toArray(config.discord)) {
739
943
  const transport = new DiscordTransport(discordConfig);
740
944
  transports.push(transport);
741
- const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, store);
945
+ const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, state);
742
946
  streams.push({
743
947
  level: "trace",
744
948
  stream: discordStream
@@ -747,21 +951,40 @@ function createStreams(config, store) {
747
951
  for (const telegramConfig of toArray(config.telegram)) {
748
952
  const transport = new TelegramTransport(telegramConfig);
749
953
  transports.push(transport);
750
- const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, store);
954
+ const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, state);
751
955
  streams.push({
752
956
  level: "trace",
753
957
  stream: telegramStream
754
958
  });
755
959
  }
960
+ for (const zohoCliqConfig of toArray(config.zohoCliq)) {
961
+ const transport = new ZohoCliqTransport(zohoCliqConfig);
962
+ transports.push(transport);
963
+ const zohoStream = createHttpTransportStream(transport, zohoCliqConfig.level, zohoCliqConfig.rules, state);
964
+ streams.push({
965
+ level: "trace",
966
+ stream: zohoStream
967
+ });
968
+ }
756
969
  for (const cloudwatchConfig of toArray(config.cloudwatch)) {
757
970
  const transport = new CloudWatchTransport(cloudwatchConfig, config.hostname);
758
971
  transports.push(transport);
759
- const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, store);
972
+ const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, state);
760
973
  streams.push({
761
974
  level: "trace",
762
975
  stream: cwStream
763
976
  });
764
977
  }
978
+ if (config.relay) {
979
+ const relayStream = pino.transport({
980
+ target: "@rawnodes/logger/relay",
981
+ options: config.relay
982
+ });
983
+ streams.push({
984
+ level: "trace",
985
+ stream: relayStream
986
+ });
987
+ }
765
988
  return {
766
989
  destination: pino.multistream(streams),
767
990
  transports
@@ -771,7 +994,7 @@ function toArray(value) {
771
994
  if (!value) return [];
772
995
  return Array.isArray(value) ? value : [value];
773
996
  }
774
- function createHttpTransportStream(transport, level, rules, store) {
997
+ function createHttpTransportStream(transport, level, rules, state) {
775
998
  return new Writable({
776
999
  write(chunk, _encoding, callback) {
777
1000
  const line = chunk.toString().trim();
@@ -784,15 +1007,15 @@ function createHttpTransportStream(transport, level, rules, store) {
784
1007
  callback();
785
1008
  return;
786
1009
  }
787
- if (!shouldPassTransport(log, level, rules, store)) {
1010
+ if (!shouldPassTransport(log, level, rules, state)) {
788
1011
  callback();
789
1012
  return;
790
1013
  }
791
1014
  const levelName = getLevelName(log.level);
792
- const storeContext = store.getStore();
1015
+ const storeContext = state.store.getStore();
793
1016
  const meta = {};
794
1017
  for (const [key, value] of Object.entries(log)) {
795
- if (!["level", "time", "msg", "context"].includes(key)) {
1018
+ if (!RESERVED_LOG_FIELDS.has(key)) {
796
1019
  meta[key] = value;
797
1020
  }
798
1021
  }
@@ -841,45 +1064,53 @@ function buildIndexes(overrides) {
841
1064
  }
842
1065
  return { contextIndex, complexRules };
843
1066
  }
1067
+ function canonicalMatchKey(match) {
1068
+ const keys = Object.keys(match).sort();
1069
+ const sorted = {};
1070
+ for (const key of keys) {
1071
+ sorted[key] = match[key];
1072
+ }
1073
+ return JSON.stringify(sorted);
1074
+ }
844
1075
  function createState(config, store) {
845
1076
  const { defaultLevel, rules } = parseLevelConfig(config.level);
846
1077
  const loggerStore = store ?? new LoggerStore();
847
1078
  const levelOverrides = /* @__PURE__ */ new Map();
848
1079
  for (const rule of rules) {
849
- const key = JSON.stringify(rule.match);
1080
+ const key = canonicalMatchKey(rule.match);
850
1081
  levelOverrides.set(key, rule);
851
1082
  }
852
1083
  const { contextIndex, complexRules } = buildIndexes(levelOverrides);
853
- const { destination, transports } = createStreams(config, loggerStore);
1084
+ const state = {
1085
+ pino: null,
1086
+ store: loggerStore,
1087
+ defaultLevel,
1088
+ levelOverrides,
1089
+ contextIndex,
1090
+ complexRules,
1091
+ callerConfig: void 0,
1092
+ transports: []
1093
+ };
1094
+ const { destination, transports } = createStreams(config, state);
1095
+ state.transports = transports;
854
1096
  const options = {
855
1097
  level: "trace",
856
1098
  // Accept all, we filter in shouldLog()
857
1099
  customLevels: CUSTOM_LEVELS,
858
1100
  base: { hostname: config.hostname ?? hostname() }
859
1101
  };
860
- const pinoLogger = pino(options, destination);
861
- let callerConfig;
1102
+ state.pino = pino(options, destination);
862
1103
  if (config.caller === true) {
863
- callerConfig = {};
1104
+ state.callerConfig = {};
864
1105
  } else if (config.caller && typeof config.caller === "object") {
865
- callerConfig = config.caller;
1106
+ state.callerConfig = config.caller;
866
1107
  }
867
- return {
868
- pino: pinoLogger,
869
- store: loggerStore,
870
- defaultLevel,
871
- levelOverrides,
872
- contextIndex,
873
- complexRules,
874
- callerConfig,
875
- transports
876
- };
1108
+ return state;
877
1109
  }
878
1110
  function rebuildIndexes(state) {
879
1111
  const { contextIndex, complexRules } = buildIndexes(state.levelOverrides);
880
1112
  state.contextIndex = contextIndex;
881
- state.complexRules.length = 0;
882
- state.complexRules.push(...complexRules);
1113
+ state.complexRules = complexRules;
883
1114
  }
884
1115
  function shouldLog(state, level, context) {
885
1116
  const effectiveLevel = getEffectiveLevel(state, context);
@@ -903,6 +1134,20 @@ function matchesContext(storeContext, loggerContext, match, logMeta) {
903
1134
  const combined = { ...logMeta, ...storeContext, context: loggerContext };
904
1135
  return Object.entries(match).every(([key, value]) => combined[key] === value);
905
1136
  }
1137
+ function getGlobalOverrideLevel2(state, loggerContext, logMeta) {
1138
+ if (loggerContext) {
1139
+ const indexed = state.contextIndex.get(loggerContext);
1140
+ if (indexed && !indexed.readonly) return indexed.level;
1141
+ }
1142
+ const storeContext = state.store.getStore();
1143
+ for (const override of state.complexRules) {
1144
+ if (override.readonly) continue;
1145
+ if (matchesContext(storeContext, loggerContext, override.match, logMeta)) {
1146
+ return override.level;
1147
+ }
1148
+ }
1149
+ return void 0;
1150
+ }
906
1151
  var LogLevelSchema = z.enum([
907
1152
  "off",
908
1153
  "error",
@@ -941,7 +1186,12 @@ var HttpTransportBaseConfigSchema = z.object({
941
1186
  batchSize: z.number().int().positive().optional(),
942
1187
  flushInterval: z.number().int().positive().optional(),
943
1188
  maxRetries: z.number().int().nonnegative().optional(),
944
- retryDelay: z.number().int().positive().optional()
1189
+ retryDelay: z.number().int().positive().optional(),
1190
+ onError: z.function().optional(),
1191
+ onDrop: z.function().optional(),
1192
+ maxQueueSize: z.number().int().positive().optional(),
1193
+ dropPolicy: z.enum(["drop-oldest", "drop-newest"]).optional(),
1194
+ requestTimeout: z.number().int().positive().optional()
945
1195
  });
946
1196
  var DiscordConfigSchema = z.object({
947
1197
  level: LogLevelSchema.optional(),
@@ -950,6 +1200,11 @@ var DiscordConfigSchema = z.object({
950
1200
  flushInterval: z.number().int().positive().optional(),
951
1201
  maxRetries: z.number().int().nonnegative().optional(),
952
1202
  retryDelay: z.number().int().positive().optional(),
1203
+ onError: z.function().optional(),
1204
+ onDrop: z.function().optional(),
1205
+ maxQueueSize: z.number().int().positive().optional(),
1206
+ dropPolicy: z.enum(["drop-oldest", "drop-newest"]).optional(),
1207
+ requestTimeout: z.number().int().positive().optional(),
953
1208
  webhookUrl: z.string().url("webhookUrl must be a valid URL"),
954
1209
  format: z.enum(["embed", "markdown"]).optional(),
955
1210
  username: z.string().optional(),
@@ -966,6 +1221,11 @@ var TelegramConfigSchema = z.object({
966
1221
  flushInterval: z.number().int().positive().optional(),
967
1222
  maxRetries: z.number().int().nonnegative().optional(),
968
1223
  retryDelay: z.number().int().positive().optional(),
1224
+ onError: z.function().optional(),
1225
+ onDrop: z.function().optional(),
1226
+ maxQueueSize: z.number().int().positive().optional(),
1227
+ dropPolicy: z.enum(["drop-oldest", "drop-newest"]).optional(),
1228
+ requestTimeout: z.number().int().positive().optional(),
969
1229
  botToken: z.string().min(1, "botToken is required"),
970
1230
  chatId: z.union([z.string(), z.number()]),
971
1231
  parseMode: z.enum(["Markdown", "MarkdownV2", "HTML"]).optional(),
@@ -998,6 +1258,11 @@ var CloudWatchConfigSchema = z.object({
998
1258
  flushInterval: z.number().int().positive().optional(),
999
1259
  maxRetries: z.number().int().nonnegative().optional(),
1000
1260
  retryDelay: z.number().int().positive().optional(),
1261
+ onError: z.function().optional(),
1262
+ onDrop: z.function().optional(),
1263
+ maxQueueSize: z.number().int().positive().optional(),
1264
+ dropPolicy: z.enum(["drop-oldest", "drop-newest"]).optional(),
1265
+ requestTimeout: z.number().int().positive().optional(),
1001
1266
  logGroupName: z.string().min(1, "logGroupName is required"),
1002
1267
  logStreamName: LogStreamNameSchema.optional(),
1003
1268
  region: z.string().min(1, "region is required"),
@@ -1006,6 +1271,43 @@ var CloudWatchConfigSchema = z.object({
1006
1271
  createLogGroup: z.boolean().optional(),
1007
1272
  createLogStream: z.boolean().optional()
1008
1273
  });
1274
+ var ZohoCliqConfigBaseSchema = z.object({
1275
+ level: LogLevelSchema.optional(),
1276
+ rules: z.array(LevelRuleSchema).optional(),
1277
+ batchSize: z.number().int().positive().optional(),
1278
+ flushInterval: z.number().int().positive().optional(),
1279
+ maxRetries: z.number().int().nonnegative().optional(),
1280
+ retryDelay: z.number().int().positive().optional(),
1281
+ onError: z.function().optional(),
1282
+ onDrop: z.function().optional(),
1283
+ maxQueueSize: z.number().int().positive().optional(),
1284
+ dropPolicy: z.enum(["drop-oldest", "drop-newest"]).optional(),
1285
+ requestTimeout: z.number().int().positive().optional(),
1286
+ webhookUrl: z.string().url().optional(),
1287
+ companyId: z.string().min(1).optional(),
1288
+ channel: z.string().min(1).optional(),
1289
+ region: z.string().min(1).optional(),
1290
+ apiKey: z.string().min(1, "apiKey is required"),
1291
+ broadcast: z.boolean().optional(),
1292
+ bot: z.object({
1293
+ name: z.string().min(1),
1294
+ image: z.string().url().optional()
1295
+ }).optional(),
1296
+ includeTimestamp: z.boolean().optional(),
1297
+ includeMeta: z.boolean().optional()
1298
+ });
1299
+ var ZohoCliqConfigSchema = ZohoCliqConfigBaseSchema.refine(
1300
+ (cfg) => Boolean(cfg.webhookUrl) || Boolean(cfg.companyId) && Boolean(cfg.channel),
1301
+ { message: "Either webhookUrl or both companyId and channel must be provided" }
1302
+ );
1303
+ var RelayConfigSchema = z.object({
1304
+ apiUrl: z.string().url("apiUrl must be a valid URL"),
1305
+ token: z.string().min(1, "token is required"),
1306
+ pollInterval: z.number().int().positive().optional(),
1307
+ bufferSize: z.number().int().positive().optional(),
1308
+ reconnectDelay: z.number().int().positive().optional(),
1309
+ maxReconnectDelay: z.number().int().positive().optional()
1310
+ });
1009
1311
  var LevelConfigObjectSchema = z.object({
1010
1312
  default: LogLevelSchema,
1011
1313
  rules: z.array(LevelRuleSchema).optional()
@@ -1029,6 +1331,8 @@ var LoggerConfigSchema = z.object({
1029
1331
  discord: z.union([DiscordConfigSchema, z.array(DiscordConfigSchema)]).optional(),
1030
1332
  telegram: z.union([TelegramConfigSchema, z.array(TelegramConfigSchema)]).optional(),
1031
1333
  cloudwatch: z.union([CloudWatchConfigSchema, z.array(CloudWatchConfigSchema)]).optional(),
1334
+ zohoCliq: z.union([ZohoCliqConfigSchema, z.array(ZohoCliqConfigSchema)]).optional(),
1335
+ relay: RelayConfigSchema.optional(),
1032
1336
  caller: z.union([z.boolean(), CallerConfigSchema]).optional(),
1033
1337
  hostname: z.string().optional(),
1034
1338
  autoShutdown: z.union([z.boolean(), AutoShutdownConfigSchema]).optional()
@@ -1276,7 +1580,10 @@ var Logger = class _Logger {
1276
1580
  this.state = state;
1277
1581
  this.context = context;
1278
1582
  }
1279
- profileTimers = /* @__PURE__ */ new Map();
1583
+ // Lazy: `.for()` creates many child loggers (hot path), most never call
1584
+ // `profile()`. Allocating a fresh Map per child wastes ~200 bytes each and
1585
+ // caused OOM in benchmarks creating millions of children.
1586
+ profileTimers;
1280
1587
  static create(config, store) {
1281
1588
  const validatedConfig = validateConfig(config);
1282
1589
  const state = createState(validatedConfig, store);
@@ -1296,12 +1603,12 @@ var Logger = class _Logger {
1296
1603
  }
1297
1604
  setLevelOverride(match, level) {
1298
1605
  assertLogLevel(level);
1299
- const key = JSON.stringify(match);
1606
+ const key = canonicalMatchKey(match);
1300
1607
  this.state.levelOverrides.set(key, { match, level });
1301
1608
  rebuildIndexes(this.state);
1302
1609
  }
1303
1610
  removeLevelOverride(match) {
1304
- const key = JSON.stringify(match);
1611
+ const key = canonicalMatchKey(match);
1305
1612
  const override = this.state.levelOverrides.get(key);
1306
1613
  if (override?.readonly) {
1307
1614
  return false;
@@ -1331,33 +1638,55 @@ var Logger = class _Logger {
1331
1638
  /**
1332
1639
  * Gracefully shutdown the logger, flushing all pending messages.
1333
1640
  * Should be called before process exit to ensure no logs are lost.
1641
+ *
1642
+ * `timeoutMs` is forwarded to each transport's `close()` as a per-transport
1643
+ * cap; it prevents a dead transport from blocking the whole shutdown.
1644
+ * Default: 5000ms. Pass `Infinity` for legacy unbounded behaviour.
1334
1645
  */
1335
- async shutdown() {
1336
- const closePromises = this.state.transports.map((transport) => transport.close());
1646
+ async shutdown(timeoutMs = 5e3) {
1647
+ const closePromises = this.state.transports.map((transport) => transport.close(timeoutMs));
1337
1648
  await Promise.all(closePromises);
1338
1649
  }
1339
1650
  // Profiling
1340
1651
  profile(id, meta) {
1341
- const existing = this.profileTimers.get(id);
1652
+ const timers = this.profileTimers ??= /* @__PURE__ */ new Map();
1653
+ const existing = timers.get(id);
1342
1654
  if (existing) {
1343
1655
  const duration = Date.now() - existing;
1344
- this.profileTimers.delete(id);
1656
+ timers.delete(id);
1345
1657
  this.info(`${id} completed`, { ...meta, durationMs: duration });
1346
1658
  } else {
1347
- this.profileTimers.set(id, Date.now());
1659
+ timers.set(id, Date.now());
1348
1660
  }
1349
1661
  }
1350
1662
  // Logging methods
1663
+ /**
1664
+ * Log an error. Supported shapes:
1665
+ * error(message: string)
1666
+ * error(message: string, meta)
1667
+ * error(error: Error | unknown)
1668
+ * error(error: Error | unknown, message: string)
1669
+ * error(error: Error | unknown, meta)
1670
+ * error(error: Error | unknown, message: string, meta)
1671
+ *
1672
+ * The first argument is treated as an error value whenever it is not a string.
1673
+ * Non-Error values (plain objects, numbers, etc. — e.g. anything caught by
1674
+ * TypeScript's `catch (err)` clause, which is typed as `unknown`) are passed
1675
+ * through `serializeError` so they produce a sensible `errorMessage` and, when
1676
+ * possible, a `stack` / HTTP diagnostic payload.
1677
+ */
1351
1678
  error(errorOrMessage, messageOrMeta, meta) {
1352
1679
  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 {
1680
+ if (typeof errorOrMessage === "string") {
1360
1681
  this.log("error", errorOrMessage, messageOrMeta);
1682
+ return;
1683
+ }
1684
+ const errorValue = errorOrMessage;
1685
+ if (typeof messageOrMeta === "string") {
1686
+ this.log("error", messageOrMeta, meta, errorValue);
1687
+ } else {
1688
+ const fallbackMessage = errorValue instanceof Error ? errorValue.message : serializeError(errorValue).errorMessage;
1689
+ this.log("error", fallbackMessage, messageOrMeta, errorValue);
1361
1690
  }
1362
1691
  }
1363
1692
  warn(message, meta) {
@@ -1392,6 +1721,10 @@ var Logger = class _Logger {
1392
1721
  if (storeContext) {
1393
1722
  Object.assign(logMeta, storeContext);
1394
1723
  }
1724
+ const overrideLevel = getGlobalOverrideLevel2(this.state, this.context, logMeta);
1725
+ if (overrideLevel !== void 0) {
1726
+ logMeta.__or = overrideLevel;
1727
+ }
1395
1728
  if (this.state.callerConfig) {
1396
1729
  const callerInfo = getCallerInfo(this.state.callerConfig, callerOffset);
1397
1730
  if (callerInfo) {
@@ -1660,6 +1993,6 @@ function formatLogfmt(data) {
1660
1993
  return Object.entries(flattened).filter(([, value]) => value !== void 0 && value !== null).map(([key, value]) => `${key}=${formatLogfmtValue(value)}`).join(" ");
1661
1994
  }
1662
1995
 
1663
- export { AutoShutdownConfigSchema, BaseHttpTransport, CallerConfigSchema, CloudWatchConfigSchema, CloudWatchTransport, ConsoleConfigSchema, DiscordConfigSchema, DiscordTransport, FileConfigSchema, HttpTransportBaseConfigSchema, LOG_LEVELS, LevelConfigObjectSchema, LevelConfigSchema, LevelRuleSchema, LogFormatSchema, LogLevelSchema, LogStreamNameSchema, LogStreamPatternConfigSchema, LogStreamPatternSchema, LogStreamTemplateConfigSchema, Logger, LoggerConfigSchema, LoggerStore, MessageBuffer, TelegramConfigSchema, TelegramTransport, assertLogLevel, createMasker, createSingletonLogger, extractRequestId, flattenObject, formatCallerInfo, formatLogfmt, formatLogfmtValue, generateRequestId, getCallerInfo, getOrGenerateRequestId, isValidLogLevel, maskSecrets, matchesContext, measureAsync, measureSync, registerShutdown, safeValidateConfig, serializeError, validateConfig };
1996
+ export { AutoShutdownConfigSchema, BaseHttpTransport, CallerConfigSchema, CloudWatchConfigSchema, CloudWatchTransport, ConsoleConfigSchema, DiscordConfigSchema, DiscordTransport, FileConfigSchema, HttpTransportBaseConfigSchema, LOG_LEVELS, LevelConfigObjectSchema, LevelConfigSchema, LevelRuleSchema, LogFormatSchema, LogLevelSchema, LogStreamNameSchema, LogStreamPatternConfigSchema, LogStreamPatternSchema, LogStreamTemplateConfigSchema, Logger, LoggerConfigSchema, LoggerStore, MessageBuffer, RelayConfigSchema, TelegramConfigSchema, TelegramTransport, ZohoCliqConfigSchema, ZohoCliqTransport, assertLogLevel, createMasker, createSingletonLogger, extractRequestId, flattenObject, formatCallerInfo, formatLogfmt, formatLogfmtValue, generateRequestId, getCallerInfo, getOrGenerateRequestId, isValidLogLevel, maskSecrets, matchesContext, measureAsync, measureSync, registerShutdown, safeValidateConfig, serializeError, validateConfig };
1664
1997
  //# sourceMappingURL=index.mjs.map
1665
1998
  //# sourceMappingURL=index.mjs.map