- 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>
95 lines
3.4 KiB
JavaScript
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
|