/** * Structured logging for email worker with daily rotation * * Uses pino for high-performance JSON logging. * Console output is human-readable via pino-pretty in dev, * and JSON in production (for Docker json-file driver). * * File logging uses a simple daily rotation approach. */ import pino from 'pino'; import { existsSync, mkdirSync, createWriteStream, type WriteStream } from 'node:fs'; import { join } from 'node:path'; // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- const LOG_DIR = '/var/log/email-worker'; const LOG_FILE_PREFIX = 'worker'; // --------------------------------------------------------------------------- // File stream (best-effort, never crashes the worker) // --------------------------------------------------------------------------- let fileStream: WriteStream | null = null; let currentDateStr = ''; function getDateStr(): string { return new Date().toISOString().slice(0, 10); // YYYY-MM-DD } function ensureFileStream(): WriteStream | null { const today = getDateStr(); if (fileStream && currentDateStr === today) return fileStream; try { if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true }); 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, // In production Docker we write plain JSON to stdout; // pino-pretty can be used during dev via `pino-pretty` pipe. }); // --------------------------------------------------------------------------- // Log level mapping (matches Python worker levels) // --------------------------------------------------------------------------- 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 – mirrors Python's log(message, level, worker_name) // --------------------------------------------------------------------------- export function log( message: string, level: LogLevel = 'INFO', workerName = 'unified-worker', ): void { const prefix = level === 'SUCCESS' ? '[SUCCESS] ' : ''; const formatted = `[${workerName}] ${prefix}${message}`; // Pino const method = LEVEL_MAP[level] ?? 'info'; (logger as any)[method](formatted); // File (best-effort) 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); } }