Initial commit
This commit is contained in:
commit
2683eae6b3
36 files changed
+7369
No files matched your search
+143
@@ -0,0 +1,143 @@
|
||||
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' } })
|
||||
Reference in new issue
Block a user