/** * Centralized logger for Redlight server. * * Produces clean, colorized, tagged log lines inspired by Greenlight/Rails lograge style. * * Format: TIMESTAMP LEVEL [TAG] message * Example: 2026-03-01 12:00:00 INFO [BBB] GET getMeetings → 200 SUCCESS (45ms) */ // ── ANSI colors ───────────────────────────────────────────────────────────── const C = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m', }; const USE_COLOR = process.env.NO_COLOR ? false : true; const c = (color, text) => USE_COLOR ? `${color}${text}${C.reset}` : text; // ── Timestamp ─────────────────────────────────────────────────────────────── function ts() { const d = new Date(); const pad = n => String(n).padStart(2, '0'); const ms = String(d.getMilliseconds()).padStart(3, '0'); return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}.${ms}`; } // ── Level formatting ──────────────────────────────────────────────────────── const LEVEL_STYLE = { DEBUG: { color: C.gray, label: 'DEBUG' }, INFO: { color: C.green, label: ' INFO' }, WARN: { color: C.yellow, label: ' WARN' }, ERROR: { color: C.red, label: 'ERROR' }, }; // ── Tag colors ────────────────────────────────────────────────────────────── const TAG_COLORS = { BBB: C.magenta, HTTP: C.cyan, Federation: C.blue, FedSync: C.blue, DB: C.yellow, Auth: C.green, Server: C.white, Mailer: C.cyan, Redis: C.magenta, Admin: C.yellow, Rooms: C.green, Recordings: C.cyan, Branding: C.white, }; function formatLine(level, tag, message) { const lvl = LEVEL_STYLE[level] || LEVEL_STYLE.INFO; const tagColor = TAG_COLORS[tag] || C.white; const timestamp = c(C.gray, ts()); const levelStr = c(lvl.color, lvl.label); const tagStr = c(tagColor, `[${tag}]`); return `${timestamp} ${levelStr} ${tagStr} ${message}`; } // ── Public API ────────────────────────────────────────────────────────────── /** * Create a tagged logger. * @param {string} tag - e.g. 'BBB', 'HTTP', 'Federation' */ export function createLogger(tag) { return { debug: (msg, ...args) => console.debug(formatLine('DEBUG', tag, msg), ...args), info: (msg, ...args) => console.info(formatLine('INFO', tag, msg), ...args), warn: (msg, ...args) => console.warn(formatLine('WARN', tag, msg), ...args), error: (msg, ...args) => console.error(formatLine('ERROR', tag, msg), ...args), }; } // ── Pre-built loggers for common subsystems ───────────────────────────────── export const log = { bbb: createLogger('BBB'), http: createLogger('HTTP'), federation: createLogger('Federation'), fedSync: createLogger('FedSync'), db: createLogger('DB'), auth: createLogger('Auth'), server: createLogger('Server'), mailer: createLogger('Mailer'), redis: createLogger('Redis'), admin: createLogger('Admin'), rooms: createLogger('Rooms'), recordings: createLogger('Recordings'), branding: createLogger('Branding'), }; // ── Helpers ───────────────────────────────────────────────────────────────── /** Format duration with unit and color. */ export function fmtDuration(ms) { const num = Number(ms); if (num < 100) return c(C.green, `${num.toFixed(0)}ms`); if (num < 1000) return c(C.yellow, `${num.toFixed(0)}ms`); return c(C.red, `${(num / 1000).toFixed(2)}s`); } /** Format HTTP status with color. */ export function fmtStatus(status) { const s = Number(status); if (s < 300) return c(C.green, String(s)); if (s < 400) return c(C.cyan, String(s)); if (s < 500) return c(C.yellow, String(s)); return c(C.red, String(s)); } /** Format HTTP method with color. */ export function fmtMethod(method) { const m = String(method).toUpperCase(); const colors = { GET: C.green, POST: C.cyan, PUT: C.yellow, PATCH: C.yellow, DELETE: C.red }; return c(colors[m] || C.white, m.padEnd(6)); } /** Format BBB returncode with color. */ export function fmtReturncode(code) { if (code === 'SUCCESS') return c(C.green, code); if (code === 'FAILED') return c(C.red, code); return c(C.yellow, code || '-'); } // ── Sensitive value filtering ─────────────────────────────────────────────── const SENSITIVE_KEYS = [/pass/i, /pwd/i, /password/i, /token/i, /jwt/i, /secret/i, /authorization/i, /api[_-]?key/i]; export function isSensitiveKey(key) { return SENSITIVE_KEYS.some(rx => rx.test(key)); } /** * Sanitize BBB params for logging: filter sensitive values, truncate long strings, omit checksum. */ export function sanitizeBBBParams(params) { const parts = []; for (const k of Object.keys(params || {})) { if (k.toLowerCase() === 'checksum') continue; if (isSensitiveKey(k)) { parts.push(`${k}=${c(C.dim, '[FILTERED]')}`); } else { let v = params[k]; if (typeof v === 'string' && v.length > 80) v = v.slice(0, 80) + '…'; parts.push(`${c(C.gray, k)}=${v}`); } } return parts.join(' ') || '-'; }