/** * Structured logging for email worker with daily rotation AND retention * * Uses pino for high-performance JSON logging. * Includes logic to delete logs older than X days. */ import pino from 'pino'; import { existsSync, mkdirSync, createWriteStream, type WriteStream, readdirSync, statSync, unlinkSync } from 'node:fs'; import { join } from 'node:path'; // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- const LOG_DIR = '/var/log/email-worker'; const LOG_FILE_PREFIX = 'worker'; const RETENTION_DAYS = 14; // Logs älter als 14 Tage löschen // --------------------------------------------------------------------------- // File stream & Retention Logic // --------------------------------------------------------------------------- let fileStream: WriteStream | null = null; let currentDateStr = ''; function getDateStr(): string { return new Date().toISOString().slice(0, 10); // YYYY-MM-DD } /** * Löscht alte Log-Dateien basierend auf RETENTION_DAYS */ function cleanUpOldLogs(): void { try { if (!existsSync(LOG_DIR)) return; const files = readdirSync(LOG_DIR); const now = Date.now(); const maxAgeMs = RETENTION_DAYS * 24 * 60 * 60 * 1000; for (const file of files) { // Prüfen ob es eine unserer Log-Dateien ist if (!file.startsWith(LOG_FILE_PREFIX) || !file.endsWith('.log')) continue; const filePath = join(LOG_DIR, file); try { const stats = statSync(filePath); const ageMs = now - stats.mtimeMs; if (ageMs > maxAgeMs) { unlinkSync(filePath); // Einmalig auf stdout loggen, damit man sieht, dass aufgeräumt wurde process.stdout.write(`[INFO] Deleted old log file: ${file}\n`); } } catch (err) { // Ignorieren, falls Datei gerade gelöscht wurde oder Zugriff verweigert } } } catch (err) { process.stderr.write(`[WARN] Failed to clean up old logs: ${err}\n`); } } function ensureFileStream(): WriteStream | null { const today = getDateStr(); // Wenn wir bereits einen Stream für heute haben, zurückgeben if (fileStream && currentDateStr === today) return fileStream; try { if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true }); // Wenn sich das Datum geändert hat (oder beim ersten Start): Aufräumen if (currentDateStr !== today) { cleanUpOldLogs(); } // Alten Stream schließen, falls vorhanden if (fileStream) { fileStream.end(); } const filePath = join(LOG_DIR, `${LOG_FILE_PREFIX}.${today}.log`); fileStream = createWriteStream(filePath, { flags: 'a' }); currentDateStr = today; return fileStream; } catch { // Silently continue without file logging (e.g. permission issue) return null; } } // --------------------------------------------------------------------------- // Pino logger // --------------------------------------------------------------------------- const logger = pino({ level: 'info', formatters: { level(label) { return { level: label }; }, }, timestamp: pino.stdTimeFunctions.isoTime, }); // --------------------------------------------------------------------------- // Log level mapping // --------------------------------------------------------------------------- type LogLevel = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'SUCCESS'; const LEVEL_MAP: Record = { DEBUG: 'debug', INFO: 'info', WARNING: 'warn', ERROR: 'error', CRITICAL: 'fatal', SUCCESS: 'info', }; // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- export function log( message: string, level: LogLevel = 'INFO', workerName = 'unified-worker', ): void { const prefix = level === 'SUCCESS' ? '[SUCCESS] ' : ''; const formatted = `[${workerName}] ${prefix}${message}`; // Pino (stdout/json) const method = LEVEL_MAP[level] ?? 'info'; (logger as any)[method](formatted); // File (plain text) const stream = ensureFileStream(); if (stream) { const ts = new Date().toISOString().replace('T', ' ').slice(0, 19); const line = `[${ts}] [${level}] [${workerName}] ${prefix}${message}\n`; stream.write(line); } }