refactor(logger): simplify logger configuration and improve file rotation

This commit is contained in:
2025-11-21 21:15:06 +08:00
parent 0612477325
commit de97fb4858
4 changed files with 65 additions and 71 deletions

View File

@@ -4,6 +4,9 @@
*/ */
import { LogLevel, LoggerOptions, LogEvent, ILogger } from "./types"; import { LogLevel, LoggerOptions, LogEvent, ILogger } from "./types";
import { processor } from "./processors";
import { ConsoleStream } from "./streams";
import { textRenderer } from "./renderers";
/** /**
* The main Logger class that orchestrates the logging pipeline. * The main Logger class that orchestrates the logging pipeline.
@@ -13,26 +16,19 @@ import { LogLevel, LoggerOptions, LogEvent, ILogger } from "./types";
*/ */
export class Logger implements ILogger { export class Logger implements ILogger {
private options: LoggerOptions; private options: LoggerOptions;
private loggerName?: string;
/** /**
* Create a new Logger instance. * Create a new Logger instance.
* *
* @param options - Configuration options for the logger * @param options - Configuration options for the logger
* @param name - The name of the logger
*/ */
constructor(options: Partial<LoggerOptions>) { constructor(options: LoggerOptions, name?: string) {
this.options = { this.options = options;
processors: options.processors ?? [], this.loggerName = name;
renderer: options.renderer ?? this.defaultRenderer,
streams: options.streams ?? [],
};
} }
/**
* Default renderer that returns an empty string.
* Used as fallback when no renderer is provided.
*/
private defaultRenderer = (): string => "";
/** /**
* Main logging method that handles the complete logging pipeline. * Main logging method that handles the complete logging pipeline.
* *
@@ -51,6 +47,8 @@ export class Logger implements ILogger {
["message", message], ["message", message],
...Object.entries(context), ...Object.entries(context),
]); ]);
if (this.loggerName !== undefined)
event.set("loggerName", this.loggerName);
// 2. Process through the processor chain // 2. Process through the processor chain
for (const processor of this.options.processors) { for (const processor of this.options.processors) {
@@ -62,12 +60,11 @@ export class Logger implements ILogger {
// 3. Render and output if event wasn't dropped // 3. Render and output if event wasn't dropped
if (event !== undefined) { if (event !== undefined) {
const finalEvent = event; const output = this.options.renderer(event);
const output = this.options.renderer(finalEvent);
// Send to all configured streams // Send to all configured streams
for (const stream of this.options.streams) { for (const stream of this.options.streams) {
stream.write(output, finalEvent); stream.write(output, event);
} }
} }
} }
@@ -163,3 +160,17 @@ export class Logger implements ILogger {
} }
} }
} }
let globalLoggerConfig: LoggerOptions = {
processors: [processor.addTimestamp()],
renderer: textRenderer,
streams: [new ConsoleStream()],
};
export function getStructLogger(name?: string): Logger {
return new Logger(globalLoggerConfig, name);
}
export function setStructLoggerConfig(config: LoggerOptions): void {
globalLoggerConfig = config;
}

View File

@@ -61,7 +61,7 @@ export namespace processor {
return event; // Pass through if no level is set return event; // Pass through if no level is set
} }
if (eventLevel !== undefined && eventLevel < minLevel) { if (eventLevel < minLevel) {
return undefined; // Drop the log event return undefined; // Drop the log event
} }
@@ -69,22 +69,6 @@ export namespace processor {
}; };
} }
/**
* Adds a logger name/source to the log event.
*
* This processor is useful when you have multiple loggers in your application
* and want to identify which component generated each log entry.
*
* @param name - The name/source to add to log events
* @returns A processor function that adds the source name
*/
export function addSource(name: string): Processor {
return (event) => {
event.set("source", name);
return event;
};
}
/** /**
* Adds the current computer ID to the log event. * Adds the current computer ID to the log event.
* *

View File

@@ -53,14 +53,20 @@ export const textRenderer: Renderer = (event) => {
const timeStr = event.get("timestamp") as string | undefined; const timeStr = event.get("timestamp") as string | undefined;
const level: string | undefined = LogLevel[event.get("level") as LogLevel]; const level: string | undefined = LogLevel[event.get("level") as LogLevel];
const message = (event.get("message") as string) ?? ""; const message = (event.get("message") as string) ?? "";
const loggerName = event.get("loggerName") as string | undefined;
// Start building the output // Start building the output
let output = `[${timeStr}] [${level}] ${message} \t`; let output = `${timeStr} [${level}] ${message} \t ${loggerName !== undefined ? "[" + loggerName + "]" : ""}`;
// Add context fields (excluding the core fields we already used) // Add context fields (excluding the core fields we already used)
const contextFields: string[] = []; const contextFields: string[] = [];
for (const [key, value] of event.entries()) { for (const [key, value] of event.entries()) {
if (key !== "timestamp" && key !== "level" && key !== "message") { if (
key !== "timestamp" &&
key !== "level" &&
key !== "message" &&
key !== "loggerName"
) {
contextFields.push(`${key}=${tostring(value)}`); contextFields.push(`${key}=${tostring(value)}`);
} }
} }

View File

@@ -14,7 +14,11 @@ import { LogLevel, Stream, LogEvent } from "./types";
interface FileStreamConfig { interface FileStreamConfig {
/** Path to the log file */ /** Path to the log file */
filePath: string; filePath: string;
/** Time in seconds between file rotations (0 = no rotation) */ /**
* Time in seconds between file rotations (0 = no rotation)
* Time must larger than one DAY
* @default 0
*/
rotationInterval?: number; rotationInterval?: number;
/** Auto-cleanup configuration */ /** Auto-cleanup configuration */
autoCleanup?: { autoCleanup?: {
@@ -92,6 +96,8 @@ export class FileStream implements Stream {
constructor(config: FileStreamConfig) { constructor(config: FileStreamConfig) {
this.filePath = config.filePath; this.filePath = config.filePath;
this.rotationInterval = config.rotationInterval || 0; this.rotationInterval = config.rotationInterval || 0;
if (this.rotationInterval !== 0 && this.rotationInterval < DAY)
throw Error("Rotation interval must be at least one day");
this.autoCleanupConfig = config.autoCleanup; this.autoCleanupConfig = config.autoCleanup;
this.lastRotationTime = os.time(); this.lastRotationTime = os.time();
this.openFile(); this.openFile();
@@ -124,13 +130,13 @@ export class FileStream implements Stream {
* Generate a filename with timestamp for file rotation. * Generate a filename with timestamp for file rotation.
*/ */
private getRotatedFilename(): string { private getRotatedFilename(): string {
const currentTime = os.time(); const currentTime = os.time(os.date("*t"));
const rotationPeriod = const rotationPeriod =
Math.floor(currentTime / this.rotationInterval) * Math.floor(currentTime / this.rotationInterval) *
this.rotationInterval; this.rotationInterval;
const date = os.date("*t", rotationPeriod) as LuaDate; const date = os.date("*t", rotationPeriod) as LuaDate;
const timestamp = `${date.year}-${string.format("%02d", date.month)}-${string.format("%02d", date.day)}_${string.format("%02d", date.hour)}-${string.format("%02d", date.min)}`; const timestamp = `${date.year}-${string.format("%02d", date.month)}-${string.format("%02d", date.day)}`;
// Split filename and extension // Split filename and extension
const splitStrs = this.filePath.split("."); const splitStrs = this.filePath.split(".");
@@ -150,12 +156,11 @@ export class FileStream implements Stream {
if (this.rotationInterval <= 0) return; if (this.rotationInterval <= 0) return;
const currentTime = os.time(); const currentTime = os.time();
const currentPeriod = Math.floor(currentTime / this.rotationInterval); if (
const lastPeriod = Math.floor( Math.floor(
this.lastRotationTime / this.rotationInterval, (currentTime - this.lastRotationTime) / this.rotationInterval,
); ) > 0
) {
if (currentPeriod > lastPeriod) {
// Time to rotate // Time to rotate
this.close(); this.close();
this.lastRotationTime = currentTime; this.lastRotationTime = currentTime;
@@ -177,20 +182,12 @@ export class FileStream implements Stream {
// Cleanup by file count if configured // Cleanup by file count if configured
if (config.maxFiles !== undefined && config.maxFiles > 0) { if (config.maxFiles !== undefined && config.maxFiles > 0) {
this.cleanupOldLogFiles( this.cleanupOldLogFiles(config.maxFiles, config.logDir);
config.maxFiles,
config.logDir,
config.pattern,
);
} }
// Cleanup by total size if configured // Cleanup by total size if configured
if (config.maxSizeBytes !== undefined && config.maxSizeBytes > 0) { if (config.maxSizeBytes !== undefined && config.maxSizeBytes > 0) {
this.cleanupLogFilesBySize( this.cleanupLogFilesBySize(config.maxSizeBytes, config.logDir);
config.maxSizeBytes,
config.logDir,
config.pattern,
);
} }
} }
@@ -232,16 +229,16 @@ export class FileStream implements Stream {
* Search for log files matching the specified pattern in a directory. * Search for log files matching the specified pattern in a directory.
* *
* @param logDir - Directory containing log files (defaults to directory of current log file) * @param logDir - Directory containing log files (defaults to directory of current log file)
* @param fileName - Base File Name
* @returns Array of log file information including path, size, and modification time * @returns Array of log file information including path, size, and modification time
*/ */
private searchLogFiles( private searchLogFiles(
logDir?: string, logDir?: string,
fileName?: string,
): Array<{ path: string; size: number; modified: number }> { ): Array<{ path: string; size: number; modified: number }> {
const directory = logDir || fs.getDir(this.filePath); const directory = logDir || fs.getDir(this.filePath);
const baseFileName = const splitStrs = this.filePath.split(".");
fileName || fs.getName(this.filePath).split(".")[0];
const name = splitStrs[0] + "_";
const ext = splitStrs.length > 1 ? splitStrs[1] : "log";
if (!fs.exists(directory) || !fs.isDir(directory)) { if (!fs.exists(directory) || !fs.isDir(directory)) {
return []; return [];
@@ -256,7 +253,12 @@ export class FileStream implements Stream {
for (const file of files) { for (const file of files) {
const fullPath = fs.combine(directory, file); const fullPath = fs.combine(directory, file);
if (fs.isDir(fullPath) || !file.startsWith(baseFileName)) continue; if (
fs.isDir(fullPath) ||
!file.startsWith(name) ||
!file.endsWith(ext)
)
continue;
const attributes = fs.attributes(fullPath); const attributes = fs.attributes(fullPath);
if (attributes !== undefined) { if (attributes !== undefined) {
@@ -276,16 +278,11 @@ export class FileStream implements Stream {
* *
* @param maxFiles - Maximum number of log files to keep * @param maxFiles - Maximum number of log files to keep
* @param logDir - Directory containing log files (defaults to directory of current log file) * @param logDir - Directory containing log files (defaults to directory of current log file)
* @param fileName - Base File Name
*/ */
public cleanupOldLogFiles( public cleanupOldLogFiles(maxFiles: number, logDir?: string): void {
maxFiles: number,
logDir?: string,
fileName?: string,
): void {
if (maxFiles <= 0) return; if (maxFiles <= 0) return;
const logFiles = this.searchLogFiles(logDir, fileName); const logFiles = this.searchLogFiles(logDir);
if (logFiles.length <= maxFiles) return; if (logFiles.length <= maxFiles) return;
// Sort by modification time (newest first) // Sort by modification time (newest first)
@@ -310,14 +307,10 @@ export class FileStream implements Stream {
* @param logDir - Directory containing log files (defaults to directory of current log file) * @param logDir - Directory containing log files (defaults to directory of current log file)
* @param fileName - Base File Name * @param fileName - Base File Name
*/ */
public cleanupLogFilesBySize( public cleanupLogFilesBySize(maxSizeBytes: number, logDir?: string): void {
maxSizeBytes: number,
logDir?: string,
fileName?: string,
): void {
if (maxSizeBytes <= 0) return; if (maxSizeBytes <= 0) return;
const logFiles = this.searchLogFiles(logDir, fileName); const logFiles = this.searchLogFiles(logDir);
if (logFiles.length === 0) return; if (logFiles.length === 0) return;
// Calculate total size // Calculate total size