@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.js CHANGED
@@ -6,7 +6,6 @@ var async_hooks = require('async_hooks');
6
6
  var stream = require('stream');
7
7
  var promises = require('fs/promises');
8
8
  var rotatingFileStream = require('rotating-file-stream');
9
- var events = require('events');
10
9
  var clientCloudwatchLogs = require('@aws-sdk/client-cloudwatch-logs');
11
10
  var crypto = require('crypto');
12
11
  var zod = require('zod');
@@ -56,8 +55,24 @@ var MessageBuffer = class {
56
55
  timer = null;
57
56
  flushing = false;
58
57
  closed = false;
58
+ droppedCount = 0;
59
59
  add(message) {
60
60
  if (this.closed) return;
61
+ const limit = this.options.maxQueueSize;
62
+ if (limit !== void 0 && this.queue.length >= limit) {
63
+ const policy = this.options.dropPolicy ?? "drop-oldest";
64
+ const dropped = policy === "drop-oldest" ? [this.queue.shift()] : [message];
65
+ this.droppedCount += 1;
66
+ if (this.options.onDrop) {
67
+ try {
68
+ this.options.onDrop(dropped);
69
+ } catch {
70
+ }
71
+ }
72
+ if (policy === "drop-newest") {
73
+ return;
74
+ }
75
+ }
61
76
  this.queue.push(message);
62
77
  if (this.queue.length >= this.options.batchSize) {
63
78
  void this.flush();
@@ -81,13 +96,34 @@ var MessageBuffer = class {
81
96
  }
82
97
  }
83
98
  }
84
- async close() {
99
+ /**
100
+ * Stops accepting new messages and attempts to flush what's already buffered.
101
+ * Returns when the queue is drained OR when `timeoutMs` elapses. A timeout
102
+ * is essential during graceful shutdown — without it, a dead transport would
103
+ * block process exit indefinitely, and orchestrators like Kubernetes would
104
+ * escalate to SIGKILL after their grace period.
105
+ *
106
+ * Default timeout: 5 seconds. Pass `Infinity` to preserve legacy behaviour.
107
+ */
108
+ async close(timeoutMs = 5e3) {
85
109
  this.closed = true;
86
110
  this.clearTimer();
111
+ const deadline = timeoutMs === Infinity ? Infinity : Date.now() + timeoutMs;
87
112
  while (this.queue.length > 0) {
113
+ if (Date.now() >= deadline) {
114
+ return;
115
+ }
88
116
  await this.flush();
89
117
  }
90
118
  }
119
+ /** Current number of messages waiting in the queue. */
120
+ get size() {
121
+ return this.queue.length;
122
+ }
123
+ /** Total number of messages dropped due to queue overflow since creation. */
124
+ get droppedTotal() {
125
+ return this.droppedCount;
126
+ }
91
127
  scheduleFlush() {
92
128
  if (this.timer || this.closed) return;
93
129
  this.timer = setTimeout(() => {
@@ -128,15 +164,19 @@ var DEFAULT_OPTIONS = {
128
164
  maxRetries: 3,
129
165
  retryDelay: 1e3
130
166
  };
131
- var BaseHttpTransport = class extends events.EventEmitter {
167
+ var BaseHttpTransport = class {
132
168
  buffer;
169
+ onErrorCallback;
133
170
  constructor(opts = {}) {
134
- super();
171
+ this.onErrorCallback = opts.onError;
135
172
  this.buffer = new MessageBuffer({
136
173
  batchSize: opts.batchSize ?? DEFAULT_OPTIONS.batchSize,
137
174
  flushInterval: opts.flushInterval ?? DEFAULT_OPTIONS.flushInterval,
138
175
  maxRetries: opts.maxRetries ?? DEFAULT_OPTIONS.maxRetries,
139
176
  retryDelay: opts.retryDelay ?? DEFAULT_OPTIONS.retryDelay,
177
+ maxQueueSize: opts.maxQueueSize,
178
+ dropPolicy: opts.dropPolicy,
179
+ onDrop: opts.onDrop,
140
180
  onFlush: this.sendBatch.bind(this),
141
181
  onError: this.handleError.bind(this)
142
182
  });
@@ -146,8 +186,20 @@ var BaseHttpTransport = class extends events.EventEmitter {
146
186
  this.buffer.add(message);
147
187
  callback();
148
188
  }
149
- close() {
150
- return this.buffer.close();
189
+ /**
190
+ * Stop accepting new messages and flush what's buffered. `timeoutMs` caps
191
+ * how long the flush may take; after that the remaining queue is discarded
192
+ * so shutdown can complete. Default 5000ms.
193
+ */
194
+ close(timeoutMs) {
195
+ return this.buffer.close(timeoutMs);
196
+ }
197
+ /** Current buffered message count and total drops since creation. */
198
+ getMetrics() {
199
+ return {
200
+ queueSize: this.buffer.size,
201
+ droppedTotal: this.buffer.droppedTotal
202
+ };
151
203
  }
152
204
  transformMessage(info) {
153
205
  const { level, message, timestamp, context, ...meta } = info;
@@ -160,11 +212,21 @@ var BaseHttpTransport = class extends events.EventEmitter {
160
212
  };
161
213
  }
162
214
  handleError(error, messages) {
215
+ if (this.onErrorCallback) {
216
+ try {
217
+ this.onErrorCallback(error, messages);
218
+ return;
219
+ } catch (callbackError) {
220
+ console.error(
221
+ `[${this.constructor.name}] onError callback threw:`,
222
+ callbackError instanceof Error ? callbackError.message : callbackError
223
+ );
224
+ }
225
+ }
163
226
  console.error(
164
227
  `[${this.constructor.name}] Failed to send ${messages.length} messages:`,
165
228
  error.message
166
229
  );
167
- this.emit("error", error);
168
230
  }
169
231
  };
170
232
 
@@ -189,16 +251,23 @@ var LEVEL_EMOJI = {
189
251
  debug: "\u26AA",
190
252
  silly: "\u26AB"
191
253
  };
254
+ var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
192
255
  var DiscordTransport = class extends BaseHttpTransport {
193
256
  config;
257
+ requestTimeout;
194
258
  constructor(config) {
195
259
  super({
196
260
  batchSize: config.batchSize ?? 10,
197
261
  flushInterval: config.flushInterval ?? 2e3,
198
262
  maxRetries: config.maxRetries,
199
- retryDelay: config.retryDelay
263
+ retryDelay: config.retryDelay,
264
+ maxQueueSize: config.maxQueueSize,
265
+ dropPolicy: config.dropPolicy,
266
+ onDrop: config.onDrop,
267
+ onError: config.onError
200
268
  });
201
269
  this.config = config;
270
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
202
271
  }
203
272
  async sendBatch(messages) {
204
273
  if (this.config.format === "markdown") {
@@ -302,7 +371,8 @@ ${msg.message}`;
302
371
  const response = await fetch(this.config.webhookUrl, {
303
372
  method: "POST",
304
373
  headers: { "Content-Type": "application/json" },
305
- body: JSON.stringify(payload)
374
+ body: JSON.stringify(payload),
375
+ signal: AbortSignal.timeout(this.requestTimeout)
306
376
  });
307
377
  if (!response.ok) {
308
378
  const text = await response.text();
@@ -329,18 +399,25 @@ var LEVEL_EMOJI2 = {
329
399
  debug: "\u26AA",
330
400
  silly: "\u26AB"
331
401
  };
402
+ var DEFAULT_REQUEST_TIMEOUT_MS2 = 1e4;
332
403
  var TelegramTransport = class extends BaseHttpTransport {
333
404
  config;
334
405
  apiUrl;
406
+ requestTimeout;
335
407
  constructor(config) {
336
408
  super({
337
409
  batchSize: config.batchSize ?? 20,
338
410
  flushInterval: config.flushInterval ?? 1e3,
339
411
  maxRetries: config.maxRetries,
340
- retryDelay: config.retryDelay
412
+ retryDelay: config.retryDelay,
413
+ maxQueueSize: config.maxQueueSize,
414
+ dropPolicy: config.dropPolicy,
415
+ onDrop: config.onDrop,
416
+ onError: config.onError
341
417
  });
342
418
  this.config = config;
343
419
  this.apiUrl = `https://api.telegram.org/bot${config.botToken}`;
420
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS2;
344
421
  }
345
422
  async sendBatch(messages) {
346
423
  const text = this.formatBatchMessage(messages);
@@ -401,7 +478,8 @@ var TelegramTransport = class extends BaseHttpTransport {
401
478
  const response = await fetch(`${this.apiUrl}/sendMessage`, {
402
479
  method: "POST",
403
480
  headers: { "Content-Type": "application/json" },
404
- body: JSON.stringify(body)
481
+ body: JSON.stringify(body),
482
+ signal: AbortSignal.timeout(this.requestTimeout)
405
483
  });
406
484
  if (!response.ok) {
407
485
  const result = await response.json();
@@ -422,6 +500,112 @@ var TelegramTransport = class extends BaseHttpTransport {
422
500
  return text.replace(/[&<>"']/g, (c) => entities[c] || c);
423
501
  }
424
502
  };
503
+
504
+ // src/transports/zoho-cliq.ts
505
+ var LEVEL_EMOJI3 = {
506
+ off: "",
507
+ error: "\u{1F534}",
508
+ warn: "\u{1F7E1}",
509
+ info: "\u{1F7E2}",
510
+ http: "\u{1F535}",
511
+ verbose: "\u{1F7E3}",
512
+ debug: "\u26AA",
513
+ silly: "\u26AB"
514
+ };
515
+ var DEFAULT_REQUEST_TIMEOUT_MS3 = 1e4;
516
+ var MAX_MESSAGE_LENGTH = 9500;
517
+ var ZohoCliqTransport = class extends BaseHttpTransport {
518
+ config;
519
+ endpoint;
520
+ requestTimeout;
521
+ constructor(config) {
522
+ super({
523
+ batchSize: config.batchSize ?? 20,
524
+ flushInterval: config.flushInterval ?? 2e3,
525
+ maxRetries: config.maxRetries,
526
+ retryDelay: config.retryDelay,
527
+ maxQueueSize: config.maxQueueSize,
528
+ dropPolicy: config.dropPolicy,
529
+ onDrop: config.onDrop,
530
+ onError: config.onError
531
+ });
532
+ this.config = config;
533
+ this.requestTimeout = config.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT_MS3;
534
+ this.endpoint = this.buildEndpoint();
535
+ }
536
+ buildEndpoint() {
537
+ if (this.config.webhookUrl) {
538
+ return this.config.webhookUrl;
539
+ }
540
+ const region = this.config.region ?? "eu";
541
+ const companyId = this.config.companyId;
542
+ const channel = encodeURIComponent(this.config.channel);
543
+ return `https://cliq.zoho.${region}/company/${companyId}/api/v2/channelsbyname/${channel}/message`;
544
+ }
545
+ async sendBatch(messages) {
546
+ const text = messages.map((msg) => this.formatMessage(msg)).join("\n\n---\n\n");
547
+ for (const chunk of this.splitContent(text, MAX_MESSAGE_LENGTH)) {
548
+ await this.post(chunk);
549
+ }
550
+ }
551
+ formatMessage(msg) {
552
+ const emoji = LEVEL_EMOJI3[msg.level];
553
+ const level = msg.level.toUpperCase();
554
+ const context = msg.context || "APP";
555
+ const timestamp = this.config.includeTimestamp !== false ? ` ${msg.timestamp.toISOString()}` : "";
556
+ let text = `${emoji} *${level}* [${context}]${timestamp}
557
+ ${msg.message}`;
558
+ if (this.config.includeMeta !== false && msg.meta && Object.keys(msg.meta).length > 0) {
559
+ text += "\n```\n" + JSON.stringify(msg.meta, null, 2) + "\n```";
560
+ }
561
+ return text;
562
+ }
563
+ splitContent(content, maxLength) {
564
+ if (content.length <= maxLength) return [content];
565
+ const chunks = [];
566
+ let current = content;
567
+ while (current.length > 0) {
568
+ if (current.length <= maxLength) {
569
+ chunks.push(current);
570
+ break;
571
+ }
572
+ let splitAt = current.lastIndexOf("\n", maxLength);
573
+ if (splitAt === -1 || splitAt < maxLength / 2) {
574
+ splitAt = current.lastIndexOf(" ", maxLength);
575
+ }
576
+ if (splitAt === -1 || splitAt < maxLength / 2) {
577
+ splitAt = maxLength;
578
+ }
579
+ chunks.push(current.slice(0, splitAt));
580
+ current = current.slice(splitAt).trimStart();
581
+ }
582
+ return chunks;
583
+ }
584
+ async post(text) {
585
+ const url = new URL(this.endpoint);
586
+ url.searchParams.set("zapikey", this.config.apiKey);
587
+ if (this.config.bot?.name) {
588
+ url.searchParams.set("bot_unique_name", this.config.bot.name);
589
+ }
590
+ const body = {
591
+ text,
592
+ broadcast: this.config.broadcast ?? true
593
+ };
594
+ if (this.config.bot) {
595
+ body.bot = this.config.bot;
596
+ }
597
+ const response = await fetch(url.toString(), {
598
+ method: "POST",
599
+ headers: { "Content-Type": "application/json" },
600
+ body: JSON.stringify(body),
601
+ signal: AbortSignal.timeout(this.requestTimeout)
602
+ });
603
+ if (!response.ok && response.status !== 204) {
604
+ const respText = await response.text().catch(() => "");
605
+ throw new Error(`Zoho Cliq API failed: ${response.status} ${respText}`);
606
+ }
607
+ }
608
+ };
425
609
  var instanceUuid = crypto.randomUUID().slice(0, 8);
426
610
  function formatDate() {
427
611
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -470,7 +654,11 @@ var CloudWatchTransport = class extends BaseHttpTransport {
470
654
  batchSize: config.batchSize ?? 100,
471
655
  flushInterval: config.flushInterval ?? 1e3,
472
656
  maxRetries: config.maxRetries,
473
- retryDelay: config.retryDelay
657
+ retryDelay: config.retryDelay,
658
+ maxQueueSize: config.maxQueueSize,
659
+ dropPolicy: config.dropPolicy,
660
+ onDrop: config.onDrop,
661
+ onError: config.onError
474
662
  });
475
663
  this.config = config;
476
664
  this.resolvedLogStreamName = resolveLogStreamName(config.logStreamName, configHostname);
@@ -522,7 +710,10 @@ var CloudWatchTransport = class extends BaseHttpTransport {
522
710
  async ensureInitialized() {
523
711
  if (this.initialized) return;
524
712
  if (!this.initPromise) {
525
- this.initPromise = this.initialize();
713
+ this.initPromise = this.initialize().catch((err) => {
714
+ this.initPromise = null;
715
+ throw err;
716
+ });
526
717
  }
527
718
  await this.initPromise;
528
719
  }
@@ -598,21 +789,34 @@ function getLevelName(levelNum) {
598
789
  if (levelNum >= 20) return "debug";
599
790
  return "silly";
600
791
  }
601
- function shouldPassTransport(log, level, rules, store) {
792
+ function shouldPassTransport(log, level, rules, state) {
602
793
  const logLevel = getLevelName(log.level);
794
+ const embeddedOverride = log.__or;
795
+ if (typeof embeddedOverride === "string") {
796
+ if (embeddedOverride === "off") return false;
797
+ const overrideLevel = embeddedOverride;
798
+ return LOG_LEVELS[logLevel] <= LOG_LEVELS[overrideLevel];
799
+ }
603
800
  const context = log.context;
604
- const storeContext = store.getStore();
605
- const logMeta = {};
606
- for (const [key, value] of Object.entries(log)) {
607
- if (!["level", "time", "msg", "context"].includes(key)) {
608
- logMeta[key] = value;
801
+ if (rules && rules.length > 0) {
802
+ const storeContext = state.store.getStore();
803
+ const logMeta = {};
804
+ for (const [key, value] of Object.entries(log)) {
805
+ if (!RESERVED_LOG_FIELDS.has(key)) {
806
+ logMeta[key] = value;
807
+ }
808
+ }
809
+ const matchingRule = rules.find((rule) => matchesContext(storeContext, context, rule.match, logMeta));
810
+ if (matchingRule) {
811
+ if (matchingRule.level === "off") return false;
812
+ return LOG_LEVELS[logLevel] <= LOG_LEVELS[matchingRule.level];
609
813
  }
610
814
  }
611
- const matchingRule = rules?.find((rule) => matchesContext(storeContext, context, rule.match, logMeta));
612
- const effectiveLevel = matchingRule?.level ?? level ?? "silly";
815
+ const effectiveLevel = level ?? "silly";
613
816
  if (effectiveLevel === "off") return false;
614
817
  return LOG_LEVELS[logLevel] <= LOG_LEVELS[effectiveLevel];
615
818
  }
819
+ var RESERVED_LOG_FIELDS = /* @__PURE__ */ new Set(["level", "time", "msg", "context", "__or"]);
616
820
  function formatLog(log, format, store) {
617
821
  const levelName = getLevelName(log.level);
618
822
  const timestamp = new Date(log.time).toISOString();
@@ -621,7 +825,7 @@ function formatLog(log, format, store) {
621
825
  const storeContext = store.getStore();
622
826
  const meta = {};
623
827
  for (const [key, value] of Object.entries(log)) {
624
- if (!["level", "time", "msg", "context"].includes(key)) {
828
+ if (!RESERVED_LOG_FIELDS.has(key)) {
625
829
  meta[key] = value;
626
830
  }
627
831
  }
@@ -670,7 +874,7 @@ function formatLog(log, format, store) {
670
874
  return `[${timestamp}] ${levelName}: ${message}
671
875
  `;
672
876
  }
673
- function createFormattedFilterStream(format, level, rules, store, destination) {
877
+ function createFormattedFilterStream(format, level, rules, state, destination) {
674
878
  return new stream.Transform({
675
879
  transform(chunk, _encoding, callback) {
676
880
  const line = chunk.toString().trim();
@@ -683,24 +887,24 @@ function createFormattedFilterStream(format, level, rules, store, destination) {
683
887
  callback();
684
888
  return;
685
889
  }
686
- if (!shouldPassTransport(log, level, rules, store)) {
890
+ if (!shouldPassTransport(log, level, rules, state)) {
687
891
  callback();
688
892
  return;
689
893
  }
690
- const formatted = formatLog(log, format, store);
894
+ const formatted = formatLog(log, format, state.store);
691
895
  destination.write(formatted);
692
896
  callback();
693
897
  }
694
898
  });
695
899
  }
696
- function createStreams(config, store) {
900
+ function createStreams(config, state) {
697
901
  const streams = [];
698
902
  const transports = [];
699
903
  const consoleStream = createFormattedFilterStream(
700
904
  config.console.format,
701
905
  config.console.level,
702
906
  config.console.rules,
703
- store,
907
+ state,
704
908
  process.stdout
705
909
  );
706
910
  streams.push({
@@ -733,7 +937,7 @@ function createStreams(config, store) {
733
937
  fileConfig.format,
734
938
  fileConfig.level,
735
939
  fileConfig.rules,
736
- store,
940
+ state,
737
941
  rotatingStream
738
942
  );
739
943
  streams.push({
@@ -744,7 +948,7 @@ function createStreams(config, store) {
744
948
  for (const discordConfig of toArray(config.discord)) {
745
949
  const transport = new DiscordTransport(discordConfig);
746
950
  transports.push(transport);
747
- const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, store);
951
+ const discordStream = createHttpTransportStream(transport, discordConfig.level, discordConfig.rules, state);
748
952
  streams.push({
749
953
  level: "trace",
750
954
  stream: discordStream
@@ -753,21 +957,40 @@ function createStreams(config, store) {
753
957
  for (const telegramConfig of toArray(config.telegram)) {
754
958
  const transport = new TelegramTransport(telegramConfig);
755
959
  transports.push(transport);
756
- const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, store);
960
+ const telegramStream = createHttpTransportStream(transport, telegramConfig.level, telegramConfig.rules, state);
757
961
  streams.push({
758
962
  level: "trace",
759
963
  stream: telegramStream
760
964
  });
761
965
  }
966
+ for (const zohoCliqConfig of toArray(config.zohoCliq)) {
967
+ const transport = new ZohoCliqTransport(zohoCliqConfig);
968
+ transports.push(transport);
969
+ const zohoStream = createHttpTransportStream(transport, zohoCliqConfig.level, zohoCliqConfig.rules, state);
970
+ streams.push({
971
+ level: "trace",
972
+ stream: zohoStream
973
+ });
974
+ }
762
975
  for (const cloudwatchConfig of toArray(config.cloudwatch)) {
763
976
  const transport = new CloudWatchTransport(cloudwatchConfig, config.hostname);
764
977
  transports.push(transport);
765
- const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, store);
978
+ const cwStream = createHttpTransportStream(transport, cloudwatchConfig.level, cloudwatchConfig.rules, state);
766
979
  streams.push({
767
980
  level: "trace",
768
981
  stream: cwStream
769
982
  });
770
983
  }
984
+ if (config.relay) {
985
+ const relayStream = pino__default.default.transport({
986
+ target: "@rawnodes/logger/relay",
987
+ options: config.relay
988
+ });
989
+ streams.push({
990
+ level: "trace",
991
+ stream: relayStream
992
+ });
993
+ }
771
994
  return {
772
995
  destination: pino__default.default.multistream(streams),
773
996
  transports
@@ -777,7 +1000,7 @@ function toArray(value) {
777
1000
  if (!value) return [];
778
1001
  return Array.isArray(value) ? value : [value];
779
1002
  }
780
- function createHttpTransportStream(transport, level, rules, store) {
1003
+ function createHttpTransportStream(transport, level, rules, state) {
781
1004
  return new stream.Writable({
782
1005
  write(chunk, _encoding, callback) {
783
1006
  const line = chunk.toString().trim();
@@ -790,15 +1013,15 @@ function createHttpTransportStream(transport, level, rules, store) {
790
1013
  callback();
791
1014
  return;
792
1015
  }
793
- if (!shouldPassTransport(log, level, rules, store)) {
1016
+ if (!shouldPassTransport(log, level, rules, state)) {
794
1017
  callback();
795
1018
  return;
796
1019
  }
797
1020
  const levelName = getLevelName(log.level);
798
- const storeContext = store.getStore();
1021
+ const storeContext = state.store.getStore();
799
1022
  const meta = {};
800
1023
  for (const [key, value] of Object.entries(log)) {
801
- if (!["level", "time", "msg", "context"].includes(key)) {
1024
+ if (!RESERVED_LOG_FIELDS.has(key)) {
802
1025
  meta[key] = value;
803
1026
  }
804
1027
  }
@@ -847,45 +1070,53 @@ function buildIndexes(overrides) {
847
1070
  }
848
1071
  return { contextIndex, complexRules };
849
1072
  }
1073
+ function canonicalMatchKey(match) {
1074
+ const keys = Object.keys(match).sort();
1075
+ const sorted = {};
1076
+ for (const key of keys) {
1077
+ sorted[key] = match[key];
1078
+ }
1079
+ return JSON.stringify(sorted);
1080
+ }
850
1081
  function createState(config, store) {
851
1082
  const { defaultLevel, rules } = parseLevelConfig(config.level);
852
1083
  const loggerStore = store ?? new LoggerStore();
853
1084
  const levelOverrides = /* @__PURE__ */ new Map();
854
1085
  for (const rule of rules) {
855
- const key = JSON.stringify(rule.match);
1086
+ const key = canonicalMatchKey(rule.match);
856
1087
  levelOverrides.set(key, rule);
857
1088
  }
858
1089
  const { contextIndex, complexRules } = buildIndexes(levelOverrides);
859
- const { destination, transports } = createStreams(config, loggerStore);
1090
+ const state = {
1091
+ pino: null,
1092
+ store: loggerStore,
1093
+ defaultLevel,
1094
+ levelOverrides,
1095
+ contextIndex,
1096
+ complexRules,
1097
+ callerConfig: void 0,
1098
+ transports: []
1099
+ };
1100
+ const { destination, transports } = createStreams(config, state);
1101
+ state.transports = transports;
860
1102
  const options = {
861
1103
  level: "trace",
862
1104
  // Accept all, we filter in shouldLog()
863
1105
  customLevels: CUSTOM_LEVELS,
864
1106
  base: { hostname: config.hostname ?? os.hostname() }
865
1107
  };
866
- const pinoLogger = pino__default.default(options, destination);
867
- let callerConfig;
1108
+ state.pino = pino__default.default(options, destination);
868
1109
  if (config.caller === true) {
869
- callerConfig = {};
1110
+ state.callerConfig = {};
870
1111
  } else if (config.caller && typeof config.caller === "object") {
871
- callerConfig = config.caller;
1112
+ state.callerConfig = config.caller;
872
1113
  }
873
- return {
874
- pino: pinoLogger,
875
- store: loggerStore,
876
- defaultLevel,
877
- levelOverrides,
878
- contextIndex,
879
- complexRules,
880
- callerConfig,
881
- transports
882
- };
1114
+ return state;
883
1115
  }
884
1116
  function rebuildIndexes(state) {
885
1117
  const { contextIndex, complexRules } = buildIndexes(state.levelOverrides);
886
1118
  state.contextIndex = contextIndex;
887
- state.complexRules.length = 0;
888
- state.complexRules.push(...complexRules);
1119
+ state.complexRules = complexRules;
889
1120
  }
890
1121
  function shouldLog(state, level, context) {
891
1122
  const effectiveLevel = getEffectiveLevel(state, context);
@@ -909,6 +1140,20 @@ function matchesContext(storeContext, loggerContext, match, logMeta) {
909
1140
  const combined = { ...logMeta, ...storeContext, context: loggerContext };
910
1141
  return Object.entries(match).every(([key, value]) => combined[key] === value);
911
1142
  }
1143
+ function getGlobalOverrideLevel2(state, loggerContext, logMeta) {
1144
+ if (loggerContext) {
1145
+ const indexed = state.contextIndex.get(loggerContext);
1146
+ if (indexed && !indexed.readonly) return indexed.level;
1147
+ }
1148
+ const storeContext = state.store.getStore();
1149
+ for (const override of state.complexRules) {
1150
+ if (override.readonly) continue;
1151
+ if (matchesContext(storeContext, loggerContext, override.match, logMeta)) {
1152
+ return override.level;
1153
+ }
1154
+ }
1155
+ return void 0;
1156
+ }
912
1157
  var LogLevelSchema = zod.z.enum([
913
1158
  "off",
914
1159
  "error",
@@ -947,7 +1192,12 @@ var HttpTransportBaseConfigSchema = zod.z.object({
947
1192
  batchSize: zod.z.number().int().positive().optional(),
948
1193
  flushInterval: zod.z.number().int().positive().optional(),
949
1194
  maxRetries: zod.z.number().int().nonnegative().optional(),
950
- retryDelay: zod.z.number().int().positive().optional()
1195
+ retryDelay: zod.z.number().int().positive().optional(),
1196
+ onError: zod.z.function().optional(),
1197
+ onDrop: zod.z.function().optional(),
1198
+ maxQueueSize: zod.z.number().int().positive().optional(),
1199
+ dropPolicy: zod.z.enum(["drop-oldest", "drop-newest"]).optional(),
1200
+ requestTimeout: zod.z.number().int().positive().optional()
951
1201
  });
952
1202
  var DiscordConfigSchema = zod.z.object({
953
1203
  level: LogLevelSchema.optional(),
@@ -956,6 +1206,11 @@ var DiscordConfigSchema = zod.z.object({
956
1206
  flushInterval: zod.z.number().int().positive().optional(),
957
1207
  maxRetries: zod.z.number().int().nonnegative().optional(),
958
1208
  retryDelay: zod.z.number().int().positive().optional(),
1209
+ onError: zod.z.function().optional(),
1210
+ onDrop: zod.z.function().optional(),
1211
+ maxQueueSize: zod.z.number().int().positive().optional(),
1212
+ dropPolicy: zod.z.enum(["drop-oldest", "drop-newest"]).optional(),
1213
+ requestTimeout: zod.z.number().int().positive().optional(),
959
1214
  webhookUrl: zod.z.string().url("webhookUrl must be a valid URL"),
960
1215
  format: zod.z.enum(["embed", "markdown"]).optional(),
961
1216
  username: zod.z.string().optional(),
@@ -972,6 +1227,11 @@ var TelegramConfigSchema = zod.z.object({
972
1227
  flushInterval: zod.z.number().int().positive().optional(),
973
1228
  maxRetries: zod.z.number().int().nonnegative().optional(),
974
1229
  retryDelay: zod.z.number().int().positive().optional(),
1230
+ onError: zod.z.function().optional(),
1231
+ onDrop: zod.z.function().optional(),
1232
+ maxQueueSize: zod.z.number().int().positive().optional(),
1233
+ dropPolicy: zod.z.enum(["drop-oldest", "drop-newest"]).optional(),
1234
+ requestTimeout: zod.z.number().int().positive().optional(),
975
1235
  botToken: zod.z.string().min(1, "botToken is required"),
976
1236
  chatId: zod.z.union([zod.z.string(), zod.z.number()]),
977
1237
  parseMode: zod.z.enum(["Markdown", "MarkdownV2", "HTML"]).optional(),
@@ -1004,6 +1264,11 @@ var CloudWatchConfigSchema = zod.z.object({
1004
1264
  flushInterval: zod.z.number().int().positive().optional(),
1005
1265
  maxRetries: zod.z.number().int().nonnegative().optional(),
1006
1266
  retryDelay: zod.z.number().int().positive().optional(),
1267
+ onError: zod.z.function().optional(),
1268
+ onDrop: zod.z.function().optional(),
1269
+ maxQueueSize: zod.z.number().int().positive().optional(),
1270
+ dropPolicy: zod.z.enum(["drop-oldest", "drop-newest"]).optional(),
1271
+ requestTimeout: zod.z.number().int().positive().optional(),
1007
1272
  logGroupName: zod.z.string().min(1, "logGroupName is required"),
1008
1273
  logStreamName: LogStreamNameSchema.optional(),
1009
1274
  region: zod.z.string().min(1, "region is required"),
@@ -1012,6 +1277,43 @@ var CloudWatchConfigSchema = zod.z.object({
1012
1277
  createLogGroup: zod.z.boolean().optional(),
1013
1278
  createLogStream: zod.z.boolean().optional()
1014
1279
  });
1280
+ var ZohoCliqConfigBaseSchema = zod.z.object({
1281
+ level: LogLevelSchema.optional(),
1282
+ rules: zod.z.array(LevelRuleSchema).optional(),
1283
+ batchSize: zod.z.number().int().positive().optional(),
1284
+ flushInterval: zod.z.number().int().positive().optional(),
1285
+ maxRetries: zod.z.number().int().nonnegative().optional(),
1286
+ retryDelay: zod.z.number().int().positive().optional(),
1287
+ onError: zod.z.function().optional(),
1288
+ onDrop: zod.z.function().optional(),
1289
+ maxQueueSize: zod.z.number().int().positive().optional(),
1290
+ dropPolicy: zod.z.enum(["drop-oldest", "drop-newest"]).optional(),
1291
+ requestTimeout: zod.z.number().int().positive().optional(),
1292
+ webhookUrl: zod.z.string().url().optional(),
1293
+ companyId: zod.z.string().min(1).optional(),
1294
+ channel: zod.z.string().min(1).optional(),
1295
+ region: zod.z.string().min(1).optional(),
1296
+ apiKey: zod.z.string().min(1, "apiKey is required"),
1297
+ broadcast: zod.z.boolean().optional(),
1298
+ bot: zod.z.object({
1299
+ name: zod.z.string().min(1),
1300
+ image: zod.z.string().url().optional()
1301
+ }).optional(),
1302
+ includeTimestamp: zod.z.boolean().optional(),
1303
+ includeMeta: zod.z.boolean().optional()
1304
+ });
1305
+ var ZohoCliqConfigSchema = ZohoCliqConfigBaseSchema.refine(
1306
+ (cfg) => Boolean(cfg.webhookUrl) || Boolean(cfg.companyId) && Boolean(cfg.channel),
1307
+ { message: "Either webhookUrl or both companyId and channel must be provided" }
1308
+ );
1309
+ var RelayConfigSchema = zod.z.object({
1310
+ apiUrl: zod.z.string().url("apiUrl must be a valid URL"),
1311
+ token: zod.z.string().min(1, "token is required"),
1312
+ pollInterval: zod.z.number().int().positive().optional(),
1313
+ bufferSize: zod.z.number().int().positive().optional(),
1314
+ reconnectDelay: zod.z.number().int().positive().optional(),
1315
+ maxReconnectDelay: zod.z.number().int().positive().optional()
1316
+ });
1015
1317
  var LevelConfigObjectSchema = zod.z.object({
1016
1318
  default: LogLevelSchema,
1017
1319
  rules: zod.z.array(LevelRuleSchema).optional()
@@ -1035,6 +1337,8 @@ var LoggerConfigSchema = zod.z.object({
1035
1337
  discord: zod.z.union([DiscordConfigSchema, zod.z.array(DiscordConfigSchema)]).optional(),
1036
1338
  telegram: zod.z.union([TelegramConfigSchema, zod.z.array(TelegramConfigSchema)]).optional(),
1037
1339
  cloudwatch: zod.z.union([CloudWatchConfigSchema, zod.z.array(CloudWatchConfigSchema)]).optional(),
1340
+ zohoCliq: zod.z.union([ZohoCliqConfigSchema, zod.z.array(ZohoCliqConfigSchema)]).optional(),
1341
+ relay: RelayConfigSchema.optional(),
1038
1342
  caller: zod.z.union([zod.z.boolean(), CallerConfigSchema]).optional(),
1039
1343
  hostname: zod.z.string().optional(),
1040
1344
  autoShutdown: zod.z.union([zod.z.boolean(), AutoShutdownConfigSchema]).optional()
@@ -1282,7 +1586,10 @@ var Logger = class _Logger {
1282
1586
  this.state = state;
1283
1587
  this.context = context;
1284
1588
  }
1285
- profileTimers = /* @__PURE__ */ new Map();
1589
+ // Lazy: `.for()` creates many child loggers (hot path), most never call
1590
+ // `profile()`. Allocating a fresh Map per child wastes ~200 bytes each and
1591
+ // caused OOM in benchmarks creating millions of children.
1592
+ profileTimers;
1286
1593
  static create(config, store) {
1287
1594
  const validatedConfig = validateConfig(config);
1288
1595
  const state = createState(validatedConfig, store);
@@ -1302,12 +1609,12 @@ var Logger = class _Logger {
1302
1609
  }
1303
1610
  setLevelOverride(match, level) {
1304
1611
  assertLogLevel(level);
1305
- const key = JSON.stringify(match);
1612
+ const key = canonicalMatchKey(match);
1306
1613
  this.state.levelOverrides.set(key, { match, level });
1307
1614
  rebuildIndexes(this.state);
1308
1615
  }
1309
1616
  removeLevelOverride(match) {
1310
- const key = JSON.stringify(match);
1617
+ const key = canonicalMatchKey(match);
1311
1618
  const override = this.state.levelOverrides.get(key);
1312
1619
  if (override?.readonly) {
1313
1620
  return false;
@@ -1337,33 +1644,55 @@ var Logger = class _Logger {
1337
1644
  /**
1338
1645
  * Gracefully shutdown the logger, flushing all pending messages.
1339
1646
  * Should be called before process exit to ensure no logs are lost.
1647
+ *
1648
+ * `timeoutMs` is forwarded to each transport's `close()` as a per-transport
1649
+ * cap; it prevents a dead transport from blocking the whole shutdown.
1650
+ * Default: 5000ms. Pass `Infinity` for legacy unbounded behaviour.
1340
1651
  */
1341
- async shutdown() {
1342
- const closePromises = this.state.transports.map((transport) => transport.close());
1652
+ async shutdown(timeoutMs = 5e3) {
1653
+ const closePromises = this.state.transports.map((transport) => transport.close(timeoutMs));
1343
1654
  await Promise.all(closePromises);
1344
1655
  }
1345
1656
  // Profiling
1346
1657
  profile(id, meta) {
1347
- const existing = this.profileTimers.get(id);
1658
+ const timers = this.profileTimers ??= /* @__PURE__ */ new Map();
1659
+ const existing = timers.get(id);
1348
1660
  if (existing) {
1349
1661
  const duration = Date.now() - existing;
1350
- this.profileTimers.delete(id);
1662
+ timers.delete(id);
1351
1663
  this.info(`${id} completed`, { ...meta, durationMs: duration });
1352
1664
  } else {
1353
- this.profileTimers.set(id, Date.now());
1665
+ timers.set(id, Date.now());
1354
1666
  }
1355
1667
  }
1356
1668
  // Logging methods
1669
+ /**
1670
+ * Log an error. Supported shapes:
1671
+ * error(message: string)
1672
+ * error(message: string, meta)
1673
+ * error(error: Error | unknown)
1674
+ * error(error: Error | unknown, message: string)
1675
+ * error(error: Error | unknown, meta)
1676
+ * error(error: Error | unknown, message: string, meta)
1677
+ *
1678
+ * The first argument is treated as an error value whenever it is not a string.
1679
+ * Non-Error values (plain objects, numbers, etc. — e.g. anything caught by
1680
+ * TypeScript's `catch (err)` clause, which is typed as `unknown`) are passed
1681
+ * through `serializeError` so they produce a sensible `errorMessage` and, when
1682
+ * possible, a `stack` / HTTP diagnostic payload.
1683
+ */
1357
1684
  error(errorOrMessage, messageOrMeta, meta) {
1358
1685
  if (!shouldLog(this.state, "error", this.context)) return;
1359
- if (errorOrMessage instanceof Error) {
1360
- if (typeof messageOrMeta === "string") {
1361
- this.log("error", messageOrMeta, meta, errorOrMessage);
1362
- } else {
1363
- this.log("error", errorOrMessage.message, messageOrMeta, errorOrMessage);
1364
- }
1365
- } else {
1686
+ if (typeof errorOrMessage === "string") {
1366
1687
  this.log("error", errorOrMessage, messageOrMeta);
1688
+ return;
1689
+ }
1690
+ const errorValue = errorOrMessage;
1691
+ if (typeof messageOrMeta === "string") {
1692
+ this.log("error", messageOrMeta, meta, errorValue);
1693
+ } else {
1694
+ const fallbackMessage = errorValue instanceof Error ? errorValue.message : serializeError(errorValue).errorMessage;
1695
+ this.log("error", fallbackMessage, messageOrMeta, errorValue);
1367
1696
  }
1368
1697
  }
1369
1698
  warn(message, meta) {
@@ -1398,6 +1727,10 @@ var Logger = class _Logger {
1398
1727
  if (storeContext) {
1399
1728
  Object.assign(logMeta, storeContext);
1400
1729
  }
1730
+ const overrideLevel = getGlobalOverrideLevel2(this.state, this.context, logMeta);
1731
+ if (overrideLevel !== void 0) {
1732
+ logMeta.__or = overrideLevel;
1733
+ }
1401
1734
  if (this.state.callerConfig) {
1402
1735
  const callerInfo = getCallerInfo(this.state.callerConfig, callerOffset);
1403
1736
  if (callerInfo) {
@@ -1690,8 +2023,11 @@ exports.Logger = Logger;
1690
2023
  exports.LoggerConfigSchema = LoggerConfigSchema;
1691
2024
  exports.LoggerStore = LoggerStore;
1692
2025
  exports.MessageBuffer = MessageBuffer;
2026
+ exports.RelayConfigSchema = RelayConfigSchema;
1693
2027
  exports.TelegramConfigSchema = TelegramConfigSchema;
1694
2028
  exports.TelegramTransport = TelegramTransport;
2029
+ exports.ZohoCliqConfigSchema = ZohoCliqConfigSchema;
2030
+ exports.ZohoCliqTransport = ZohoCliqTransport;
1695
2031
  exports.assertLogLevel = assertLogLevel;
1696
2032
  exports.createMasker = createMasker;
1697
2033
  exports.createSingletonLogger = createSingletonLogger;