Files
website/server/src/utils/logger.js
whitlocktech eef79e2403 Initial commit: UOMysticmoon backend (Express + MariaDB + JWT)
- Layered API (router -> controller -> model -> db), serverlinkr pattern
- Public / auth / admin route groups; posts, wiki, settings, users, activity models
- JWT httpOnly-cookie auth (Secure auto-detected: LAN HTTP + Pangolin HTTPS)
- Site LIVE/MAINTENANCE mode with admin preview bypass
- Dual file+console logging (info/warn/error/debug) + HTTP access logs
- Docker Compose (app + MariaDB), schema.sql + seed, .env.example
- Verified end-to-end against MariaDB (27/27 smoke checks)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:58:32 -05:00

95 lines
3.4 KiB
JavaScript

// 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 <server>/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