feat(logging): add auto-cleanup functionality to FileStream

This commit is contained in:
2025-11-21 15:58:42 +08:00
parent 82a9fec46d
commit 0612477325
3 changed files with 222 additions and 15 deletions

View File

@@ -13,6 +13,7 @@ import {
FileStream,
Logger,
LogLevel,
MB,
processor,
textRenderer,
} from "@/lib/ccStructLog";
@@ -29,7 +30,15 @@ const logger = new Logger({
renderer: textRenderer,
streams: [
new ConditionalStream(new ConsoleStream(), () => isOnConsoleStream),
new FileStream("accesscontrol.log", DAY),
new FileStream({
filePath: "accesscontrol.log",
rotationInterval: DAY,
autoCleanup: {
enabled: true,
maxFiles: 7,
maxSizeBytes: MB,
},
}),
],
});

View File

@@ -8,6 +8,29 @@
import { LogLevel, Stream, LogEvent } from "./types";
/**
* Configuration interface for FileStream with auto-cleanup options.
*/
interface FileStreamConfig {
/** Path to the log file */
filePath: string;
/** Time in seconds between file rotations (0 = no rotation) */
rotationInterval?: number;
/** Auto-cleanup configuration */
autoCleanup?: {
/** Whether to enable auto-cleanup */
enabled: boolean;
/** Maximum number of log files to keep */
maxFiles?: number;
/** Maximum total size in bytes for all log files */
maxSizeBytes?: number;
/** Directory to search for log files (defaults to log file directory) */
logDir?: string;
/** File pattern to match (defaults to base filename pattern) */
pattern?: string;
};
}
/**
* Console stream that outputs to the CC:Tweaked terminal.
*
@@ -59,20 +82,18 @@ export class FileStream implements Stream {
private filePath: string;
private rotationInterval: number;
private lastRotationTime: number;
private baseFilename: string;
private autoCleanupConfig?: FileStreamConfig["autoCleanup"];
/**
* Create a new file stream.
* Create a new file stream with configuration object.
*
* @param filePath - Path to the log file
* @param rotationInterval - Time in seconds between file rotations (0 = no rotation)
* @param config - FileStream configuration object
*/
constructor(filePath: string, rotationInterval: number = 0) {
this.filePath = filePath;
this.rotationInterval = rotationInterval;
constructor(config: FileStreamConfig) {
this.filePath = config.filePath;
this.rotationInterval = config.rotationInterval || 0;
this.autoCleanupConfig = config.autoCleanup;
this.lastRotationTime = os.time();
this.baseFilename = filePath;
this.openFile();
}
@@ -94,6 +115,9 @@ export class FileStream implements Stream {
return;
}
this.fileHandle = handle;
// Perform auto-cleanup when opening file
this.performAutoCleanup();
}
/**
@@ -109,9 +133,9 @@ export class FileStream implements Stream {
const timestamp = `${date.year}-${string.format("%02d", date.month)}-${string.format("%02d", date.day)}_${string.format("%02d", date.hour)}-${string.format("%02d", date.min)}`;
// Split filename and extension
const splitStrs = this.baseFilename.split(".");
const splitStrs = this.filePath.split(".");
if (splitStrs.length === 1) {
return `${this.baseFilename}_${timestamp}.log`;
return `${this.filePath}_${timestamp}.log`;
}
const name = splitStrs[0];
@@ -136,9 +160,49 @@ export class FileStream implements Stream {
this.close();
this.lastRotationTime = currentTime;
this.openFile();
// Auto-cleanup is performed in openFile()
}
}
/**
* Perform auto-cleanup based on configuration.
* This method is called automatically when opening files or rotating.
*/
private performAutoCleanup(): void {
if (!this.autoCleanupConfig || !this.autoCleanupConfig.enabled) {
return;
}
const config = this.autoCleanupConfig;
// Cleanup by file count if configured
if (config.maxFiles !== undefined && config.maxFiles > 0) {
this.cleanupOldLogFiles(
config.maxFiles,
config.logDir,
config.pattern,
);
}
// Cleanup by total size if configured
if (config.maxSizeBytes !== undefined && config.maxSizeBytes > 0) {
this.cleanupLogFilesBySize(
config.maxSizeBytes,
config.logDir,
config.pattern,
);
}
}
/**
* Enable or update auto-cleanup configuration at runtime.
*
* @param config - Auto-cleanup configuration
*/
public setAutoCleanup(config: FileStreamConfig["autoCleanup"]): void {
this.autoCleanupConfig = config;
}
/**
* Write a formatted log message to the file.
*
@@ -163,6 +227,127 @@ export class FileStream implements Stream {
this.fileHandle = undefined;
}
}
/**
* 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 fileName - Base File Name
* @returns Array of log file information including path, size, and modification time
*/
private searchLogFiles(
logDir?: string,
fileName?: string,
): Array<{ path: string; size: number; modified: number }> {
const directory = logDir || fs.getDir(this.filePath);
const baseFileName =
fileName || fs.getName(this.filePath).split(".")[0];
if (!fs.exists(directory) || !fs.isDir(directory)) {
return [];
}
const logFiles: Array<{
path: string;
size: number;
modified: number;
}> = [];
const files = fs.list(directory);
for (const file of files) {
const fullPath = fs.combine(directory, file);
if (fs.isDir(fullPath) || !file.startsWith(baseFileName)) continue;
const attributes = fs.attributes(fullPath);
if (attributes !== undefined) {
logFiles.push({
path: fullPath,
size: attributes.size,
modified: attributes.modified,
});
}
}
return logFiles;
}
/**
* Clean up old log files by keeping only the specified number of most recent files.
*
* @param maxFiles - Maximum number of log files to keep
* @param logDir - Directory containing log files (defaults to directory of current log file)
* @param fileName - Base File Name
*/
public cleanupOldLogFiles(
maxFiles: number,
logDir?: string,
fileName?: string,
): void {
if (maxFiles <= 0) return;
const logFiles = this.searchLogFiles(logDir, fileName);
if (logFiles.length <= maxFiles) return;
// Sort by modification time (newest first)
logFiles.sort((a, b) => b.modified - a.modified);
// Delete files beyond the limit
for (let i = maxFiles; i < logFiles.length; i++) {
try {
fs.delete(logFiles[i].path);
} catch (err) {
printError(
`Failed to delete old log file ${logFiles[i].path}: ${err}`,
);
}
}
}
/**
* Clean up log files by total size, deleting oldest files until total size is under limit.
*
* @param maxSizeBytes - Maximum total size in bytes for all log files
* @param logDir - Directory containing log files (defaults to directory of current log file)
* @param fileName - Base File Name
*/
public cleanupLogFilesBySize(
maxSizeBytes: number,
logDir?: string,
fileName?: string,
): void {
if (maxSizeBytes <= 0) return;
const logFiles = this.searchLogFiles(logDir, fileName);
if (logFiles.length === 0) return;
// Calculate total size
let totalSize = 0;
for (const logFile of logFiles) {
totalSize += logFile.size;
}
// If total size is within limit, no cleanup needed
if (totalSize <= maxSizeBytes) {
return;
}
// Sort by modification time (oldest first for deletion)
logFiles.sort((a, b) => a.modified - b.modified);
// Delete oldest files until we're under the size limit
for (const logFile of logFiles) {
if (totalSize <= maxSizeBytes) {
break;
}
try {
fs.delete(logFile.path);
totalSize -= logFile.size;
} catch (err) {
printError(`Failed to delete log file ${logFile.path}: ${err}`);
}
}
}
}
/**
@@ -307,3 +492,7 @@ export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;
// Byte constants for file rotation
export const MB = 1024 * 1024;
export const KB = 1024;

View File

@@ -36,7 +36,13 @@ const customLogger = new Logger({
}),
],
renderer: jsonRenderer,
streams: [new ConsoleStream(), new FileStream("custom.log", HOUR)],
streams: [
new ConsoleStream(),
new FileStream({
filePath: "custom.log",
rotationInterval: HOUR,
}),
],
});
customLogger.info("Custom logger example", {
@@ -136,7 +142,10 @@ const multiFormatLogger = new Logger({
{
write: (_, event) => {
const formatted = jsonRenderer(event);
new FileStream("structured.log").write(formatted, event);
new FileStream({ filePath: "structured.log" }).write(
formatted,
event,
);
},
},
],
@@ -193,7 +202,7 @@ print("\n=== Cleanup Examples ===");
const fileLogger = new Logger({
processors: [processor.addTimestamp()],
renderer: jsonRenderer,
streams: [new FileStream("temp.log")],
streams: [new FileStream({ filePath: "structured.log" })],
});
fileLogger.info("Temporary log entry");