zario 0.3.5 → 0.4.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/README.md +102 -359
- package/dist/cjs/aggregation/LogAggregator.js +60 -31
- package/dist/cjs/core/Formatter.js +3 -4
- package/dist/cjs/core/Logger.js +41 -24
- package/dist/cjs/filters/Filter.js +52 -18
- package/dist/cjs/filters/index.js +0 -1
- package/dist/cjs/index.js +6 -1
- package/dist/cjs/transports/CircuitBreakerTransport.js +171 -0
- package/dist/cjs/transports/DeadLetterQueue.js +129 -0
- package/dist/cjs/transports/FileTransport.js +78 -7
- package/dist/cjs/transports/HttpTransport.js +15 -3
- package/dist/cjs/transports/RetryTransport.js +199 -0
- package/dist/cjs/transports/index.js +3 -0
- package/dist/cjs/types/TypeInterfaces.js +2 -0
- package/dist/cjs/utils/index.js +78 -0
- package/dist/esm/aggregation/LogAggregator.d.ts +8 -29
- package/dist/esm/aggregation/LogAggregator.js +60 -31
- package/dist/esm/core/Formatter.js +1 -2
- package/dist/esm/core/Logger.d.ts +13 -3
- package/dist/esm/core/Logger.js +40 -23
- package/dist/esm/filters/Filter.d.ts +24 -22
- package/dist/esm/filters/Filter.js +47 -17
- package/dist/esm/filters/index.d.ts +0 -1
- package/dist/esm/filters/index.js +0 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +6 -3
- package/dist/esm/transports/CircuitBreakerTransport.d.ts +43 -0
- package/dist/esm/transports/CircuitBreakerTransport.js +167 -0
- package/dist/esm/transports/DeadLetterQueue.d.ts +34 -0
- package/dist/esm/transports/DeadLetterQueue.js +92 -0
- package/dist/esm/transports/FileTransport.d.ts +4 -0
- package/dist/esm/transports/FileTransport.js +78 -7
- package/dist/esm/transports/HttpTransport.d.ts +2 -0
- package/dist/esm/transports/HttpTransport.js +15 -3
- package/dist/esm/transports/RetryTransport.d.ts +67 -0
- package/dist/esm/transports/RetryTransport.js +195 -0
- package/dist/esm/transports/Transport.d.ts +1 -0
- package/dist/esm/transports/index.d.ts +3 -0
- package/dist/esm/transports/index.js +3 -0
- package/dist/esm/types/TypeInterfaces.d.ts +7 -0
- package/dist/esm/types/TypeInterfaces.js +1 -0
- package/dist/esm/utils/index.d.ts +15 -0
- package/dist/esm/utils/index.js +72 -0
- package/package.json +87 -71
- package/dist/cjs/filters/SpecificFilters.js +0 -71
- package/dist/cjs/utils/ColorUtil.js +0 -42
- package/dist/cjs/utils/TimeUtil.js +0 -26
- package/dist/cjs/utils/Timerutil.js +0 -22
- package/dist/esm/filters/SpecificFilters.d.ts +0 -41
- package/dist/esm/filters/SpecificFilters.js +0 -64
- package/dist/esm/utils/ColorUtil.d.ts +0 -4
- package/dist/esm/utils/ColorUtil.js +0 -38
- package/dist/esm/utils/TimeUtil.d.ts +0 -3
- package/dist/esm/utils/TimeUtil.js +0 -22
- package/dist/esm/utils/Timerutil.d.ts +0 -8
- package/dist/esm/utils/Timerutil.js +0 -18
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export class DeadLetterQueue {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
this.deadLetters = [];
|
|
4
|
+
const { transport, maxRetries = 3, retryableErrorCodes = ['ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND'], deadLetterFile, onDeadLetter, } = options;
|
|
5
|
+
// Initialize the wrapped transport
|
|
6
|
+
if (typeof transport === 'function') {
|
|
7
|
+
this.transport = transport();
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
this.transport = transport;
|
|
11
|
+
}
|
|
12
|
+
this.maxRetries = maxRetries;
|
|
13
|
+
this.retryableErrorCodes = new Set(retryableErrorCodes);
|
|
14
|
+
if (deadLetterFile)
|
|
15
|
+
this.deadLetterFile = deadLetterFile;
|
|
16
|
+
if (onDeadLetter)
|
|
17
|
+
this.onDeadLetter = onDeadLetter;
|
|
18
|
+
}
|
|
19
|
+
async write(data, formatter) {
|
|
20
|
+
return this.writeWithRetry(data, formatter, 0);
|
|
21
|
+
}
|
|
22
|
+
async writeAsync(data, formatter) {
|
|
23
|
+
return this.writeWithRetry(data, formatter, 0);
|
|
24
|
+
}
|
|
25
|
+
async writeWithRetry(data, formatter, attempt) {
|
|
26
|
+
try {
|
|
27
|
+
if (this.transport.writeAsync) {
|
|
28
|
+
await this.transport.writeAsync(data, formatter);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.transport.write(data, formatter);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const errorCode = error?.code || 'UNKNOWN';
|
|
36
|
+
// Check if this error is retryable
|
|
37
|
+
if (this.retryableErrorCodes.has(errorCode)) {
|
|
38
|
+
if (attempt < this.maxRetries) {
|
|
39
|
+
// Exponential backoff with jitter
|
|
40
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
|
|
41
|
+
const jitter = Math.random() * 0.1 * delay;
|
|
42
|
+
await new Promise(resolve => setTimeout(resolve, delay + jitter));
|
|
43
|
+
return this.writeWithRetry(data, formatter, attempt + 1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// This is a permanent failure - create dead letter
|
|
47
|
+
const deadLetter = {
|
|
48
|
+
...data,
|
|
49
|
+
deadLetterReason: error instanceof Error ? error.message : String(error),
|
|
50
|
+
originalError: errorCode,
|
|
51
|
+
retryCount: attempt,
|
|
52
|
+
failedAt: new Date(),
|
|
53
|
+
};
|
|
54
|
+
this.deadLetters.push(deadLetter);
|
|
55
|
+
if (this.deadLetterFile) {
|
|
56
|
+
await this.writeDeadLetterToFile(deadLetter);
|
|
57
|
+
}
|
|
58
|
+
if (this.onDeadLetter) {
|
|
59
|
+
this.onDeadLetter(deadLetter);
|
|
60
|
+
}
|
|
61
|
+
// Re-throw the error for upstream handling
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async writeDeadLetterToFile(deadLetter) {
|
|
66
|
+
if (!this.deadLetterFile)
|
|
67
|
+
return;
|
|
68
|
+
try {
|
|
69
|
+
const fs = await import('fs');
|
|
70
|
+
const deadLetterLine = JSON.stringify(deadLetter) + '\n';
|
|
71
|
+
await fs.promises.appendFile(this.deadLetterFile, deadLetterLine);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error('Failed to write dead letter:', error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
getDeadLetters() {
|
|
78
|
+
return [...this.deadLetters];
|
|
79
|
+
}
|
|
80
|
+
clearDeadLetters() {
|
|
81
|
+
this.deadLetters = [];
|
|
82
|
+
}
|
|
83
|
+
// Clean up resources
|
|
84
|
+
async destroy() {
|
|
85
|
+
if (this.transport && typeof this.transport.destroy === 'function') {
|
|
86
|
+
await this.transport.destroy();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
isAsyncSupported() {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -9,6 +9,7 @@ export interface FileTransportOptions {
|
|
|
9
9
|
compression?: CompressionType;
|
|
10
10
|
batchInterval?: number;
|
|
11
11
|
compressOldFiles?: boolean;
|
|
12
|
+
maxQueueSize?: number;
|
|
12
13
|
}
|
|
13
14
|
export interface BatchLogEntry {
|
|
14
15
|
data: string;
|
|
@@ -21,6 +22,7 @@ export declare class FileTransport implements Transport {
|
|
|
21
22
|
private compression;
|
|
22
23
|
private batchInterval;
|
|
23
24
|
private compressOldFiles;
|
|
25
|
+
private maxQueueSize;
|
|
24
26
|
private batchQueue;
|
|
25
27
|
private batchTimer;
|
|
26
28
|
constructor(options: FileTransportOptions);
|
|
@@ -31,6 +33,8 @@ export declare class FileTransport implements Transport {
|
|
|
31
33
|
private rotateFilesAsync;
|
|
32
34
|
private performRotation;
|
|
33
35
|
private performRotationAsync;
|
|
36
|
+
private performRotationWithStreams;
|
|
37
|
+
private performRotationWithStreamsAsync;
|
|
34
38
|
private getRotatedFilePath;
|
|
35
39
|
private filterRotatedFiles;
|
|
36
40
|
private cleanupOldFiles;
|
|
@@ -6,17 +6,18 @@ const compressGzip = promisify(zlib.gzip);
|
|
|
6
6
|
const compressDeflate = promisify(zlib.deflate);
|
|
7
7
|
export class FileTransport {
|
|
8
8
|
constructor(options) {
|
|
9
|
-
// batching funct
|
|
10
9
|
this.batchQueue = [];
|
|
11
10
|
this.batchTimer = null;
|
|
12
11
|
const { path: filePath, maxSize = 10 * 1024 * 1024, maxFiles = 5, compression = "none", batchInterval = 0, // no batching
|
|
13
|
-
compressOldFiles = true,
|
|
12
|
+
compressOldFiles = true, maxQueueSize = 10000, // default maximum queue size
|
|
13
|
+
} = options;
|
|
14
14
|
this.filePath = filePath;
|
|
15
15
|
this.maxSize = maxSize;
|
|
16
16
|
this.maxFiles = maxFiles;
|
|
17
17
|
this.compression = compression;
|
|
18
18
|
this.batchInterval = batchInterval;
|
|
19
19
|
this.compressOldFiles = compressOldFiles;
|
|
20
|
+
this.maxQueueSize = maxQueueSize;
|
|
20
21
|
const dir = path.dirname(this.filePath);
|
|
21
22
|
if (!fs.existsSync(dir)) {
|
|
22
23
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -33,7 +34,11 @@ export class FileTransport {
|
|
|
33
34
|
const output = formatter.format(data);
|
|
34
35
|
const formattedOutput = output + "\n";
|
|
35
36
|
if (this.batchInterval > 0) {
|
|
36
|
-
// Queue entry if batching is enabled
|
|
37
|
+
// Queue entry if batching is enabled, with queue size limit
|
|
38
|
+
if (this.batchQueue.length >= this.maxQueueSize) {
|
|
39
|
+
// Drop oldest entry to maintain queue limit
|
|
40
|
+
this.batchQueue.shift();
|
|
41
|
+
}
|
|
37
42
|
this.batchQueue.push({
|
|
38
43
|
data: formattedOutput,
|
|
39
44
|
timestamp: new Date(),
|
|
@@ -50,6 +55,9 @@ export class FileTransport {
|
|
|
50
55
|
async writeAsync(data, formatter) {
|
|
51
56
|
const formattedOutput = formatter.format(data) + "\n";
|
|
52
57
|
if (this.batchInterval > 0) {
|
|
58
|
+
if (this.batchQueue.length >= this.maxQueueSize) {
|
|
59
|
+
this.batchQueue.shift();
|
|
60
|
+
}
|
|
53
61
|
this.batchQueue.push({
|
|
54
62
|
data: formattedOutput,
|
|
55
63
|
timestamp: new Date(),
|
|
@@ -81,8 +89,7 @@ export class FileTransport {
|
|
|
81
89
|
try {
|
|
82
90
|
if (!fs.existsSync(this.filePath))
|
|
83
91
|
return;
|
|
84
|
-
|
|
85
|
-
this.performRotation(currentContent, fs.writeFileSync);
|
|
92
|
+
this.performRotationWithStreams();
|
|
86
93
|
this.cleanupOldFiles();
|
|
87
94
|
}
|
|
88
95
|
catch (error) {
|
|
@@ -93,8 +100,7 @@ export class FileTransport {
|
|
|
93
100
|
try {
|
|
94
101
|
if (!fs.existsSync(this.filePath))
|
|
95
102
|
return;
|
|
96
|
-
|
|
97
|
-
await this.performRotationAsync(currentContent);
|
|
103
|
+
await this.performRotationWithStreamsAsync();
|
|
98
104
|
await this.cleanupOldFilesAsync();
|
|
99
105
|
}
|
|
100
106
|
catch (error) {
|
|
@@ -125,6 +131,71 @@ export class FileTransport {
|
|
|
125
131
|
}
|
|
126
132
|
await fs.promises.writeFile(this.filePath, "", "utf8");
|
|
127
133
|
}
|
|
134
|
+
performRotationWithStreams() {
|
|
135
|
+
const rotatedFilePath = this.getRotatedFilePath();
|
|
136
|
+
const readStream = fs.createReadStream(this.filePath);
|
|
137
|
+
if (this.compression !== "none" && this.compressOldFiles) {
|
|
138
|
+
const compressedFilePath = `${rotatedFilePath}.${this.compression === "gzip" ? "gz" : "zz"}`;
|
|
139
|
+
const writeStream = fs.createWriteStream(compressedFilePath);
|
|
140
|
+
const compressStream = this.compression === "gzip" ? zlib.createGzip() : zlib.createDeflate();
|
|
141
|
+
readStream.pipe(compressStream).pipe(writeStream);
|
|
142
|
+
writeStream.on('finish', () => {
|
|
143
|
+
fs.writeFileSync(this.filePath, "", "utf8");
|
|
144
|
+
});
|
|
145
|
+
writeStream.on('error', (error) => {
|
|
146
|
+
console.error("Error during stream compression:", error);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const writeStream = fs.createWriteStream(rotatedFilePath);
|
|
151
|
+
readStream.pipe(writeStream);
|
|
152
|
+
writeStream.on('finish', () => {
|
|
153
|
+
fs.writeFileSync(this.filePath, "", "utf8");
|
|
154
|
+
});
|
|
155
|
+
writeStream.on('error', (error) => {
|
|
156
|
+
console.error("Error during stream rotation:", error);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async performRotationWithStreamsAsync() {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const rotatedFilePath = this.getRotatedFilePath();
|
|
163
|
+
const readStream = fs.createReadStream(this.filePath);
|
|
164
|
+
if (this.compression !== "none" && this.compressOldFiles) {
|
|
165
|
+
const compressedFilePath = `${rotatedFilePath}.${this.compression === "gzip" ? "gz" : "zz"}`;
|
|
166
|
+
const writeStream = fs.createWriteStream(compressedFilePath);
|
|
167
|
+
const compressStream = this.compression === "gzip" ? zlib.createGzip() : zlib.createDeflate();
|
|
168
|
+
readStream.pipe(compressStream).pipe(writeStream);
|
|
169
|
+
writeStream.on('finish', async () => {
|
|
170
|
+
try {
|
|
171
|
+
await fs.promises.writeFile(this.filePath, "", "utf8");
|
|
172
|
+
resolve();
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
reject(error);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
writeStream.on('error', reject);
|
|
179
|
+
readStream.on('error', reject);
|
|
180
|
+
compressStream.on('error', reject);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const writeStream = fs.createWriteStream(rotatedFilePath);
|
|
184
|
+
readStream.pipe(writeStream);
|
|
185
|
+
writeStream.on('finish', async () => {
|
|
186
|
+
try {
|
|
187
|
+
await fs.promises.writeFile(this.filePath, "", "utf8");
|
|
188
|
+
resolve();
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
reject(error);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
writeStream.on('error', reject);
|
|
195
|
+
readStream.on('error', reject);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
128
199
|
getRotatedFilePath() {
|
|
129
200
|
const dir = path.dirname(this.filePath);
|
|
130
201
|
return path.join(dir, `${path.basename(this.filePath)}.${Date.now()}`);
|
|
@@ -7,6 +7,7 @@ export interface HttpTransportOptions {
|
|
|
7
7
|
headers?: Record<string, string>;
|
|
8
8
|
timeout?: number;
|
|
9
9
|
retries?: number;
|
|
10
|
+
forceAsync?: boolean;
|
|
10
11
|
}
|
|
11
12
|
export declare class HttpTransport implements Transport {
|
|
12
13
|
private url;
|
|
@@ -14,6 +15,7 @@ export declare class HttpTransport implements Transport {
|
|
|
14
15
|
private headers;
|
|
15
16
|
private timeout;
|
|
16
17
|
private retries;
|
|
18
|
+
private forceAsync;
|
|
17
19
|
constructor(options: HttpTransportOptions);
|
|
18
20
|
write(data: LogData, formatter: Formatter): void;
|
|
19
21
|
writeAsync(data: LogData, formatter: Formatter): Promise<void>;
|
|
@@ -3,7 +3,8 @@ import * as https from "https";
|
|
|
3
3
|
import * as url from "url";
|
|
4
4
|
export class HttpTransport {
|
|
5
5
|
constructor(options) {
|
|
6
|
-
const { url, method = 'POST', headers = {}, timeout = 5000, retries = 3 // defaults
|
|
6
|
+
const { url, method = 'POST', headers = {}, timeout = 5000, retries = 3, // defaults
|
|
7
|
+
forceAsync = false // Force async mode even in write() method
|
|
7
8
|
} = options;
|
|
8
9
|
if (!url) {
|
|
9
10
|
throw new Error('HttpTransport requires a URL option');
|
|
@@ -13,6 +14,7 @@ export class HttpTransport {
|
|
|
13
14
|
this.headers = { ...headers };
|
|
14
15
|
this.timeout = timeout;
|
|
15
16
|
this.retries = retries;
|
|
17
|
+
this.forceAsync = forceAsync;
|
|
16
18
|
// Set default Content-Type if not provided
|
|
17
19
|
if (!this.headers['Content-Type'] && !this.headers['content-type']) {
|
|
18
20
|
this.headers['Content-Type'] = 'application/json';
|
|
@@ -22,12 +24,22 @@ export class HttpTransport {
|
|
|
22
24
|
// Format the data as JSON for HTTP transport
|
|
23
25
|
const logObject = this.parseFormattedData(data);
|
|
24
26
|
const body = JSON.stringify(logObject);
|
|
25
|
-
|
|
27
|
+
if (this.forceAsync) {
|
|
28
|
+
// Force async mode using setImmediate
|
|
29
|
+
setImmediate(() => {
|
|
30
|
+
this.sendHttpRequestWithRetry(body, 0)
|
|
31
|
+
.catch((error) => {
|
|
32
|
+
console.error('HttpTransport error (forced async mode):', error.message);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Best-effort synchronous mode - note: actual network I/O is still async
|
|
26
38
|
this.sendHttpRequestWithRetry(body, 0)
|
|
27
39
|
.catch((error) => {
|
|
28
40
|
console.error('HttpTransport error (sync mode):', error.message);
|
|
29
41
|
});
|
|
30
|
-
}
|
|
42
|
+
}
|
|
31
43
|
}
|
|
32
44
|
async writeAsync(data, formatter) {
|
|
33
45
|
// json formating for HttpTransport
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Transport } from "./Transport.js";
|
|
2
|
+
import { LogData } from "../types/index.js";
|
|
3
|
+
import { Formatter } from "../core/Formatter.js";
|
|
4
|
+
import { EventEmitter } from "events";
|
|
5
|
+
export interface RetryTransportOptions {
|
|
6
|
+
wrappedTransport: Transport;
|
|
7
|
+
maxAttempts?: number;
|
|
8
|
+
baseDelay?: number;
|
|
9
|
+
maxDelay?: number;
|
|
10
|
+
backoffMultiplier?: number;
|
|
11
|
+
jitter?: boolean;
|
|
12
|
+
retryableErrorCodes?: string[];
|
|
13
|
+
retryableErrorPatterns?: RegExp[];
|
|
14
|
+
circuitBreakerThreshold?: number;
|
|
15
|
+
circuitBreakerTimeout?: number;
|
|
16
|
+
onRetryAttempt?: (attempt: number, error: Error, delay: number) => void;
|
|
17
|
+
onRetryExhausted?: (lastError: Error, attempts: number) => void;
|
|
18
|
+
onCircuitBreakerOpen?: () => void;
|
|
19
|
+
onCircuitBreakerClose?: () => void;
|
|
20
|
+
}
|
|
21
|
+
export interface RetryContext {
|
|
22
|
+
attempt: number;
|
|
23
|
+
totalAttempts: number;
|
|
24
|
+
originalError: Error;
|
|
25
|
+
delay: number;
|
|
26
|
+
startTime: number;
|
|
27
|
+
}
|
|
28
|
+
export declare enum CircuitBreakerState {
|
|
29
|
+
CLOSED = "closed",
|
|
30
|
+
OPEN = "open",
|
|
31
|
+
HALF_OPEN = "half_open"
|
|
32
|
+
}
|
|
33
|
+
export declare class RetryTransport extends EventEmitter implements Transport {
|
|
34
|
+
private wrappedTransport;
|
|
35
|
+
private maxAttempts;
|
|
36
|
+
private baseDelay;
|
|
37
|
+
private maxDelay;
|
|
38
|
+
private backoffMultiplier;
|
|
39
|
+
private jitter;
|
|
40
|
+
private retryableErrorCodes;
|
|
41
|
+
private retryableErrorPatterns;
|
|
42
|
+
private circuitBreakerThreshold;
|
|
43
|
+
private circuitBreakerTimeout;
|
|
44
|
+
private circuitBreakerState;
|
|
45
|
+
private failureCount;
|
|
46
|
+
private circuitBreakerOpenTime;
|
|
47
|
+
private onRetryAttempt;
|
|
48
|
+
private onRetryExhausted;
|
|
49
|
+
private onCircuitBreakerOpen;
|
|
50
|
+
private onCircuitBreakerClose;
|
|
51
|
+
constructor(options: RetryTransportOptions);
|
|
52
|
+
write(data: LogData, formatter: Formatter): void;
|
|
53
|
+
writeAsync(data: LogData, formatter: Formatter): Promise<void>;
|
|
54
|
+
private writeWithRetry;
|
|
55
|
+
private isRetryableError;
|
|
56
|
+
private calculateDelay;
|
|
57
|
+
private delay;
|
|
58
|
+
private isCircuitBreakerOpen;
|
|
59
|
+
private incrementFailureCount;
|
|
60
|
+
private resetFailureCount;
|
|
61
|
+
private maybeOpenCircuitBreaker;
|
|
62
|
+
private maybeCloseCircuitBreaker;
|
|
63
|
+
getCircuitBreakerState(): CircuitBreakerState;
|
|
64
|
+
getFailureCount(): number;
|
|
65
|
+
resetCircuitBreaker(): void;
|
|
66
|
+
getWrappedTransport(): Transport;
|
|
67
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
export var CircuitBreakerState;
|
|
3
|
+
(function (CircuitBreakerState) {
|
|
4
|
+
CircuitBreakerState["CLOSED"] = "closed";
|
|
5
|
+
CircuitBreakerState["OPEN"] = "open";
|
|
6
|
+
CircuitBreakerState["HALF_OPEN"] = "half_open";
|
|
7
|
+
})(CircuitBreakerState || (CircuitBreakerState = {}));
|
|
8
|
+
export class RetryTransport extends EventEmitter {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super();
|
|
11
|
+
this.circuitBreakerState = CircuitBreakerState.CLOSED;
|
|
12
|
+
this.failureCount = 0;
|
|
13
|
+
this.circuitBreakerOpenTime = 0;
|
|
14
|
+
const { wrappedTransport, maxAttempts = 3, baseDelay = 1000, maxDelay = 30000, backoffMultiplier = 2, jitter = true, retryableErrorCodes = [
|
|
15
|
+
'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND',
|
|
16
|
+
'EAI_AGAIN', 'EHOSTUNREACH', 'ENETUNREACH', 'ENOENT',
|
|
17
|
+
'EMFILE', 'ENFILE'
|
|
18
|
+
], retryableErrorPatterns = [
|
|
19
|
+
/timeout/i,
|
|
20
|
+
/network/i,
|
|
21
|
+
/connection/i,
|
|
22
|
+
/temporary/i,
|
|
23
|
+
/rate limit/i,
|
|
24
|
+
/too many requests/i,
|
|
25
|
+
/service unavailable/i,
|
|
26
|
+
/bad gateway/i
|
|
27
|
+
], circuitBreakerThreshold = 5, circuitBreakerTimeout = 60000, onRetryAttempt, onRetryExhausted, onCircuitBreakerOpen, onCircuitBreakerClose } = options;
|
|
28
|
+
if (!wrappedTransport) {
|
|
29
|
+
throw new Error('RetryTransport requires a wrappedTransport');
|
|
30
|
+
}
|
|
31
|
+
this.wrappedTransport = wrappedTransport;
|
|
32
|
+
this.maxAttempts = maxAttempts;
|
|
33
|
+
this.baseDelay = baseDelay;
|
|
34
|
+
this.maxDelay = maxDelay;
|
|
35
|
+
this.backoffMultiplier = backoffMultiplier;
|
|
36
|
+
this.jitter = jitter;
|
|
37
|
+
this.retryableErrorCodes = new Set(retryableErrorCodes);
|
|
38
|
+
this.retryableErrorPatterns = retryableErrorPatterns;
|
|
39
|
+
this.circuitBreakerThreshold = circuitBreakerThreshold;
|
|
40
|
+
this.circuitBreakerTimeout = circuitBreakerTimeout;
|
|
41
|
+
this.onRetryAttempt = onRetryAttempt;
|
|
42
|
+
this.onRetryExhausted = onRetryExhausted;
|
|
43
|
+
this.onCircuitBreakerOpen = onCircuitBreakerOpen;
|
|
44
|
+
this.onCircuitBreakerClose = onCircuitBreakerClose;
|
|
45
|
+
}
|
|
46
|
+
write(data, formatter) {
|
|
47
|
+
setImmediate(async () => {
|
|
48
|
+
try {
|
|
49
|
+
await this.writeWithRetry(data, formatter);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
this.emit('error', { type: 'retry_transport_exhausted', error });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async writeAsync(data, formatter) {
|
|
57
|
+
return this.writeWithRetry(data, formatter);
|
|
58
|
+
}
|
|
59
|
+
async writeWithRetry(data, formatter) {
|
|
60
|
+
if (this.isCircuitBreakerOpen()) {
|
|
61
|
+
throw new Error('Circuit breaker is open - rejecting requests');
|
|
62
|
+
}
|
|
63
|
+
let lastError = null;
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
|
|
66
|
+
try {
|
|
67
|
+
if (this.wrappedTransport.writeAsync) {
|
|
68
|
+
await this.wrappedTransport.writeAsync(data, formatter);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await new Promise((resolve, reject) => {
|
|
72
|
+
try {
|
|
73
|
+
this.wrappedTransport.write(data, formatter);
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
reject(error);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
this.resetFailureCount();
|
|
82
|
+
this.maybeCloseCircuitBreaker();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
lastError = error;
|
|
87
|
+
if (!this.isRetryableError(lastError) || attempt === this.maxAttempts) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
const delay = this.calculateDelay(attempt);
|
|
91
|
+
const retryContext = {
|
|
92
|
+
attempt,
|
|
93
|
+
totalAttempts: this.maxAttempts,
|
|
94
|
+
originalError: lastError,
|
|
95
|
+
delay,
|
|
96
|
+
startTime
|
|
97
|
+
};
|
|
98
|
+
this.emit('retryAttempt', retryContext);
|
|
99
|
+
this.onRetryAttempt?.(attempt, lastError, delay);
|
|
100
|
+
await this.delay(delay);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
this.incrementFailureCount();
|
|
104
|
+
this.maybeOpenCircuitBreaker();
|
|
105
|
+
const errorContext = {
|
|
106
|
+
lastError,
|
|
107
|
+
attempts: this.maxAttempts,
|
|
108
|
+
totalTime: Date.now() - startTime,
|
|
109
|
+
data: {
|
|
110
|
+
level: data.level,
|
|
111
|
+
message: data.message,
|
|
112
|
+
timestamp: data.timestamp
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
this.emit('retryExhausted', errorContext);
|
|
116
|
+
this.onRetryExhausted?.(lastError, this.maxAttempts);
|
|
117
|
+
throw lastError;
|
|
118
|
+
}
|
|
119
|
+
isRetryableError(error) {
|
|
120
|
+
const errorCode = error.code;
|
|
121
|
+
const errorMessage = error.message;
|
|
122
|
+
if (errorCode && this.retryableErrorCodes.has(errorCode)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
for (const pattern of this.retryableErrorPatterns) {
|
|
126
|
+
if (pattern.test(errorMessage)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
calculateDelay(attempt) {
|
|
133
|
+
let delay = this.baseDelay * Math.pow(this.backoffMultiplier, attempt - 1);
|
|
134
|
+
delay = Math.min(delay, this.maxDelay);
|
|
135
|
+
if (this.jitter) {
|
|
136
|
+
const jitterAmount = delay * 0.25;
|
|
137
|
+
const jitter = (Math.random() * 2 - 1) * jitterAmount;
|
|
138
|
+
delay += jitter;
|
|
139
|
+
}
|
|
140
|
+
return Math.max(0, Math.floor(delay));
|
|
141
|
+
}
|
|
142
|
+
delay(ms) {
|
|
143
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
144
|
+
}
|
|
145
|
+
isCircuitBreakerOpen() {
|
|
146
|
+
if (this.circuitBreakerState === CircuitBreakerState.OPEN) {
|
|
147
|
+
if (Date.now() - this.circuitBreakerOpenTime >= this.circuitBreakerTimeout) {
|
|
148
|
+
this.circuitBreakerState = CircuitBreakerState.HALF_OPEN;
|
|
149
|
+
this.emit('circuitBreakerHalfOpen');
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
incrementFailureCount() {
|
|
157
|
+
this.failureCount++;
|
|
158
|
+
this.maybeOpenCircuitBreaker();
|
|
159
|
+
}
|
|
160
|
+
resetFailureCount() {
|
|
161
|
+
this.failureCount = 0;
|
|
162
|
+
}
|
|
163
|
+
maybeOpenCircuitBreaker() {
|
|
164
|
+
if (this.circuitBreakerState === CircuitBreakerState.CLOSED &&
|
|
165
|
+
this.failureCount >= this.circuitBreakerThreshold) {
|
|
166
|
+
this.circuitBreakerState = CircuitBreakerState.OPEN;
|
|
167
|
+
this.circuitBreakerOpenTime = Date.now();
|
|
168
|
+
this.emit('circuitBreakerOpen');
|
|
169
|
+
this.onCircuitBreakerOpen?.();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
maybeCloseCircuitBreaker() {
|
|
173
|
+
if (this.circuitBreakerState === CircuitBreakerState.HALF_OPEN) {
|
|
174
|
+
this.circuitBreakerState = CircuitBreakerState.CLOSED;
|
|
175
|
+
this.failureCount = 0;
|
|
176
|
+
this.emit('circuitBreakerClose');
|
|
177
|
+
this.onCircuitBreakerClose?.();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
getCircuitBreakerState() {
|
|
181
|
+
return this.circuitBreakerState;
|
|
182
|
+
}
|
|
183
|
+
getFailureCount() {
|
|
184
|
+
return this.failureCount;
|
|
185
|
+
}
|
|
186
|
+
resetCircuitBreaker() {
|
|
187
|
+
this.circuitBreakerState = CircuitBreakerState.CLOSED;
|
|
188
|
+
this.failureCount = 0;
|
|
189
|
+
this.circuitBreakerOpenTime = 0;
|
|
190
|
+
this.emit('circuitBreakerReset');
|
|
191
|
+
}
|
|
192
|
+
getWrappedTransport() {
|
|
193
|
+
return this.wrappedTransport;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -3,3 +3,6 @@ export * from "./ConsoleTransport.js";
|
|
|
3
3
|
export * from "./FileTransport.js";
|
|
4
4
|
export * from "./HttpTransport.js";
|
|
5
5
|
export * from "./FilterableTransport.js";
|
|
6
|
+
export * from "./RetryTransport.js";
|
|
7
|
+
export * from "./CircuitBreakerTransport.js";
|
|
8
|
+
export * from "./DeadLetterQueue.js";
|
|
@@ -3,3 +3,6 @@ export * from "./ConsoleTransport.js";
|
|
|
3
3
|
export * from "./FileTransport.js";
|
|
4
4
|
export * from "./HttpTransport.js";
|
|
5
5
|
export * from "./FilterableTransport.js";
|
|
6
|
+
export * from "./RetryTransport.js";
|
|
7
|
+
export * from "./CircuitBreakerTransport.js";
|
|
8
|
+
export * from "./DeadLetterQueue.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class ColorUtil {
|
|
2
|
+
private static readonly ANSI_COLORS;
|
|
3
|
+
static colorize(text: string, color: string): string;
|
|
4
|
+
}
|
|
5
|
+
export declare class TimeUtil {
|
|
6
|
+
static format(date: Date, format: string): string;
|
|
7
|
+
}
|
|
8
|
+
export declare class Timer {
|
|
9
|
+
private startTime;
|
|
10
|
+
private name;
|
|
11
|
+
private logFn;
|
|
12
|
+
private hasEnded;
|
|
13
|
+
constructor(name: string, logFn: (message: string) => void);
|
|
14
|
+
end(): void;
|
|
15
|
+
}
|