@rawnodes/logger 2.11.0 → 2.12.1

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
@@ -2,9 +2,8 @@ import { hostname } from 'os';
2
2
  import pino from 'pino';
3
3
  import { AsyncLocalStorage } from 'async_hooks';
4
4
  import { Transform, Writable } from 'stream';
5
- import { mkdir } from 'fs/promises';
6
5
  import { createStream } from 'rotating-file-stream';
7
- import { CloudWatchLogsClient, PutLogEventsCommand, CreateLogGroupCommand, CreateLogStreamCommand, DescribeLogStreamsCommand } from '@aws-sdk/client-cloudwatch-logs';
6
+ import { CloudWatchLogsClient, PutLogEventsCommand, CreateLogGroupCommand, CreateLogStreamCommand } from '@aws-sdk/client-cloudwatch-logs';
8
7
  import { randomUUID } from 'crypto';
9
8
  import { z } from 'zod';
10
9
  import { relative, basename } from 'path';
@@ -39,6 +38,89 @@ function assertLogLevel(level) {
39
38
  throw new Error(`Invalid log level: "${level}". Valid levels: ${Object.keys(LOG_LEVELS).join(", ")}`);
40
39
  }
41
40
  }
41
+ function reportFileFailure(error) {
42
+ const code = "code" in error && typeof error.code === "string" ? error.code : error.name;
43
+ process.stderr.write(
44
+ `[Logger] File logging stopped (${code}); other transports remain active.
45
+ `
46
+ );
47
+ }
48
+ function createFileDestination(config) {
49
+ let failed = false;
50
+ let rotatingStream;
51
+ const fail = (error) => {
52
+ if (failed) return;
53
+ failed = true;
54
+ if (config.onError) {
55
+ try {
56
+ config.onError(error);
57
+ } catch {
58
+ reportFileFailure(error);
59
+ }
60
+ } else {
61
+ reportFileFailure(error);
62
+ }
63
+ };
64
+ try {
65
+ rotatingStream = createStream(
66
+ (time, index) => {
67
+ const date = time instanceof Date ? time : /* @__PURE__ */ new Date();
68
+ const dateStr = date.toISOString().split("T")[0];
69
+ const base = config.filename.replace(".log", "");
70
+ return `${base}-${dateStr}${index ? `.${index}` : ""}.log`;
71
+ },
72
+ {
73
+ path: config.dirname,
74
+ size: (config.maxSize || "20M").replace(
75
+ /([kmg])$/i,
76
+ (unit) => unit.toUpperCase()
77
+ ),
78
+ interval: config.interval || (config.datePattern === "YYYY-MM-DD-HH" ? "1h" : "1d"),
79
+ compress: config.zippedArchive ? "gzip" : false,
80
+ maxFiles: parseInt(config.maxFiles?.replace("d", "") || "14")
81
+ }
82
+ );
83
+ rotatingStream.on("error", fail);
84
+ } catch (error) {
85
+ fail(error instanceof Error ? error : new Error(String(error)));
86
+ }
87
+ return new Writable({
88
+ write(chunk, encoding, callback) {
89
+ if (failed || !rotatingStream) {
90
+ callback();
91
+ return;
92
+ }
93
+ try {
94
+ rotatingStream.write(chunk, encoding, (error) => {
95
+ if (error) fail(error);
96
+ callback();
97
+ });
98
+ } catch (error) {
99
+ fail(error instanceof Error ? error : new Error(String(error)));
100
+ callback();
101
+ }
102
+ },
103
+ final(callback) {
104
+ if (!rotatingStream || rotatingStream.destroyed) {
105
+ callback();
106
+ return;
107
+ }
108
+ try {
109
+ rotatingStream.end((error) => {
110
+ if (error) fail(error);
111
+ callback();
112
+ });
113
+ } catch (error) {
114
+ fail(error instanceof Error ? error : new Error(String(error)));
115
+ callback();
116
+ }
117
+ },
118
+ destroy(_error, callback) {
119
+ rotatingStream?.destroy();
120
+ callback();
121
+ }
122
+ });
123
+ }
42
124
 
