UNPKG

@logux/server

Version:

Build own Logux server

275 lines (241 loc) 7.7 kB
import os from 'node:os' import { stripVTControlCharacters, styleText } from 'node:util' import { mulberry32, onceXmur3 } from './utils.js' const INDENT = ' ' const PADDING = ' ' const SEPARATOR = os.EOL + os.EOL const NEXT_LINE = os.EOL === '\n' ? '\r\v' : os.EOL const PARAMS_BLACKLIST = { component: true, err: true, hint: true, hostname: true, level: true, listen: true, msg: true, name: true, note: true, pid: true, server: true, time: true, v: true } const LABELS = { 20: str => label(' DEBUG ', 'white', 'bgWhite', 'black', str), 30: str => label(' INFO ', 'green', 'bgGreen', 'black', str), 40: str => label(' WARN ', 'yellow', 'bgYellow', 'black', str), 50: str => label(' ERROR ', 'red', 'bgRed', 'white', str), 60: str => label(' FATAL ', 'red', 'bgRed', 'white', str) } const COLORS = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan'] function formatNow() { let date = new Date() let year = date.getFullYear() let month = String(date.getMonth() + 1).padStart(2, '0') let day = String(date.getDate()).padStart(2, '0') let hour = String(date.getHours()).padStart(2, '0') let minutes = String(date.getMinutes()).padStart(2, '0') let seconds = String(date.getSeconds()).padStart(2, '0') return `${year}-${month}-${day} ${hour}:${minutes}:${seconds}` } function rightPag(str, length) { let add = length - stripVTControlCharacters(str).length for (let i = 0; i < add; i++) str += ' ' return str } function label(type, color, labelBg, labelText, message) { let pagged = rightPag(styleText(labelBg, styleText(labelText, type)), 8) let time = styleText('dim', `at ${formatNow()}`) let highlighted = message.replace(/`([^`]+)`/g, styleText('yellow', '$1')) return `${pagged}${styleText('bold', styleText(color, highlighted))} ${time}` } function formatName(key) { return key .replace(/[A-Z]/g, char => ` ${char.toLowerCase()}`) .split(' ') .map(word => (word === 'ip' || word === 'id' ? word.toUpperCase() : word)) .join(' ') .replace(/^\w/, char => char.toUpperCase()) } function shuffledColors(str) { let index = -1 let result = Array.from(COLORS) let lastIndex = result.length - 1 let seed = onceXmur3(str) let randomFn = mulberry32(seed) while (++index < COLORS.length) { let randIndex = index + Math.floor(randomFn() * (lastIndex - index + 1)) let value = result[randIndex] result[randIndex] = result[index] result[index] = value } return result } function splitAndColorize(partLength, str) { let strBuilder = [] let colors = shuffledColors(str) for ( let start = 0, end = partLength, n = 0, color = colors[n]; start < str.length; start += partLength, end += partLength, n = n + 1, color = colors[n % colors.length] ) { let strToColorize = str.slice(start, end) if (strToColorize.length === 1) { color = colors[Math.abs(n - 1) % colors.length] } strBuilder.push(styleText(color, strToColorize)) } return strBuilder.join('') } function formatNodeId(nodeId) { let pos = nodeId.lastIndexOf(':') if (pos === -1) { return nodeId } else { let s = nodeId.split(':') let id = styleText('bold', s[0]) let random = splitAndColorize(3, s[1]) return `${id}:${random}` } } function formatValue(value) { if (typeof value === 'string') { return '"' + styleText('bold', value) + '"' } else if (Array.isArray(value)) { return formatArray(value) } else if (typeof value === 'object' && value) { return formatObject(value) } else { return styleText('bold', `${value}`) } } function formatObject(obj) { let items = Object.keys(obj).map(k => `${k}: ${formatValue(obj[k])}`) return '{ ' + items.join(', ') + ' }' } function formatArray(array) { let items = array.map(i => formatValue(i)) return '[' + items.join(', ') + ']' } function formatActionId(id) { let p = id.split(' ') if (p.length === 1) { return p } return ( `${styleText('bold', splitAndColorize(4, p[0]))} ` + `${formatNodeId(p[1])} ${styleText('bold', p[2])}` ) } function formatParams(params, parent) { let maxName = params.reduce((max, param) => { let name = param[0] return name.length > max ? name.length : max }, 0) return params .map(param => { let name = param[0] let value = param[1] let start = PADDING + rightPag(`${name}: `, maxName + 2) if (name === 'Node ID' || (parent === 'Meta' && name === 'server')) { return start + formatNodeId(value) } else if ( parent === 'Meta' && (name === 'clients' || name === 'excludeClients') ) { return `${start}[${value.map(v => `"${formatNodeId(v)}"`).join()}]` } else if (name === 'Action ID' || (parent === 'Meta' && name === 'id')) { return start + formatActionId(value) } else if (Array.isArray(value)) { return start + formatArray(value) } else if (typeof value === 'object' && value) { let nested = Object.keys(value).map(key => [key, value[key]]) return ( start + NEXT_LINE + INDENT + formatParams(nested, name) .split(NEXT_LINE) .join(NEXT_LINE + INDENT) ) } else if (typeof value === 'string' && parent) { return start + '"' + styleText('bold', value) + '"' } else { return start + styleText('bold', `${value}`) } }) .join(NEXT_LINE) } function splitByLength(string, max) { let words = string.split(' ') let lines = [''] for (let word of words) { let last = lines[lines.length - 1] if (last.length + word.length > max) { lines.push(`${word} `) } else { lines[lines.length - 1] = `${last}${word} ` } } return lines.map(i => i.trim()) } function prettyStackTrace(stack, basepath) { return stack .split('\n') .slice(1) .map(line => { let match = line.match(/\s+at ([^(]+) \(([^)]+)\)/) let isSystem = !match || !match[2].startsWith(basepath) if (isSystem) { return styleText('gray', line.replace(/^\s*/, PADDING)) } else { let func = match[1] let relative = match[2].slice(basepath.length) let converted = `${PADDING}at ${func} (./${relative})` let isDependency = match[2].includes('node_modules') return isDependency ? styleText('gray', converted) : styleText('red', converted) } }) .join(NEXT_LINE) } export default function humanFormatter(options) { let basepath = options.basepath return function format(record) { let message = [LABELS[record.level](record.msg)] let params = Object.keys(record) .filter(key => !PARAMS_BLACKLIST[key]) .map(key => [formatName(key), record[key]]) if (record.loguxServer) { params.unshift(['PID', record.pid]) if (record.server) { params.push(['Listen', 'Custom HTTP server']) } else { params.push(['Listen', record.listen]) } } if (record.err && record.err.stack) { message.push(prettyStackTrace(record.err.stack, basepath)) } message.push(formatParams(params)) if (record.note) { let note = record.note if (typeof note === 'string') { note = note.replace(/`([^`]+)`/g, styleText('bold', '$1')) note = [].concat( ...note .split('\n') .map(row => splitByLength(row, 80 - PADDING.length)) ) } message.push( note.map(i => PADDING + styleText('gray', i)).join(NEXT_LINE) ) } return message.filter(i => i !== '').join(NEXT_LINE) + SEPARATOR } }