Add daily log rotation with monthly folder structure

All log output is now additionally written to daily log files:
  daily-logs/YYYY-MM/YYYY-MM-DD.log (main log)
  daily-logs/YYYY-MM/YYYY-MM-DD-rename.log (rename log)

Automatic cleanup of daily logs older than 30 days. The existing
rd_downloader.log and rename.log continue to work as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-24 09:16:24 +01:00
parent 1c78bb61c6
commit d7149829ea
3 changed files with 623 additions and 428 deletions

View File

@ -40,6 +40,7 @@ import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "
import { getDebugSetupCheck } from "./debug-setup"; import { getDebugSetupCheck } from "./debug-setup";
import { buildLinkExportSelection, serializeLinkExportText } from "./link-export"; import { buildLinkExportSelection, serializeLinkExportText } from "./link-export";
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log"; import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log";
import { initDailyLog, shutdownDailyLog } from "./daily-log";
import { buildAccountSummary, diffAccountSummary } from "./support-data"; import { buildAccountSummary, diffAccountSummary } from "./support-data";
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle"; import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log"; import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
@ -85,6 +86,7 @@ export class AppController {
initItemLogs(this.storagePaths.baseDir); initItemLogs(this.storagePaths.baseDir);
initAuditLog(this.storagePaths.baseDir); initAuditLog(this.storagePaths.baseDir);
initRenameLog(this.storagePaths.baseDir); initRenameLog(this.storagePaths.baseDir);
initDailyLog(this.storagePaths.baseDir);
initTraceLog(this.storagePaths.baseDir); initTraceLog(this.storagePaths.baseDir);
this.settings = loadSettings(this.storagePaths); this.settings = loadSettings(this.storagePaths);
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
@ -670,6 +672,7 @@ export class AppController {
shutdownPackageLogs(); shutdownPackageLogs();
shutdownItemLogs(); shutdownItemLogs();
shutdownRenameLog(); shutdownRenameLog();
shutdownDailyLog();
this.audit("INFO", "App beendet"); this.audit("INFO", "App beendet");
shutdownTraceLog(); shutdownTraceLog();
shutdownAuditLog(); shutdownAuditLog();

193
src/main/daily-log.ts Normal file
View File

@ -0,0 +1,193 @@
import fs from "node:fs";
import path from "node:path";
import { addLogListener, removeLogListener } from "./logger";
const DAILY_LOG_RETENTION_DAYS = 30;
const CLEANUP_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // every 6 hours
let dailyLogDir = "";
let currentDayKey = "";
let currentLogFd: number | null = null;
let currentRenameFd: number | null = null;
let logListener: ((line: string) => void) | null = null;
let cleanupTimer: NodeJS.Timeout | null = null;
let lastCleanupAt = 0;
function getDayKey(now = new Date()): string {
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function getMonthDir(dayKey: string): string {
return dayKey.slice(0, 7); // "YYYY-MM"
}
function ensureDayFile(dayKey: string): number | null {
if (currentDayKey === dayKey && currentLogFd !== null) {
return currentLogFd;
}
// Close previous day's fd
if (currentLogFd !== null) {
try { fs.closeSync(currentLogFd); } catch { /* ignore */ }
currentLogFd = null;
}
if (currentRenameFd !== null) {
try { fs.closeSync(currentRenameFd); } catch { /* ignore */ }
currentRenameFd = null;
}
currentDayKey = dayKey;
const monthDir = path.join(dailyLogDir, getMonthDir(dayKey));
try {
fs.mkdirSync(monthDir, { recursive: true });
const filePath = path.join(monthDir, `${dayKey}.log`);
currentLogFd = fs.openSync(filePath, "a");
return currentLogFd;
} catch {
return null;
}
}
function ensureRenameFd(dayKey: string): number | null {
if (currentDayKey === dayKey && currentRenameFd !== null) {
return currentRenameFd;
}
// ensureDayFile handles day transitions
if (currentDayKey !== dayKey) {
ensureDayFile(dayKey);
}
if (currentRenameFd !== null) {
return currentRenameFd;
}
const monthDir = path.join(dailyLogDir, getMonthDir(dayKey));
try {
fs.mkdirSync(monthDir, { recursive: true });
const filePath = path.join(monthDir, `${dayKey}-rename.log`);
currentRenameFd = fs.openSync(filePath, "a");
return currentRenameFd;
} catch {
return null;
}
}
function writeToDailyLog(line: string): void {
if (!dailyLogDir) return;
const dayKey = getDayKey();
const fd = ensureDayFile(dayKey);
if (fd === null) return;
try {
fs.writeSync(fd, line);
} catch {
// Close and retry on next write
try { fs.closeSync(fd); } catch { /* ignore */ }
currentLogFd = null;
}
}
export function writeToDailyRenameLog(line: string): void {
if (!dailyLogDir) return;
const dayKey = getDayKey();
const fd = ensureRenameFd(dayKey);
if (fd === null) return;
try {
fs.writeSync(fd, line);
} catch {
try { fs.closeSync(fd); } catch { /* ignore */ }
currentRenameFd = null;
}
}
function cleanupOldDailyLogs(): void {
if (!dailyLogDir) return;
const now = Date.now();
if (now - lastCleanupAt < CLEANUP_CHECK_INTERVAL_MS) return;
lastCleanupAt = now;
const cutoffMs = now - DAILY_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
try {
const monthDirs = fs.readdirSync(dailyLogDir, { withFileTypes: true })
.filter((e) => e.isDirectory() && /^\d{4}-\d{2}$/.test(e.name));
for (const monthDir of monthDirs) {
const monthPath = path.join(dailyLogDir, monthDir.name);
const files = fs.readdirSync(monthPath, { withFileTypes: true })
.filter((e) => e.isFile() && /^\d{4}-\d{2}-\d{2}/.test(e.name));
for (const file of files) {
const filePath = path.join(monthPath, file.name);
try {
const stat = fs.statSync(filePath);
if (stat.mtimeMs < cutoffMs) {
fs.rmSync(filePath, { force: true });
}
} catch { /* ignore */ }
}
// Remove empty month dirs
try {
const remaining = fs.readdirSync(monthPath);
if (remaining.length === 0) {
fs.rmdirSync(monthPath);
}
} catch { /* ignore */ }
}
} catch {
// ignore cleanup errors
}
}
export function initDailyLog(baseDir: string): void {
dailyLogDir = path.join(baseDir, "daily-logs");
try {
fs.mkdirSync(dailyLogDir, { recursive: true });
} catch { /* ignore */ }
// Attach listener to main logger
logListener = (line: string) => writeToDailyLog(line);
addLogListener(logListener);
// Initial cleanup
cleanupOldDailyLogs();
// Periodic cleanup
cleanupTimer = setInterval(cleanupOldDailyLogs, CLEANUP_CHECK_INTERVAL_MS);
if (cleanupTimer.unref) cleanupTimer.unref();
}
export function shutdownDailyLog(): void {
if (logListener) {
removeLogListener(logListener);
logListener = null;
}
if (cleanupTimer) {
clearInterval(cleanupTimer);
cleanupTimer = null;
}
if (currentLogFd !== null) {
try { fs.closeSync(currentLogFd); } catch { /* ignore */ }
currentLogFd = null;
}
if (currentRenameFd !== null) {
try { fs.closeSync(currentRenameFd); } catch { /* ignore */ }
currentRenameFd = null;
}
currentDayKey = "";
}
export function getDailyLogDir(): string {
return dailyLogDir;
}

View File

@ -1,5 +1,6 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { writeToDailyRenameLog } from "./daily-log";
type RenameLogLevel = "INFO" | "WARN" | "ERROR"; type RenameLogLevel = "INFO" | "WARN" | "ERROR";
@ -93,11 +94,9 @@ export function logRenameEvent(level: RenameLogLevel, message: string, fields?:
if (!fs.existsSync(renameLogPath)) { if (!fs.existsSync(renameLogPath)) {
fs.writeFileSync(renameLogPath, "", "utf8"); fs.writeFileSync(renameLogPath, "", "utf8");
} }
fs.appendFileSync( const line = `${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`;
renameLogPath, fs.appendFileSync(renameLogPath, line, "utf8");
`${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`, writeToDailyRenameLog(line);
"utf8"
);
} catch { } catch {
// ignore write errors // ignore write errors
} }