mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-29 12:57:50 +08:00
refactor(logger): simplify logger configuration and improve file rotation
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user