/** * Centralized structured logging for ForkFolio. * * - Emits newline-delimited JSON to stdout (collected by Docker, rotated by the * json-file log driver, and forwarded to OpenObserve by Fluent Bit). * - `log.*` helpers accept console-style arguments, so existing * `console.error('Message:', err)` call sites translate 1:1 while Error * objects are serialized with their stack trace under the `err` key. * - `httpLogger` is Express middleware that logs one line per HTTP request. * * Environment variables: * LOG_LEVEL trace | debug | info | warn | error | fatal | silent (default: info) */ const crypto = require('crypto'); const util = require('util'); const pino = require('pino'); const pinoHttp = require('pino-http'); const logger = pino({ level: process.env.LOG_LEVEL || 'info', base: { app: 'forkfolio' }, timestamp: pino.stdTimeFunctions.isoTime, formatters: { // Use readable level names ("info") instead of numeric levels (30) level: (label) => ({ level: label }) }, redact: { paths: ['password', '*.password', 'token', '*.token', 'apiKey', '*.apiKey'], censor: '[REDACTED]' } }); /** * Builds a console-compatible log function for the given pino level. * The first Error argument is attached as `err`; remaining arguments are * formatted into the message exactly like console.log would format them. */ function consoleStyle(level) { return (...args) => { const err = args.find((a) => a instanceof Error); const rest = err ? args.filter((a) => a !== err) : args; const msg = util.format(...rest); if (err) { // Drop the trailing "Message:" colon since the error is attached separately logger[level]({ err }, msg.replace(/:\s*$/, '')); } else { logger[level](msg); } }; } const log = { debug: consoleStyle('debug'), info: consoleStyle('info'), warn: consoleStyle('warn'), error: consoleStyle('error') }; // Static asset requests are high-volume and low-value; skip them in request logs const STATIC_ASSET_RE = /\.(?:js|css|png|jpe?g|gif|svg|ico|webp|woff2?|ttf|map|json|html)$/i; const httpLogger = pinoHttp({ logger, genReqId: (req, res) => { const id = req.headers['x-request-id'] || crypto.randomUUID(); res.setHeader('X-Request-Id', id); return id; }, autoLogging: { ignore: (req) => STATIC_ASSET_RE.test((req.url || '').split('?')[0]) }, customLogLevel: (req, res, err) => { if (err || res.statusCode >= 500) return 'error'; if (res.statusCode >= 400) return 'warn'; return 'info'; }, // Keep request logs compact and never record headers (Authorization, cookies) serializers: { req: (req) => ({ id: req.id, method: req.method, url: req.url, ip: (req.headers && req.headers['x-forwarded-for']) || req.remoteAddress }), res: (res) => ({ statusCode: res.statusCode }) } }); module.exports = { logger, log, httpLogger };