153 lines
4.4 KiB
TypeScript
153 lines
4.4 KiB
TypeScript
/**
|
|
* 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',
|
|
transport: {
|
|
target: 'pino-pretty',
|
|
options: {
|
|
colorize: true,
|
|
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
|
|
ignore: 'pid,hostname',
|
|
singleLine: true
|
|
}
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Log level mapping
|
|
// ---------------------------------------------------------------------------
|
|
type LogLevel = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'SUCCESS';
|
|
|
|
const LEVEL_MAP: Record<LogLevel, keyof pino.Logger> = {
|
|
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);
|
|
}
|
|
} |