shinroute / src /lib /logRotation.ts
shinmentakezo07
Add OmniRoute codebase without binary assets
9e4583c
Raw
History Blame Contribute Delete
4.26 kB
/**
* Log Rotation & Cleanup — manages application log file rotation.
*
* Handles:
* - Rotating log files when they exceed max size
* - Cleaning up old log files past retention period
* - Creating the log directory on startup
*
* Configuration via env vars:
* - LOG_TO_FILE: enable file logging (default: true)
* - LOG_FILE_PATH: path to log file (default: logs/application/app.log)
* - LOG_MAX_FILE_SIZE: max file size before rotation (default: 50MB)
* - LOG_RETENTION_DAYS: days to keep old logs (default: 7)
*/
import { existsSync, mkdirSync, statSync, renameSync, readdirSync, unlinkSync } from "fs";
import { dirname, join, basename, extname } from "path";
const DEFAULT_LOG_PATH = "logs/application/app.log";
const DEFAULT_MAX_SIZE = 50 * 1024 * 1024; // 50MB
const DEFAULT_RETENTION_DAYS = 7;
function parseFileSize(raw: string | undefined): number {
if (!raw) return DEFAULT_MAX_SIZE;
const match = raw.match(/^(\d+)\s*(k|m|g|kb|mb|gb)?$/i);
if (!match) return DEFAULT_MAX_SIZE;
const num = parseInt(match[1], 10);
const unit = (match[2] || "").toLowerCase();
switch (unit) {
case "k":
case "kb":
return num * 1024;
case "m":
case "mb":
return num * 1024 * 1024;
case "g":
case "gb":
return num * 1024 * 1024 * 1024;
default:
return num;
}
}
export function getLogConfig() {
const logToFile = process.env.LOG_TO_FILE !== "false";
const logFilePath = process.env.LOG_FILE_PATH || join(process.cwd(), DEFAULT_LOG_PATH);
const maxFileSize = parseFileSize(process.env.LOG_MAX_FILE_SIZE);
const retentionDays = parseInt(
process.env.LOG_RETENTION_DAYS || String(DEFAULT_RETENTION_DAYS),
10
);
return { logToFile, logFilePath, maxFileSize, retentionDays };
}
/**
* Ensure the log directory exists.
*/
export function ensureLogDir(logFilePath: string): void {
const dir = dirname(logFilePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
/**
* Rotate the log file if it exceeds the max size.
* Renames current file to app.YYYY-MM-DD_HHmmss.log
*/
export function rotateIfNeeded(logFilePath: string, maxFileSize: number): void {
try {
if (!existsSync(logFilePath)) return;
const stats = statSync(logFilePath);
if (stats.size < maxFileSize) return;
const dir = dirname(logFilePath);
const ext = extname(logFilePath);
const base = basename(logFilePath, ext);
const now = new Date();
const ts = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(
now.getDate()
).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(
now.getMinutes()
).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
const rotatedPath = join(dir, `${base}.${ts}${ext}`);
renameSync(logFilePath, rotatedPath);
} catch {
// If rotation fails, continue writing to the same file
}
}
/**
* Remove log files older than the retention period.
*/
export function cleanupOldLogs(logFilePath: string, retentionDays: number): void {
try {
const dir = dirname(logFilePath);
if (!existsSync(dir)) return;
const ext = extname(logFilePath);
const base = basename(logFilePath, ext);
const files = readdirSync(dir);
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
for (const file of files) {
// Match rotated files like app.2026-02-19_030000.log
if (file.startsWith(base + ".") && file.endsWith(ext) && file !== basename(logFilePath)) {
const filePath = join(dir, file);
try {
const stats = statSync(filePath);
if (stats.mtimeMs < cutoff) {
unlinkSync(filePath);
}
} catch {
// Skip files we can't stat
}
}
}
} catch {
// Cleanup is best-effort
}
}
/**
* Initialize log rotation — call once at application startup.
* Creates directories, rotates if needed, and cleans up old files.
*/
export function initLogRotation(): void {
const config = getLogConfig();
if (!config.logToFile) return;
ensureLogDir(config.logFilePath);
rotateIfNeeded(config.logFilePath, config.maxFileSize);
cleanupOldLogs(config.logFilePath, config.retentionDays);
}