fix the max files count and deletion missing ?!?!, fix readme to actually have all the values for the json example, refactor file to class, fix custom not logging to file?, allow public flush
All checks were successful
Code quality checks / biome (push) Successful in 9s
All checks were successful
Code quality checks / biome (push) Successful in 9s
This commit is contained in:
parent
108083e0f5
commit
2c4f3cf5de
6 changed files with 187 additions and 130 deletions
39
README.md
39
README.md
|
@ -71,18 +71,41 @@ constructor > environment > logger.json > defaults
|
|||
{
|
||||
"directory": "logs",
|
||||
"level": "debug",
|
||||
"console": true,
|
||||
"consoleColor": true,
|
||||
"disableFile": false,
|
||||
|
||||
"rotate": true,
|
||||
"maxFiles": 3,
|
||||
"prettyPrint": true,
|
||||
"pattern": "{color:gray}{timestamp}{reset} {color:levelColor}[{level-name}]{reset} ({file-name}:{line}:{column}) {data}",
|
||||
|
||||
"console": true,
|
||||
"consoleColor": true,
|
||||
|
||||
"dateFormat": "yyyy-MM-dd HH:mm:ss.SSS",
|
||||
"timezone": "local",
|
||||
|
||||
"silent": false,
|
||||
|
||||
"pattern": "{color:gray}{timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{line}:{column}{color:gray}){reset} {data}",
|
||||
"levelColor": {
|
||||
"debug": "blue",
|
||||
"info": "green",
|
||||
"warn": "yellow",
|
||||
"error": "red",
|
||||
"fatal": "red"
|
||||
},
|
||||
|
||||
"customPattern": "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}",
|
||||
"customColors": {
|
||||
"GET": "green",
|
||||
"GET": "red",
|
||||
"POST": "blue",
|
||||
"DELETE": "red"
|
||||
}
|
||||
"PUT": "yellow",
|
||||
"DELETE": "red",
|
||||
"PATCH": "cyan",
|
||||
"HEAD": "magenta",
|
||||
"OPTIONS": "white",
|
||||
"TRACE": "gray"
|
||||
},
|
||||
|
||||
"prettyPrint": true
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -158,7 +181,7 @@ The output format is controlled by:
|
|||
|
||||
## Output Examples
|
||||
|
||||
### Console (with colors)
|
||||
### Console
|
||||
|
||||
```
|
||||
2025-05-24 16:15:00.000 [INFO] (index.ts:3:6) Server started
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@atums/echo",
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"version": "1.0.2",
|
||||
"description": "A minimal, flexible logger",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
@ -28,6 +28,7 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bun run build --watch",
|
||||
"build": "rm -rf dist && tsup src/index.ts --dts --out-dir dist --format esm,cjs",
|
||||
"lint": "bunx biome check",
|
||||
"lint:fix": "bunx biome check --fix",
|
||||
|
|
45
src/index.ts
45
src/index.ts
|
@ -16,12 +16,13 @@ import {
|
|||
loadLoggerConfig,
|
||||
logLevelValues,
|
||||
} from "@lib/config";
|
||||
import { writeLogJson } from "@lib/file";
|
||||
import { FileLogger } from "@lib/file";
|
||||
import type { LogLevel, LoggerConfig } from "@types";
|
||||
|
||||
class Echo {
|
||||
private readonly directory: string;
|
||||
private readonly config: Required<LoggerConfig>;
|
||||
private readonly fileLogger: FileLogger | null = null;
|
||||
|
||||
constructor(config?: string | LoggerConfig) {
|
||||
const fileConfig: LoggerConfig =
|
||||
|
@ -45,6 +46,8 @@ class Echo {
|
|||
|
||||
if (!this.config.disableFile) {
|
||||
Echo.validateDirectory(this.directory);
|
||||
|
||||
this.fileLogger = new FileLogger(this.config);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,14 +64,6 @@ class Echo {
|
|||
accessSync(dir, constants.W_OK);
|
||||
}
|
||||
|
||||
public getDirectory(): string {
|
||||
return this.directory;
|
||||
}
|
||||
|
||||
public getConfig(): Required<LoggerConfig> {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
private log(level: LogLevel, data: unknown): void {
|
||||
if (
|
||||
this.config.silent ||
|
||||
|
@ -85,8 +80,8 @@ class Echo {
|
|||
);
|
||||
}
|
||||
|
||||
if (!this.config.disableFile) {
|
||||
writeLogJson(level, data, meta, this.config);
|
||||
if (!this.config.disableFile && this.fileLogger) {
|
||||
this.fileLogger.write(level, data, meta);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,8 +109,8 @@ class Echo {
|
|||
this.log("trace", data);
|
||||
}
|
||||
|
||||
public custom(tag: string, context: string, message: unknown): void {
|
||||
if (this.config.silent || !this.config.console) return;
|
||||
public custom(tag: string, context: string, data: unknown): void {
|
||||
if (this.config.silent) return;
|
||||
|
||||
const timestamps = getTimestamp(this.config);
|
||||
|
||||
|
@ -128,14 +123,14 @@ class Echo {
|
|||
const reset = this.config.consoleColor ? ansiColors.reset : "";
|
||||
|
||||
const resolvedData =
|
||||
this.config.prettyPrint && typeof message === "object" && message !== null
|
||||
? inspect(message, {
|
||||
this.config.prettyPrint && typeof data === "object" && data !== null
|
||||
? inspect(data, {
|
||||
depth: null,
|
||||
colors: this.config.consoleColor,
|
||||
breakLength: 1,
|
||||
compact: false,
|
||||
})
|
||||
: format(message);
|
||||
: format(data);
|
||||
|
||||
const pattern =
|
||||
this.config.customPattern ??
|
||||
|
@ -152,9 +147,25 @@ class Echo {
|
|||
.replace(/{color:contextColor}/g, contextColor)
|
||||
.replace(/{reset}/g, reset);
|
||||
|
||||
if (this.config.console) {
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
if (!this.config.disableFile && this.fileLogger) {
|
||||
const meta = getCallerInfo(this.config);
|
||||
this.fileLogger.write(tag, { context, data }, meta);
|
||||
}
|
||||
}
|
||||
|
||||
public flush(): Promise<void> {
|
||||
return this.fileLogger?.flush() ?? Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function createLogger(config?: string | LoggerConfig): Echo {
|
||||
return new Echo(config);
|
||||
}
|
||||
|
||||
const echo = new Echo();
|
||||
export { echo, Echo };
|
||||
export { echo, Echo, createLogger };
|
||||
export type { LoggerConfig, LogLevel } from "@types";
|
||||
|
|
|
@ -41,13 +41,14 @@ function getCallerInfo(config: Required<LoggerConfig>): {
|
|||
} {
|
||||
const id = generateShortId();
|
||||
|
||||
const timestampInfo = getTimestamp(config);
|
||||
const fallback = {
|
||||
id: id,
|
||||
id,
|
||||
fileName: "unknown",
|
||||
line: "0",
|
||||
timestamp: getTimestamp(config).timestamp,
|
||||
prettyTimestamp: getTimestamp(config).prettyTimestamp,
|
||||
column: "0",
|
||||
timestamp: timestampInfo.timestamp,
|
||||
prettyTimestamp: timestampInfo.prettyTimestamp,
|
||||
};
|
||||
|
||||
const stack = new Error().stack;
|
||||
|
@ -73,8 +74,8 @@ function getCallerInfo(config: Required<LoggerConfig>): {
|
|||
fileName: basename(fullPath),
|
||||
line: lineNumber,
|
||||
column: columnNumber,
|
||||
timestamp: getTimestamp(config).timestamp,
|
||||
prettyTimestamp: getTimestamp(config).prettyTimestamp,
|
||||
timestamp: timestampInfo.timestamp,
|
||||
prettyTimestamp: timestampInfo.prettyTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -97,8 +98,8 @@ function getCallerInfo(config: Required<LoggerConfig>): {
|
|||
fileName: basename(fullPath),
|
||||
line: lineNumber,
|
||||
column: columnNumber,
|
||||
timestamp: getTimestamp(config).timestamp,
|
||||
prettyTimestamp: getTimestamp(config).prettyTimestamp,
|
||||
timestamp: timestampInfo.timestamp,
|
||||
prettyTimestamp: timestampInfo.prettyTimestamp,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
111
src/lib/file.ts
111
src/lib/file.ts
|
@ -3,30 +3,57 @@ import {
|
|||
createWriteStream,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
unlinkSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { LogLevel, LoggerConfig } from "@types";
|
||||
import { format } from "date-fns-tz";
|
||||
|
||||
let currentStream: WriteStream | null = null;
|
||||
let currentFilePath = "";
|
||||
let currentDate = "";
|
||||
class FileLogger {
|
||||
private stream: WriteStream | null = null;
|
||||
private filePath = "";
|
||||
private date = "";
|
||||
|
||||
function getLogFilePath(
|
||||
config: Required<LoggerConfig>,
|
||||
dateStr: string,
|
||||
): string {
|
||||
return join(config.directory, `${dateStr}.jsonl`);
|
||||
constructor(private readonly config: Required<LoggerConfig>) {
|
||||
if (!existsSync(this.config.directory)) {
|
||||
mkdirSync(this.config.directory, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function resetStream(path: string): void {
|
||||
currentStream?.end();
|
||||
currentStream = createWriteStream(path, { flags: "a", encoding: "utf-8" });
|
||||
currentFilePath = path;
|
||||
private getLogFilePath(dateStr: string): string {
|
||||
return join(this.config.directory, `${dateStr}.jsonl`);
|
||||
}
|
||||
|
||||
export function writeLogJson(
|
||||
level: LogLevel,
|
||||
private resetStream(path: string): void {
|
||||
this.stream?.end();
|
||||
this.stream = createWriteStream(path, { flags: "a", encoding: "utf-8" });
|
||||
this.filePath = path;
|
||||
}
|
||||
|
||||
private pruneOldLogs(): void {
|
||||
if (this.config.maxFiles && this.config.maxFiles < 1) {
|
||||
throw new Error("[@atums/echo] maxFiles must be >= 1 if set.");
|
||||
}
|
||||
|
||||
const files = readdirSync(this.config.directory)
|
||||
.filter((file) => /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(file))
|
||||
.sort();
|
||||
|
||||
const excess = files.slice(
|
||||
0,
|
||||
Math.max(0, files.length - this.config.maxFiles),
|
||||
);
|
||||
|
||||
for (const file of excess) {
|
||||
try {
|
||||
unlinkSync(join(this.config.directory, file));
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
public write(
|
||||
level: LogLevel | string,
|
||||
data: unknown,
|
||||
meta: {
|
||||
id: string;
|
||||
|
@ -36,37 +63,28 @@ export function writeLogJson(
|
|||
timestamp: string;
|
||||
prettyTimestamp: string;
|
||||
},
|
||||
config: Required<LoggerConfig>,
|
||||
): void {
|
||||
if (config.disableFile) return;
|
||||
if (this.config.disableFile) return;
|
||||
|
||||
const now = new Date();
|
||||
let path: string;
|
||||
|
||||
if (!existsSync(config.directory)) {
|
||||
mkdirSync(config.directory, { recursive: true });
|
||||
}
|
||||
|
||||
let filePath: string;
|
||||
|
||||
if (config.rotate) {
|
||||
if (this.config.rotate) {
|
||||
const dateStr = format(now, "yyyy-MM-dd", {
|
||||
timeZone: config.timezone,
|
||||
timeZone: this.config.timezone,
|
||||
});
|
||||
filePath = getLogFilePath(config, dateStr);
|
||||
path = this.getLogFilePath(dateStr);
|
||||
|
||||
if (
|
||||
currentStream === null ||
|
||||
currentFilePath !== filePath ||
|
||||
currentDate !== dateStr
|
||||
) {
|
||||
currentDate = dateStr;
|
||||
resetStream(filePath);
|
||||
if (!this.stream || this.filePath !== path || this.date !== dateStr) {
|
||||
this.date = dateStr;
|
||||
this.resetStream(path);
|
||||
this.pruneOldLogs();
|
||||
}
|
||||
} else {
|
||||
filePath = join(config.directory, "log.jsonl");
|
||||
path = join(this.config.directory, "log.jsonl");
|
||||
|
||||
if (currentStream === null || currentFilePath !== filePath) {
|
||||
resetStream(filePath);
|
||||
if (!this.stream || this.filePath !== path) {
|
||||
this.resetStream(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,17 +107,20 @@ export function writeLogJson(
|
|||
: data,
|
||||
})}\n`;
|
||||
|
||||
if (currentStream === null) {
|
||||
throw new Error("Logger stream is not initialized");
|
||||
try {
|
||||
this.stream?.write(line);
|
||||
} catch (err) {
|
||||
if (this.config.console) {
|
||||
throw new Error(`[@atums/echo] Failed to write to log file: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentStream.write(line);
|
||||
public flush(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.stream?.end(resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
process.on("exit", () => {
|
||||
currentStream?.end();
|
||||
});
|
||||
process.on("SIGINT", () => {
|
||||
currentStream?.end();
|
||||
process.exit();
|
||||
});
|
||||
export { FileLogger };
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue