zario 0.2.11 → 0.3.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/README.md +10 -1
- package/dist/cjs/core/CustomLogLevel.js +2 -0
- package/dist/cjs/core/Formatter.js +75 -0
- package/dist/cjs/core/LogLevel.js +2 -0
- package/dist/cjs/core/Logger.js +234 -0
- package/dist/cjs/index.js +19 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/transports/ConsoleTransport.js +39 -0
- package/dist/cjs/transports/FileTransport.js +260 -0
- package/dist/cjs/transports/HttpTransport.js +150 -0
- package/dist/cjs/transports/Transport.js +2 -0
- package/dist/cjs/transports/index.js +20 -0
- package/dist/cjs/utils/ColorUtil.js +42 -0
- package/dist/cjs/utils/TimeUtil.js +26 -0
- package/dist/cjs/utils/Timerutil.js +22 -0
- package/dist/{core → esm/core}/CustomLogLevel.d.ts +1 -1
- package/dist/esm/core/CustomLogLevel.js +1 -0
- package/dist/{core → esm/core}/Formatter.d.ts +1 -1
- package/dist/esm/core/Formatter.js +71 -0
- package/dist/esm/core/LogLevel.js +1 -0
- package/dist/{core → esm/core}/Logger.d.ts +4 -5
- package/dist/esm/core/Logger.js +230 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.js +13 -0
- package/dist/{transports → esm/transports}/ConsoleTransport.d.ts +3 -3
- package/dist/esm/transports/ConsoleTransport.js +35 -0
- package/dist/{transports → esm/transports}/FileTransport.d.ts +7 -5
- package/dist/esm/transports/FileTransport.js +223 -0
- package/dist/{transports → esm/transports}/HttpTransport.d.ts +3 -3
- package/dist/esm/transports/HttpTransport.js +113 -0
- package/dist/{transports → esm/transports}/Transport.d.ts +2 -2
- package/dist/esm/transports/Transport.js +1 -0
- package/dist/esm/transports/index.d.ts +4 -0
- package/dist/esm/transports/index.js +4 -0
- package/dist/esm/utils/ColorUtil.js +38 -0
- package/dist/esm/utils/TimeUtil.js +22 -0
- package/dist/esm/utils/Timerutil.js +18 -0
- package/package.json +19 -8
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -3
- package/dist/index.mjs +0 -3
- package/dist/transports/index.d.ts +0 -28
- /package/dist/{core → esm/core}/LogLevel.d.ts +0 -0
- /package/dist/{utils → esm/utils}/ColorUtil.d.ts +0 -0
- /package/dist/{utils → esm/utils}/TimeUtil.d.ts +0 -0
- /package/dist/{utils → esm/utils}/Timerutil.d.ts +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { Formatter } from "./Formatter.js";
|
|
2
|
+
import { ConsoleTransport } from "../transports/ConsoleTransport.js";
|
|
3
|
+
export class Logger {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.transports = [];
|
|
6
|
+
const { level, colorize, json, transports = [], timestampFormat = "YYYY-MM-DD HH:mm:ss", prefix, timestamp, context = {}, parent, asyncMode, customLevels = {}, customColors = {}, } = options;
|
|
7
|
+
this.parent = parent; // Set parent
|
|
8
|
+
this.context = { ...context }; // Init context
|
|
9
|
+
this.customLevels = customLevels; // custom log store
|
|
10
|
+
this.asyncMode = false;
|
|
11
|
+
if (this.parent) {
|
|
12
|
+
this.level = level ?? this.parent.level;
|
|
13
|
+
this.prefix = prefix ?? this.parent.prefix;
|
|
14
|
+
this.timestamp = timestamp ?? this.parent.timestamp;
|
|
15
|
+
this.asyncMode = asyncMode ?? this.parent.asyncMode;
|
|
16
|
+
this.transports =
|
|
17
|
+
transports && transports.length > 0
|
|
18
|
+
? this.initTransports(transports)
|
|
19
|
+
: this.parent.transports;
|
|
20
|
+
// Merge colors; child overrides parent
|
|
21
|
+
const mergedCColors = {
|
|
22
|
+
...this.parent.formatter.getCustomColors(),
|
|
23
|
+
...customColors,
|
|
24
|
+
};
|
|
25
|
+
this.formatter = new Formatter({
|
|
26
|
+
colorize: this.getDefaultColorizeValue(colorize) ??
|
|
27
|
+
this.parent.formatter.isColorized(),
|
|
28
|
+
json: json ?? this.parent.formatter.isJson(),
|
|
29
|
+
timestampFormat: timestampFormat ?? this.parent.formatter.getTimestampFormat(),
|
|
30
|
+
timestamp: timestamp ?? this.parent.formatter.hasTimestamp(),
|
|
31
|
+
customColors: mergedCColors,
|
|
32
|
+
});
|
|
33
|
+
this.context = { ...this.parent.context, ...this.context };
|
|
34
|
+
// Merge custom levels with parent's custom levels
|
|
35
|
+
this.customLevels = { ...this.parent.customLevels, ...customLevels };
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Auto-configure based on environment
|
|
39
|
+
const isProd = this.isProductionEnvironment();
|
|
40
|
+
this.level = level ?? this.getDefaultLevel(isProd);
|
|
41
|
+
this.prefix = prefix ?? "";
|
|
42
|
+
this.timestamp = timestamp ?? this.getDefaultTimestamp(isProd);
|
|
43
|
+
const defaultTransports = transports && transports.length > 0
|
|
44
|
+
? transports
|
|
45
|
+
: this.getDefaultTransports(isProd);
|
|
46
|
+
this.asyncMode = asyncMode ?? this.getDefaultAsyncMode(isProd);
|
|
47
|
+
this.transports = this.initTransports(defaultTransports);
|
|
48
|
+
this.formatter = new Formatter({
|
|
49
|
+
colorize: this.getDefaultColorizeValue(colorize),
|
|
50
|
+
json: json ?? this.getDefaultJson(isProd),
|
|
51
|
+
timestampFormat,
|
|
52
|
+
timestamp: this.getDefaultTimestamp(isProd),
|
|
53
|
+
customColors,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (!Logger._global) {
|
|
57
|
+
Logger._global = this;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
isProductionEnvironment() {
|
|
61
|
+
const env = process.env.NODE_ENV?.toLowerCase();
|
|
62
|
+
return env === "production" || env === "prod";
|
|
63
|
+
}
|
|
64
|
+
getDefaultLevel(isProd) {
|
|
65
|
+
return isProd ? "warn" : "debug";
|
|
66
|
+
}
|
|
67
|
+
getDefaultColorizeValue(colorize) {
|
|
68
|
+
if (colorize !== undefined) {
|
|
69
|
+
return colorize;
|
|
70
|
+
}
|
|
71
|
+
const isProd = this.isProductionEnvironment();
|
|
72
|
+
return !isProd;
|
|
73
|
+
}
|
|
74
|
+
getDefaultJson(isProd) {
|
|
75
|
+
return isProd;
|
|
76
|
+
}
|
|
77
|
+
getDefaultTimestamp(isProd) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
getDefaultTransports(isProd) {
|
|
81
|
+
if (Logger.defaultTransportsFactory) {
|
|
82
|
+
return Logger.defaultTransportsFactory(isProd);
|
|
83
|
+
}
|
|
84
|
+
return [new ConsoleTransport()];
|
|
85
|
+
}
|
|
86
|
+
getDefaultAsyncMode(isProd) {
|
|
87
|
+
return isProd;
|
|
88
|
+
}
|
|
89
|
+
initTransports(transportConfigs) {
|
|
90
|
+
const initializedTransports = [];
|
|
91
|
+
for (const transportConfig of transportConfigs) {
|
|
92
|
+
if (this.isTransport(transportConfig)) {
|
|
93
|
+
initializedTransports.push(transportConfig);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return initializedTransports;
|
|
97
|
+
}
|
|
98
|
+
isTransport(transport) {
|
|
99
|
+
return (typeof transport === "object" &&
|
|
100
|
+
transport !== null &&
|
|
101
|
+
typeof transport.write === "function");
|
|
102
|
+
}
|
|
103
|
+
shouldLog(level) {
|
|
104
|
+
// Get the priority of the current logger level
|
|
105
|
+
const currentLevelPriority = this.getLevelPriority(this.level);
|
|
106
|
+
// Get the priority of the message level
|
|
107
|
+
const messageLevelPriority = this.getLevelPriority(level);
|
|
108
|
+
return messageLevelPriority >= currentLevelPriority;
|
|
109
|
+
}
|
|
110
|
+
getLevelPriority(level) {
|
|
111
|
+
// use a static map to avoid repeated allocations
|
|
112
|
+
if (Logger.LEVEL_PRIORITIES.hasOwnProperty(level)) {
|
|
113
|
+
return Logger.LEVEL_PRIORITIES[level];
|
|
114
|
+
}
|
|
115
|
+
// Check if it's a custom level
|
|
116
|
+
if (this.customLevels && level in this.customLevels) {
|
|
117
|
+
const customPriority = this.customLevels[level];
|
|
118
|
+
return customPriority !== undefined ? customPriority : 999;
|
|
119
|
+
}
|
|
120
|
+
return 999;
|
|
121
|
+
}
|
|
122
|
+
log(level, message, metadata) {
|
|
123
|
+
if (!this.shouldLog(level) || level === "silent") {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const timestamp = new Date();
|
|
127
|
+
// Optimize metadata merging
|
|
128
|
+
let finalMetadata;
|
|
129
|
+
const hasContext = this.context && Object.keys(this.context).length > 0;
|
|
130
|
+
if (hasContext && metadata) {
|
|
131
|
+
finalMetadata = { ...this.context, ...metadata };
|
|
132
|
+
}
|
|
133
|
+
else if (hasContext) {
|
|
134
|
+
finalMetadata = this.context;
|
|
135
|
+
}
|
|
136
|
+
else if (metadata) {
|
|
137
|
+
finalMetadata = metadata;
|
|
138
|
+
}
|
|
139
|
+
// Only add metadata if it's not empty after merging
|
|
140
|
+
const logData = {
|
|
141
|
+
level,
|
|
142
|
+
message,
|
|
143
|
+
timestamp,
|
|
144
|
+
metadata: finalMetadata && Object.keys(finalMetadata).length > 0
|
|
145
|
+
? finalMetadata
|
|
146
|
+
: undefined,
|
|
147
|
+
prefix: this.prefix,
|
|
148
|
+
};
|
|
149
|
+
if (this.asyncMode) {
|
|
150
|
+
for (const transport of this.transports) {
|
|
151
|
+
if (transport.writeAsync) {
|
|
152
|
+
transport.writeAsync(logData, this.formatter).catch((error) => {
|
|
153
|
+
console.error("Error during async logging:", error);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
setImmediate(() => {
|
|
158
|
+
transport.write(logData, this.formatter);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
for (const transport of this.transports) {
|
|
165
|
+
transport.write(logData, this.formatter);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
debug(message, metadata) {
|
|
170
|
+
this.log("debug", message, metadata);
|
|
171
|
+
}
|
|
172
|
+
info(message, metadata) {
|
|
173
|
+
this.log("info", message, metadata);
|
|
174
|
+
}
|
|
175
|
+
warn(message, metadata) {
|
|
176
|
+
this.log("warn", message, metadata);
|
|
177
|
+
}
|
|
178
|
+
error(message, metadata) {
|
|
179
|
+
this.log("error", message, metadata);
|
|
180
|
+
}
|
|
181
|
+
silent(message, metadata) {
|
|
182
|
+
this.log("silent", message, metadata);
|
|
183
|
+
}
|
|
184
|
+
boring(message, metadata) {
|
|
185
|
+
this.log("boring", message, metadata);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Generic log method that allows logging with custom levels
|
|
189
|
+
*/
|
|
190
|
+
logWithLevel(level, message, metadata) {
|
|
191
|
+
this.log(level, message, metadata);
|
|
192
|
+
}
|
|
193
|
+
setLevel(level) {
|
|
194
|
+
this.level = level;
|
|
195
|
+
}
|
|
196
|
+
setFormat(format) {
|
|
197
|
+
this.formatter.setJson(format === "json");
|
|
198
|
+
}
|
|
199
|
+
setAsyncMode(asyncMode) {
|
|
200
|
+
this.asyncMode = asyncMode;
|
|
201
|
+
}
|
|
202
|
+
addTransport(transport) {
|
|
203
|
+
this.transports.push(transport);
|
|
204
|
+
}
|
|
205
|
+
getTimestampSetting() {
|
|
206
|
+
return this.timestamp;
|
|
207
|
+
}
|
|
208
|
+
static get global() {
|
|
209
|
+
if (!Logger._global) {
|
|
210
|
+
Logger._global = new Logger();
|
|
211
|
+
}
|
|
212
|
+
return Logger._global;
|
|
213
|
+
}
|
|
214
|
+
createChild(options = {}) {
|
|
215
|
+
return new Logger({ ...options, parent: this });
|
|
216
|
+
}
|
|
217
|
+
startTimer(name) {
|
|
218
|
+
const { Timer } = require("../utils/Timerutil");
|
|
219
|
+
return new Timer(name, (message) => this.info(message));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
Logger.defaultTransportsFactory = null;
|
|
223
|
+
Logger.LEVEL_PRIORITIES = {
|
|
224
|
+
silent: 0,
|
|
225
|
+
boring: 1,
|
|
226
|
+
debug: 2,
|
|
227
|
+
info: 3,
|
|
228
|
+
warn: 4,
|
|
229
|
+
error: 5,
|
|
230
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Logger } from './core/Logger.js';
|
|
2
|
+
import { LogLevel } from './core/LogLevel.js';
|
|
3
|
+
import { ConsoleTransport, FileTransport, HttpTransport, Transport } from './transports/index.js';
|
|
4
|
+
import { TransportConfig, LoggerConfig } from './types/index.js';
|
|
5
|
+
import { CustomLogLevelConfig } from './core/CustomLogLevel.js';
|
|
6
|
+
export { Logger, ConsoleTransport, FileTransport, HttpTransport };
|
|
7
|
+
export type { LogLevel, Transport, TransportConfig, LoggerConfig, CustomLogLevelConfig };
|
|
8
|
+
export default Logger;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Logger } from './core/Logger.js';
|
|
2
|
+
import { ConsoleTransport, FileTransport, HttpTransport } from './transports/index.js';
|
|
3
|
+
// Configure default transports to maintain backward compatibility
|
|
4
|
+
Logger.defaultTransportsFactory = (isProd) => {
|
|
5
|
+
if (isProd) {
|
|
6
|
+
return [new ConsoleTransport(), new FileTransport({ path: "./logs/app.log" })];
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
return [new ConsoleTransport()];
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export { Logger, ConsoleTransport, FileTransport, HttpTransport };
|
|
13
|
+
export default Logger;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Transport } from "./Transport";
|
|
2
|
-
import { LogData } from "../types";
|
|
3
|
-
import { Formatter } from "../core/Formatter";
|
|
1
|
+
import { Transport } from "./Transport.js";
|
|
2
|
+
import { LogData } from "../types/index.js";
|
|
3
|
+
import { Formatter } from "../core/Formatter.js";
|
|
4
4
|
export interface ConsoleTransportOptions {
|
|
5
5
|
colorize?: boolean;
|
|
6
6
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class ConsoleTransport {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
const { colorize = true } = options;
|
|
4
|
+
this.colorize = colorize;
|
|
5
|
+
}
|
|
6
|
+
write(data, formatter) {
|
|
7
|
+
// Toggle colorize temporarily, then restore it
|
|
8
|
+
const originalColorizeSetting = formatter["colorize"];
|
|
9
|
+
if (this.colorize !== originalColorizeSetting) {
|
|
10
|
+
formatter["colorize"] = this.colorize;
|
|
11
|
+
}
|
|
12
|
+
const output = formatter.format(data);
|
|
13
|
+
// Restore
|
|
14
|
+
if (this.colorize !== originalColorizeSetting) {
|
|
15
|
+
formatter["colorize"] = originalColorizeSetting;
|
|
16
|
+
}
|
|
17
|
+
switch (data.level) {
|
|
18
|
+
case "error":
|
|
19
|
+
console.error(output);
|
|
20
|
+
break;
|
|
21
|
+
case "warn":
|
|
22
|
+
console.warn(output);
|
|
23
|
+
break;
|
|
24
|
+
default:
|
|
25
|
+
console.log(output);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async writeAsync(data, formatter) {
|
|
30
|
+
setImmediate(() => {
|
|
31
|
+
this.write(data, formatter);
|
|
32
|
+
});
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Transport } from "./Transport";
|
|
2
|
-
import { LogData } from "../types";
|
|
3
|
-
import { Formatter } from "../core/Formatter";
|
|
1
|
+
import { Transport } from "./Transport.js";
|
|
2
|
+
import { LogData } from "../types/index.js";
|
|
3
|
+
import { Formatter } from "../core/Formatter.js";
|
|
4
4
|
export type CompressionType = "gzip" | "deflate" | "none";
|
|
5
5
|
export interface FileTransportOptions {
|
|
6
6
|
path: string;
|
|
@@ -26,11 +26,13 @@ export declare class FileTransport implements Transport {
|
|
|
26
26
|
constructor(options: FileTransportOptions);
|
|
27
27
|
write(data: LogData, formatter: Formatter): void;
|
|
28
28
|
writeAsync(data: LogData, formatter: Formatter): Promise<void>;
|
|
29
|
-
private
|
|
30
|
-
private rotateIfNeededAsync;
|
|
29
|
+
private shouldRotate;
|
|
31
30
|
private rotateFiles;
|
|
32
31
|
private rotateFilesAsync;
|
|
32
|
+
private performRotation;
|
|
33
|
+
private performRotationAsync;
|
|
33
34
|
private getRotatedFilePath;
|
|
35
|
+
private filterRotatedFiles;
|
|
34
36
|
private cleanupOldFiles;
|
|
35
37
|
private cleanupOldFilesAsync;
|
|
36
38
|
private startBatching;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as zlib from "zlib";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
const compressGzip = promisify(zlib.gzip);
|
|
6
|
+
const compressDeflate = promisify(zlib.deflate);
|
|
7
|
+
export class FileTransport {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
// batching funct
|
|
10
|
+
this.batchQueue = [];
|
|
11
|
+
this.batchTimer = null;
|
|
12
|
+
const { path: filePath, maxSize = 10 * 1024 * 1024, maxFiles = 5, compression = "none", batchInterval = 0, // no batching
|
|
13
|
+
compressOldFiles = true, } = options;
|
|
14
|
+
this.filePath = filePath;
|
|
15
|
+
this.maxSize = maxSize;
|
|
16
|
+
this.maxFiles = maxFiles;
|
|
17
|
+
this.compression = compression;
|
|
18
|
+
this.batchInterval = batchInterval;
|
|
19
|
+
this.compressOldFiles = compressOldFiles;
|
|
20
|
+
const dir = path.dirname(this.filePath);
|
|
21
|
+
if (!fs.existsSync(dir)) {
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
if (!fs.existsSync(this.filePath)) {
|
|
25
|
+
fs.writeFileSync(this.filePath, "", "utf8");
|
|
26
|
+
}
|
|
27
|
+
// Start batching if an interval is set
|
|
28
|
+
if (batchInterval > 0) {
|
|
29
|
+
this.startBatching();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
write(data, formatter) {
|
|
33
|
+
const output = formatter.format(data);
|
|
34
|
+
const formattedOutput = output + "\n";
|
|
35
|
+
if (this.batchInterval > 0) {
|
|
36
|
+
// Queue entry if batching is enabled
|
|
37
|
+
this.batchQueue.push({
|
|
38
|
+
data: formattedOutput,
|
|
39
|
+
timestamp: new Date(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Write immediately when batching is disabled
|
|
44
|
+
fs.appendFileSync(this.filePath, formattedOutput);
|
|
45
|
+
if (this.shouldRotate()) {
|
|
46
|
+
this.rotateFiles();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async writeAsync(data, formatter) {
|
|
51
|
+
const formattedOutput = formatter.format(data) + "\n";
|
|
52
|
+
if (this.batchInterval > 0) {
|
|
53
|
+
this.batchQueue.push({
|
|
54
|
+
data: formattedOutput,
|
|
55
|
+
timestamp: new Date(),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
try {
|
|
60
|
+
await fs.promises.appendFile(this.filePath, formattedOutput);
|
|
61
|
+
if (this.shouldRotate()) {
|
|
62
|
+
await this.rotateFilesAsync();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
shouldRotate() {
|
|
71
|
+
if (!fs.existsSync(this.filePath))
|
|
72
|
+
return false;
|
|
73
|
+
try {
|
|
74
|
+
return fs.statSync(this.filePath).size >= this.maxSize;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
rotateFiles() {
|
|
81
|
+
try {
|
|
82
|
+
if (!fs.existsSync(this.filePath))
|
|
83
|
+
return;
|
|
84
|
+
const currentContent = fs.readFileSync(this.filePath, "utf8");
|
|
85
|
+
this.performRotation(currentContent, fs.writeFileSync);
|
|
86
|
+
this.cleanupOldFiles();
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error("Error during file rotation:", error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async rotateFilesAsync() {
|
|
93
|
+
try {
|
|
94
|
+
if (!fs.existsSync(this.filePath))
|
|
95
|
+
return;
|
|
96
|
+
const currentContent = await fs.promises.readFile(this.filePath, "utf8");
|
|
97
|
+
await this.performRotationAsync(currentContent);
|
|
98
|
+
await this.cleanupOldFilesAsync();
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error("Error during async file rotation:", error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
performRotation(content, writeFn) {
|
|
105
|
+
let rotatedFilePath = this.getRotatedFilePath();
|
|
106
|
+
if (this.compression !== "none" && this.compressOldFiles) {
|
|
107
|
+
rotatedFilePath += `.${this.compression === "gzip" ? "gz" : "zz"}`;
|
|
108
|
+
const compressed = this.compression === "gzip" ? zlib.gzipSync(content) : zlib.deflateSync(content);
|
|
109
|
+
writeFn(rotatedFilePath, compressed);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
writeFn(rotatedFilePath, content, "utf8");
|
|
113
|
+
}
|
|
114
|
+
writeFn(this.filePath, "", "utf8");
|
|
115
|
+
}
|
|
116
|
+
async performRotationAsync(content) {
|
|
117
|
+
let rotatedFilePath = this.getRotatedFilePath();
|
|
118
|
+
if (this.compression !== "none" && this.compressOldFiles) {
|
|
119
|
+
rotatedFilePath += `.${this.compression === "gzip" ? "gz" : "zz"}`;
|
|
120
|
+
const compressed = this.compression === "gzip" ? await compressGzip(content) : await compressDeflate(content);
|
|
121
|
+
await fs.promises.writeFile(rotatedFilePath, compressed);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
await fs.promises.writeFile(rotatedFilePath, content, "utf8");
|
|
125
|
+
}
|
|
126
|
+
await fs.promises.writeFile(this.filePath, "", "utf8");
|
|
127
|
+
}
|
|
128
|
+
getRotatedFilePath() {
|
|
129
|
+
const dir = path.dirname(this.filePath);
|
|
130
|
+
return path.join(dir, `${path.basename(this.filePath)}.${Date.now()}`);
|
|
131
|
+
}
|
|
132
|
+
filterRotatedFiles(files, baseName) {
|
|
133
|
+
return files
|
|
134
|
+
.filter(f => f !== baseName && f.startsWith(baseName + "."))
|
|
135
|
+
.sort((a, b) => {
|
|
136
|
+
const getTs = (s) => parseInt(s.slice(baseName.length + 1).split(".")[0] ?? "0");
|
|
137
|
+
return getTs(b) - getTs(a);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
cleanupOldFiles() {
|
|
141
|
+
const dir = path.dirname(this.filePath);
|
|
142
|
+
const baseName = path.basename(this.filePath);
|
|
143
|
+
try {
|
|
144
|
+
const files = fs.readdirSync(dir);
|
|
145
|
+
const rotated = this.filterRotatedFiles(files, baseName);
|
|
146
|
+
for (let i = this.maxFiles; i < rotated.length; i++) {
|
|
147
|
+
const file = rotated[i];
|
|
148
|
+
if (file) {
|
|
149
|
+
try {
|
|
150
|
+
fs.unlinkSync(path.join(dir, file));
|
|
151
|
+
}
|
|
152
|
+
catch { }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch { }
|
|
157
|
+
}
|
|
158
|
+
async cleanupOldFilesAsync() {
|
|
159
|
+
const dir = path.dirname(this.filePath);
|
|
160
|
+
const baseName = path.basename(this.filePath);
|
|
161
|
+
try {
|
|
162
|
+
const files = await fs.promises.readdir(dir);
|
|
163
|
+
const rotated = this.filterRotatedFiles(files, baseName);
|
|
164
|
+
await Promise.all(rotated.slice(this.maxFiles).map(f => fs.promises.unlink(path.join(dir, f)).catch(() => { })));
|
|
165
|
+
}
|
|
166
|
+
catch { }
|
|
167
|
+
}
|
|
168
|
+
startBatching() {
|
|
169
|
+
if (this.batchInterval > 0) {
|
|
170
|
+
this.batchTimer = setInterval(() => {
|
|
171
|
+
this.processBatch().catch((error) => {
|
|
172
|
+
console.error("Error in batch processing timer:", error);
|
|
173
|
+
});
|
|
174
|
+
}, this.batchInterval);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async processBatch() {
|
|
178
|
+
if (this.batchQueue.length === 0) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Atomically capture and clear queue
|
|
182
|
+
const currentBatch = this.batchQueue;
|
|
183
|
+
this.batchQueue = [];
|
|
184
|
+
// Combine queued entries into one batch
|
|
185
|
+
const batchContent = currentBatch.map((entry) => entry.data).join("");
|
|
186
|
+
try {
|
|
187
|
+
await new Promise((resolve, reject) => {
|
|
188
|
+
fs.appendFile(this.filePath, batchContent, (err) => {
|
|
189
|
+
if (err) {
|
|
190
|
+
reject(err);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
resolve();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
// Rotate if needed after writing
|
|
197
|
+
if (this.shouldRotate()) {
|
|
198
|
+
await this.rotateFilesAsync();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
console.error("Error processing log batch:", error);
|
|
203
|
+
// On error, restore entries for retry (prepend to preserve order)
|
|
204
|
+
this.batchQueue = [...currentBatch, ...this.batchQueue];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Clean up resources when the transport is disposed
|
|
208
|
+
async destroy() {
|
|
209
|
+
if (this.batchTimer) {
|
|
210
|
+
clearInterval(this.batchTimer);
|
|
211
|
+
this.batchTimer = null;
|
|
212
|
+
}
|
|
213
|
+
// Flush remaining queued entries
|
|
214
|
+
if (this.batchQueue.length > 0) {
|
|
215
|
+
try {
|
|
216
|
+
await this.processBatch();
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error("Error processing final batch:", error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Transport } from "./Transport";
|
|
2
|
-
import { LogData } from "../types";
|
|
3
|
-
import { Formatter } from "../core/Formatter";
|
|
1
|
+
import { Transport } from "./Transport.js";
|
|
2
|
+
import { LogData } from "../types/index.js";
|
|
3
|
+
import { Formatter } from "../core/Formatter.js";
|
|
4
4
|
export interface HttpTransportOptions {
|
|
5
5
|
url: string;
|
|
6
6
|
method?: string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as http from "http";
|
|
2
|
+
import * as https from "https";
|
|
3
|
+
import * as url from "url";
|
|
4
|
+
export class HttpTransport {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
const { url, method = 'POST', headers = {}, timeout = 5000, retries = 3 // defaults
|
|
7
|
+
} = options;
|
|
8
|
+
if (!url) {
|
|
9
|
+
throw new Error('HttpTransport requires a URL option');
|
|
10
|
+
}
|
|
11
|
+
this.url = url;
|
|
12
|
+
this.method = method.toUpperCase();
|
|
13
|
+
this.headers = { ...headers };
|
|
14
|
+
this.timeout = timeout;
|
|
15
|
+
this.retries = retries;
|
|
16
|
+
// Set default Content-Type if not provided
|
|
17
|
+
if (!this.headers['Content-Type'] && !this.headers['content-type']) {
|
|
18
|
+
this.headers['Content-Type'] = 'application/json';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
write(data, formatter) {
|
|
22
|
+
// Format the data as JSON for HTTP transport
|
|
23
|
+
const logObject = this.parseFormattedData(data);
|
|
24
|
+
const body = JSON.stringify(logObject);
|
|
25
|
+
setImmediate(() => {
|
|
26
|
+
this.sendHttpRequestWithRetry(body, 0)
|
|
27
|
+
.catch((error) => {
|
|
28
|
+
console.error('HttpTransport error (sync mode):', error.message);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async writeAsync(data, formatter) {
|
|
33
|
+
// json formating for HttpTransport
|
|
34
|
+
const logObject = this.parseFormattedData(data);
|
|
35
|
+
const body = JSON.stringify(logObject);
|
|
36
|
+
await this.sendHttpRequestWithRetry(body, this.retries);
|
|
37
|
+
}
|
|
38
|
+
parseFormattedData(originalData) {
|
|
39
|
+
// structured log overide original params
|
|
40
|
+
return {
|
|
41
|
+
level: originalData.level,
|
|
42
|
+
message: originalData.message,
|
|
43
|
+
timestamp: originalData.timestamp.toISOString(),
|
|
44
|
+
...(originalData.prefix && { prefix: originalData.prefix }),
|
|
45
|
+
...(originalData.metadata && { metadata: originalData.metadata })
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async sendHttpRequestWithRetry(body, maxRetries) {
|
|
49
|
+
let lastError = null;
|
|
50
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
51
|
+
try {
|
|
52
|
+
await this.sendHttpRequest(body);
|
|
53
|
+
return; // success then exit
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
lastError = error;
|
|
57
|
+
// stop if last attempt
|
|
58
|
+
if (attempt === maxRetries) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
// timer wait before continue
|
|
62
|
+
await this.delay(Math.pow(2, attempt) * 1000);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (lastError) {
|
|
66
|
+
throw lastError;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
sendHttpRequest(body) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const parsedUrl = new url.URL(this.url);
|
|
72
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
73
|
+
const client = isHttps ? https : http;
|
|
74
|
+
const requestOptions = {
|
|
75
|
+
hostname: parsedUrl.hostname,
|
|
76
|
+
port: parsedUrl.port,
|
|
77
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
78
|
+
method: this.method,
|
|
79
|
+
headers: {
|
|
80
|
+
...this.headers,
|
|
81
|
+
'Content-Length': Buffer.byteLength(body, 'utf8'),
|
|
82
|
+
},
|
|
83
|
+
timeout: this.timeout,
|
|
84
|
+
};
|
|
85
|
+
const req = client.request(requestOptions, (res) => {
|
|
86
|
+
let responseData = '';
|
|
87
|
+
res.on('data', (chunk) => {
|
|
88
|
+
responseData += chunk;
|
|
89
|
+
});
|
|
90
|
+
res.on('end', () => {
|
|
91
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
92
|
+
resolve();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
reject(new Error(`HTTP request failed with status ${res.statusCode}: ${responseData}`));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
req.on('error', (error) => {
|
|
100
|
+
reject(error);
|
|
101
|
+
});
|
|
102
|
+
req.on('timeout', () => {
|
|
103
|
+
req.destroy();
|
|
104
|
+
reject(new Error('Request timeout'));
|
|
105
|
+
});
|
|
106
|
+
req.write(body);
|
|
107
|
+
req.end();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
delay(ms) {
|
|
111
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { LogData } from '../types';
|
|
2
|
-
import { Formatter } from '../core/Formatter';
|
|
1
|
+
import { LogData } from '../types/index.js';
|
|
2
|
+
import { Formatter } from '../core/Formatter.js';
|
|
3
3
|
export interface Transport {
|
|
4
4
|
write(data: LogData, formatter: Formatter): void;
|
|
5
5
|
writeAsync?(data: LogData, formatter: Formatter): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|