mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-29 12:57:50 +08:00
feat(logging): add auto-cleanup functionality to FileStream
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
FileStream,
|
FileStream,
|
||||||
Logger,
|
Logger,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
|
MB,
|
||||||
processor,
|
processor,
|
||||||
textRenderer,
|
textRenderer,
|
||||||
} from "@/lib/ccStructLog";
|
} from "@/lib/ccStructLog";
|
||||||
@@ -29,7 +30,15 @@ const logger = new Logger({
|
|||||||
renderer: textRenderer,
|
renderer: textRenderer,
|
||||||
streams: [
|
streams: [
|
||||||
new ConditionalStream(new ConsoleStream(), () => isOnConsoleStream),
|
new ConditionalStream(new ConsoleStream(), () => isOnConsoleStream),
|
||||||
new FileStream("accesscontrol.log", DAY),
|
new FileStream({
|
||||||
|
filePath: "accesscontrol.log",
|
||||||
|
rotationInterval: DAY,
|
||||||
|
autoCleanup: {
|
||||||
|
enabled: true,
|
||||||
|
maxFiles: 7,
|
||||||
|
maxSizeBytes: MB,
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,29 @@
|
|||||||
|
|
||||||
import { LogLevel, Stream, LogEvent } from "./types";
|
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.
|
* Console stream that outputs to the CC:Tweaked terminal.
|
||||||
*
|
*
|
||||||
@@ -59,20 +82,18 @@ export class FileStream implements Stream {
|
|||||||
private filePath: string;
|
private filePath: string;
|
||||||
private rotationInterval: number;
|
private rotationInterval: number;
|
||||||
private lastRotationTime: 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 config - FileStream configuration object
|
||||||
* @param rotationInterval - Time in seconds between file rotations (0 = no rotation)
|
|
||||||
*/
|
*/
|
||||||
constructor(filePath: string, rotationInterval: number = 0) {
|
constructor(config: FileStreamConfig) {
|
||||||
this.filePath = filePath;
|
this.filePath = config.filePath;
|
||||||
this.rotationInterval = rotationInterval;
|
this.rotationInterval = config.rotationInterval || 0;
|
||||||
|
this.autoCleanupConfig = config.autoCleanup;
|
||||||
this.lastRotationTime = os.time();
|
this.lastRotationTime = os.time();
|
||||||
this.baseFilename = filePath;
|
|
||||||
|
|
||||||
this.openFile();
|
this.openFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +115,9 @@ export class FileStream implements Stream {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.fileHandle = handle;
|
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)}`;
|
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
|
// Split filename and extension
|
||||||
const splitStrs = this.baseFilename.split(".");
|
const splitStrs = this.filePath.split(".");
|
||||||
if (splitStrs.length === 1) {
|
if (splitStrs.length === 1) {
|
||||||
return `${this.baseFilename}_${timestamp}.log`;
|
return `${this.filePath}_${timestamp}.log`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = splitStrs[0];
|
const name = splitStrs[0];
|
||||||
@@ -136,9 +160,49 @@ export class FileStream implements Stream {
|
|||||||
this.close();
|
this.close();
|
||||||
this.lastRotationTime = currentTime;
|
this.lastRotationTime = currentTime;
|
||||||
this.openFile();
|
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.
|
* Write a formatted log message to the file.
|
||||||
*
|
*
|
||||||
@@ -163,6 +227,127 @@ export class FileStream implements Stream {
|
|||||||
this.fileHandle = undefined;
|
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 HOUR = 60 * MINUTE;
|
||||||
export const DAY = 24 * HOUR;
|
export const DAY = 24 * HOUR;
|
||||||
export const WEEK = 7 * DAY;
|
export const WEEK = 7 * DAY;
|
||||||
|
|
||||||
|
// Byte constants for file rotation
|
||||||
|
export const MB = 1024 * 1024;
|
||||||
|
export const KB = 1024;
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ const customLogger = new Logger({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
renderer: jsonRenderer,
|
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", {
|
customLogger.info("Custom logger example", {
|
||||||
@@ -136,7 +142,10 @@ const multiFormatLogger = new Logger({
|
|||||||
{
|
{
|
||||||
write: (_, event) => {
|
write: (_, event) => {
|
||||||
const formatted = jsonRenderer(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({
|
const fileLogger = new Logger({
|
||||||
processors: [processor.addTimestamp()],
|
processors: [processor.addTimestamp()],
|
||||||
renderer: jsonRenderer,
|
renderer: jsonRenderer,
|
||||||
streams: [new FileStream("temp.log")],
|
streams: [new FileStream({ filePath: "structured.log" })],
|
||||||
});
|
});
|
||||||
|
|
||||||
fileLogger.info("Temporary log entry");
|
fileLogger.info("Temporary log entry");
|
||||||
|
|||||||
Reference in New Issue
Block a user