43
125
  // src/transports/buffer.ts
44
126
  var MessageBuffer = class {
@@ -645,14 +727,21 @@ function resolveLogStreamName(config, configHostname) {
645
727
  }
646
728
  return hostname$1;
647
729
  }
648
- var CloudWatchTransport = class extends BaseHttpTransport {
730
+ var CloudWatchTransport = class _CloudWatchTransport extends BaseHttpTransport {
649
731
  config;
650
732
  client;
651
- sequenceToken;
652
733
  initialized = false;
653
734
  initPromise = null;
735
+ initFailures = 0;
736
+ nextInitAttempt = 0;
654
737
  resolvedLogStreamName;
655
738
  maskReplacer;
739
+ // Backoff bounds for repeated init failures (e.g. IAM not yet propagated,
740
+ // throttling). Without this, a sustained outage makes every flush re-run
741
+ // initialize() back-to-back and hammer the CloudWatch control-plane APIs,
742
+ // which only deepens the throttling.
743
+ static INIT_BACKOFF_BASE_MS = 1e3;
744
+ static INIT_BACKOFF_MAX_MS = 3e4;
656
745
  constructor(config, configHostname, maskReplacerFn) {
657
746
  super({
658
747
  batchSize: config.batchSize ?? 100,
@@ -688,36 +777,46 @@ var CloudWatchTransport = class extends BaseHttpTransport {
688
777
  }, replacer)
689
778
  }));
690
779
  logEvents.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
691
- const command = new PutLogEventsCommand({
692
- logGroupName: this.config.logGroupName,
693
- logStreamName: this.resolvedLogStreamName,
694
- logEvents,
695
- sequenceToken: this.sequenceToken
696
- });
697
780
  try {
698
- const response = await this.client.send(command);
699
- this.sequenceToken = response.nextSequenceToken;
781
+ await this.putLogEvents(logEvents);
700
782
  } catch (error) {
701
- if (this.isInvalidSequenceTokenError(error)) {
702
- await this.fetchSequenceToken();
703
- const retryCommand = new PutLogEventsCommand({
704
- logGroupName: this.config.logGroupName,
705
- logStreamName: this.resolvedLogStreamName,
706
- logEvents,
707
- sequenceToken: this.sequenceToken
708
- });
709
- const response = await this.client.send(retryCommand);
710
- this.sequenceToken = response.nextSequenceToken;
783
+ if (this.isResourceNotFoundError(error)) {
784
+ this.initialized = false;
785
+ this.initPromise = null;
786
+ await this.ensureInitialized();
787
+ await this.putLogEvents(logEvents);
711
788
  } else {
712
789
  throw error;
713
790
  }
714
791
  }
715
792
  }
793
+ // PutLogEvents no longer requires a sequence token: since 2023 CloudWatch
794
+ // Logs ignores the `sequenceToken` field for sequential writes, so we omit it
795
+ // entirely. This also removes the need for DescribeLogStreams (and the
796
+ // logs:DescribeLogStreams IAM permission) on the hot path.
797
+ async putLogEvents(logEvents) {
798
+ await this.client.send(
799
+ new PutLogEventsCommand({
800
+ logGroupName: this.config.logGroupName,
801
+ logStreamName: this.resolvedLogStreamName,
802
+ logEvents
803
+ })
804
+ );
805
+ }
716
806
  async ensureInitialized() {
717
807
  if (this.initialized) return;
808
+ if (!this.initPromise && Date.now() < this.nextInitAttempt) {
809
+ throw new Error("CloudWatch transport initialization is backing off");
810
+ }
718
811
  if (!this.initPromise) {
719
812
  this.initPromise = this.initialize().catch((err) => {
720
813
  this.initPromise = null;
814
+ this.initFailures += 1;
815
+ const delay = Math.min(
816
+ _CloudWatchTransport.INIT_BACKOFF_MAX_MS,
817
+ _CloudWatchTransport.INIT_BACKOFF_BASE_MS * 2 ** (this.initFailures - 1)
818
+ );
819
+ this.nextInitAttempt = Date.now() + delay;
721
820
  throw err;
722
821
  });
723
822
  }
@@ -730,8 +829,9 @@ var CloudWatchTransport = class extends BaseHttpTransport {
730
829
  if (this.config.createLogStream !== false) {
731
830
  await this.createLogStreamIfNotExists();
732
831
  }
733
- await this.fetchSequenceToken();
734
832
  this.initialized = true;
833
+ this.initFailures = 0;
834
+ this.nextInitAttempt = 0;
735
835
  }
736
836
  async createLogGroupIfNotExists() {
737
837
  try {
@@ -760,22 +860,16 @@ var CloudWatchTransport = class extends BaseHttpTransport {
760
860
  }
761
861
  }
762
862
  }
763
- async fetchSequenceToken() {
764
- const response = await this.client.send(
765
- new DescribeLogStreamsCommand({
766
- logGroupName: this.config.logGroupName,
767
- logStreamNamePrefix: this.resolvedLogStreamName,
768
- limit: 1
769
- })
770
- );
771
- const stream = response.logStreams?.find((s) => s.logStreamName === this.resolvedLogStreamName);
772
- this.sequenceToken = stream?.uploadSequenceToken;
773
- }
774
863
  isResourceAlreadyExistsError(error) {
775
- return typeof error === "object" && error !== null && "name" in error && error.name === "ResourceAlreadyExistsException";
864
+ return this.isAwsErrorNamed(error, "ResourceAlreadyExistsException");
865
+ }
866
+ isResourceNotFoundError(error) {
867
+ return this.isAwsErrorNamed(error, "ResourceNotFoundException");
776
868
  }
777
- isInvalidSequenceTokenError(error) {
778
- return typeof error === "object" && error !== null && "name" in error && error.name === "InvalidSequenceTokenException";
869
+ isAwsErrorNamed(error, name) {
870
+ if (typeof error !== "object" || error === null) return false;
871
+ if ("name" in error && error.name === name) return true;
872
+ return "__type" in error && typeof error.__type === "string" && error.__type.split("#").at(-1) === name;
779
873
  }
780
874
  };
781
875
 
@@ -937,26 +1031,7 @@ function createStreams(config, state) {
937
1031
  });
938
1032
  if (config.file) {
939
1033
  const fileConfig = config.file;
940
- mkdir(fileConfig.dirname, { recursive: true }).catch(() => {
941
- });
942
- const normalizeSize = (size) => {
943
- return size.replace(/([kmg])$/i, (match) => match.toUpperCase());
944
- };
945
- const rotatingStream = createStream(
946
- (time, index) => {
947
- const date = time instanceof Date ? time : /* @__PURE__ */ new Date();
948
- const dateStr = date.toISOString().split("T")[0];
949
- const base = fileConfig.filename.replace(".log", "");
950
- return `${base}-${dateStr}${index ? `.${index}` : ""}.log`;
951
- },
952
- {
953
- path: fileConfig.dirname,
954
- size: normalizeSize(fileConfig.maxSize || "20M"),
955
- interval: fileConfig.interval || (fileConfig.datePattern === "YYYY-MM-DD-HH" ? "1h" : "1d"),
956
- compress: fileConfig.zippedArchive ? "gzip" : false,
957
- maxFiles: parseInt(fileConfig.maxFiles?.replace("d", "") || "14")
958
- }
959
- );
1034
+ const rotatingStream = createFileDestination(fileConfig);
960
1035
  const fileStream = createFormattedFilterStream(
961
1036
  fileConfig.format,
962
1037
  fileConfig.level,
@@ -1348,7 +1423,8 @@ var FileConfigSchema = z.object({
1348
1423
  interval: z.string().regex(/^\d+[Mdhms]$/).optional(),
1349
1424
  zippedArchive: z.boolean().optional(),
1350
1425
  maxSize: z.string().optional(),
1351
- maxFiles: z.string().optional()
1426
+ maxFiles: z.string().optional(),
1427
+ onError: z.function().optional()
1352
1428
  });
1353
1429
  var HttpTransportBaseConfigSchema = z.object({
1354
1430
  level: LogLevelSchema.optional(),