// Dual-transport logger: writes to the console AND to a log file. // Levels: error | warn | info | debug. // LOG_LEVEL console verbosity (default info) // FILE_LOG_LEVEL file verbosity (default debug — keep a full record on disk) // LOG_TO_FILE enable file logging (default true) // LOG_DIR log directory (default /logs) // LOG_FILE log file name (default app.log) const fs = require('fs') const path = require('path') const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 } const consoleThreshold = LEVELS[(process.env.LOG_LEVEL || 'info').toLowerCase()] ?? LEVELS.info const fileThreshold = LEVELS[(process.env.FILE_LOG_LEVEL || 'debug').toLowerCase()] ?? LEVELS.debug // Color only on an interactive TTY — never in files or Docker logs. const useColor = Boolean(process.stdout.isTTY) && process.env.NO_COLOR == null const COLOR = { error: '\x1b[31m', warn: '\x1b[33m', info: '\x1b[36m', debug: '\x1b[90m' } const RESET = '\x1b[0m' // ── File transport ──────────────────────────────────────────────────── const fileEnabled = (process.env.LOG_TO_FILE || 'true').toLowerCase() !== 'false' let fileStream = null let logFilePath = null if (fileEnabled) { try { const dir = process.env.LOG_DIR || path.join(__dirname, '..', '..', 'logs') fs.mkdirSync(dir, { recursive: true }) logFilePath = path.join(dir, process.env.LOG_FILE || 'app.log') fileStream = fs.createWriteStream(logFilePath, { flags: 'a' }) fileStream.on('error', (err) => { process.stderr.write(`[logger] file logging disabled: ${err.message}\n`) fileStream = null }) } catch (err) { process.stderr.write(`[logger] could not open log file: ${err.message}\n`) fileStream = null } } function fmt(meta) { if (meta == null) return '' if (typeof meta === 'string') return meta if (meta instanceof Error) return JSON.stringify({ message: meta.message, stack: meta.stack }) try { return JSON.stringify(meta) } catch { return String(meta) } } function emit(level, tag, msg, meta) { const levelNum = LEVELS[level] if (levelNum === undefined) return const ts = new Date().toISOString() const lvl = level.toUpperCase().padEnd(5) const label = tag ? ` [${tag}]` : '' const metaStr = meta === undefined ? '' : ` ${fmt(meta)}` const plain = `${ts} ${lvl}${label} ${msg}${metaStr}` // Console transport if (levelNum <= consoleThreshold) { const line = useColor ? `${COLOR[level] || ''}${plain}${RESET}` : plain const stream = level === 'error' || level === 'warn' ? process.stderr : process.stdout stream.write(`${line}\n`) } // File transport (plain text, no color) if (fileStream && levelNum <= fileThreshold) { fileStream.write(`${plain}\n`) } } function createLogger(tag) { return { error: (msg, meta) => emit('error', tag, msg, meta), warn: (msg, meta) => emit('warn', tag, msg, meta), info: (msg, meta) => emit('info', tag, msg, meta), debug: (msg, meta) => emit('debug', tag, msg, meta), } } // Flush and close the file stream (called on graceful shutdown). createLogger.close = () => new Promise((resolve) => { if (fileStream) fileStream.end(resolve) else resolve() }) createLogger.emit = emit createLogger.logFilePath = logFilePath module.exports = createLogger