144 lines
4.0 KiB
JavaScript
144 lines
4.0 KiB
JavaScript
const LEVELS = Object.freeze({
|
|
debug: 10,
|
|
info: 20,
|
|
warn: 30,
|
|
error: 40,
|
|
silent: 100,
|
|
})
|
|
|
|
const REDACTED = '[redacted]'
|
|
const SECRET_KEY_RE = /(^|_)(api[-_]?key|authorization|password|secret|token|credential)($|_)/i
|
|
const MAX_STRING_LENGTH = 2000
|
|
|
|
function normalizeLevel(value, fallback = 'info') {
|
|
const level = String(value ?? '').trim().toLowerCase()
|
|
return Object.hasOwn(LEVELS, level) ? level : fallback
|
|
}
|
|
|
|
function normalizeFormat(value) {
|
|
return String(value ?? '').trim().toLowerCase() === 'json' ? 'json' : 'text'
|
|
}
|
|
|
|
function sanitizeValue(value) {
|
|
if (value instanceof Error) {
|
|
return {
|
|
name: value.name,
|
|
message: value.message,
|
|
code: value.code,
|
|
status: value.status,
|
|
stack: value.stack,
|
|
}
|
|
}
|
|
if (typeof value === 'string') {
|
|
return value.length > MAX_STRING_LENGTH
|
|
? `${value.slice(0, MAX_STRING_LENGTH)}… (${value.length - MAX_STRING_LENGTH} more characters)`
|
|
: value
|
|
}
|
|
if (typeof value === 'bigint') return value.toString()
|
|
if (Array.isArray(value)) return value.map(sanitizeValue)
|
|
if (value && typeof value === 'object') {
|
|
const result = {}
|
|
for (const [key, entry] of Object.entries(value)) {
|
|
result[key] = SECRET_KEY_RE.test(key) ? REDACTED : sanitizeValue(entry)
|
|
}
|
|
return result
|
|
}
|
|
return value
|
|
}
|
|
|
|
function formatScalar(value) {
|
|
if (value === null) return 'null'
|
|
if (typeof value === 'string') {
|
|
return /^[A-Za-z0-9_./:@[\]-]+$/.test(value) ? value : JSON.stringify(value)
|
|
}
|
|
if (typeof value === 'object') return JSON.stringify(value)
|
|
return String(value)
|
|
}
|
|
|
|
function formatText(timestamp, level, message, fields) {
|
|
const parts = [timestamp, level.toUpperCase().padEnd(5), message]
|
|
for (const [key, value] of Object.entries(fields)) {
|
|
if (value === undefined) continue
|
|
parts.push(`${key}=${formatScalar(value)}`)
|
|
}
|
|
return `${parts.join(' ')}\n`
|
|
}
|
|
|
|
function formatJson(timestamp, level, message, fields) {
|
|
return `${JSON.stringify({ timestamp, level, message, ...fields })}\n`
|
|
}
|
|
|
|
/**
|
|
* Create a small dependency-free logger.
|
|
*
|
|
* LOG_LEVEL: debug | info | warn | error | silent (default: info)
|
|
* LOG_FORMAT: text | json (default: text)
|
|
*
|
|
* All output goes to stderr so the CLI can keep stdout for the PDF path.
|
|
*/
|
|
export function createLogger({
|
|
level = process.env.LOG_LEVEL,
|
|
format = process.env.LOG_FORMAT,
|
|
stream = process.stderr,
|
|
bindings = {},
|
|
} = {}) {
|
|
const configuredLevel = normalizeLevel(level)
|
|
const configuredFormat = normalizeFormat(format)
|
|
const threshold = LEVELS[configuredLevel]
|
|
const parentBindings = { ...bindings }
|
|
|
|
function write(logLevel, message, fields = {}) {
|
|
if (LEVELS[logLevel] < threshold) return
|
|
|
|
const timestamp = new Date().toISOString()
|
|
const merged = { ...parentBindings, ...fields }
|
|
const sanitized = {}
|
|
for (const [key, value] of Object.entries(merged)) {
|
|
if (value === undefined) continue
|
|
sanitized[key] = SECRET_KEY_RE.test(key) ? REDACTED : sanitizeValue(value)
|
|
}
|
|
|
|
const line =
|
|
configuredFormat === 'json'
|
|
? formatJson(timestamp, logLevel, String(message), sanitized)
|
|
: formatText(timestamp, logLevel, String(message), sanitized)
|
|
|
|
try {
|
|
stream.write(line)
|
|
} catch {
|
|
// Logging must never break an export.
|
|
}
|
|
}
|
|
|
|
return {
|
|
level: configuredLevel,
|
|
format: configuredFormat,
|
|
bindings: parentBindings,
|
|
isLevelEnabled(logLevel) {
|
|
return LEVELS[logLevel] >= threshold
|
|
},
|
|
child(extraBindings = {}) {
|
|
return createLogger({
|
|
level: configuredLevel,
|
|
format: configuredFormat,
|
|
stream,
|
|
bindings: { ...parentBindings, ...extraBindings },
|
|
})
|
|
},
|
|
debug(message, fields) {
|
|
write('debug', message, fields)
|
|
},
|
|
info(message, fields) {
|
|
write('info', message, fields)
|
|
},
|
|
warn(message, fields) {
|
|
write('warn', message, fields)
|
|
},
|
|
error(message, fields) {
|
|
write('error', message, fields)
|
|
},
|
|
}
|
|
}
|
|
|
|
export const logger = createLogger({ bindings: { app: 'wiki-print' } })
|