// Code originally derived from https://github.com/hadfieldn/node-bunyan-prettystream but since heavily edited // Neither fork nor original repo appear to be maintained import { Writable } from 'node:stream'; import * as util from 'node:util'; import { isNonEmptyObject, isPlainObject, isString } from '@sindresorhus/is'; import stringify from 'json-stringify-pretty-compact'; import { regEx } from '../util/regex.ts'; import type { BunyanRecord } from './types.ts'; const bunyanFields = [ 'name', 'hostname', 'pid', 'level', 'v', 'time', 'msg', 'start_time', ]; /** * If a log contains any of these meta fields in the meta, promote them up to the log line, rather than rendering them with other meta fields. * * This only applies when these fields have a string value, otherwise they are treated as a regular meta field. */ const metaFields = [ 'repository', 'baseBranch', 'packageFile', 'depType', 'dependency', 'branch', ]; const levels: Record = { 10: 'TRACE', 20: 'DEBUG', 30: ' INFO', 40: ' WARN', 50: 'ERROR', 60: 'FATAL', }; const colorizedLevels: Record = { 10: util.styleText('gray', 'TRACE'), 20: util.styleText('blue', 'DEBUG'), 30: util.styleText('green', ' INFO'), 40: util.styleText('magenta', ' WARN'), 50: util.styleText('red', 'ERROR'), 60: util.styleText('bgRed', 'FATAL'), }; export function indent(str: string, leading = false): string { const prefix = leading ? ' ' : ''; return prefix + str.split(regEx(/\r?\n/)).join('\n '); } export function getMeta(rec: BunyanRecord, colorize = true): string { if (!rec) { return ''; } let res = rec.module ? ` [${rec.module}]` : ``; const filteredMeta = metaFields.filter((elem) => isString(rec[elem])); if (!filteredMeta.length) { return res; } const metaStr = filteredMeta .map((field) => `${field}=${String(rec[field])}`) .join(', '); res = ` (${metaStr})${res}`; return colorize ? util.styleText('gray', res) : res; } export function getDetails(rec: BunyanRecord): string { if (!rec) { return ''; } const recFiltered = { ...rec }; delete recFiltered.module; Object.keys(recFiltered).forEach((key) => { if ( key === 'logContext' || bunyanFields.includes(key) || (metaFields.includes(key) && isString(recFiltered[key])) ) { delete recFiltered[key]; } }); const remainingKeys = Object.keys(recFiltered); if (remainingKeys.length === 0) { return ''; } // Handle err.stack specially for readable multi-line output const err = recFiltered.err; if (isPlainObject(err) && isString(err.stack)) { const { stack, ...errRest } = err; recFiltered.err = isNonEmptyObject(errRest) ? errRest : undefined; const parts: string[] = []; for (const key of remainingKeys) { if (key === 'err' && recFiltered.err === undefined) { continue; } parts.push(indent(`"${key}": ${stringify(recFiltered[key])}`, true)); } const jsonPart = parts.join(',\n'); const stackPart = indent(stack, true); return jsonPart ? `${jsonPart}\n${stackPart}\n` : `${stackPart}\n`; } return `${remainingKeys .map((key) => `${indent(`"${key}": ${stringify(recFiltered[key])}`, true)}`) .join(',\n')}\n`; } export function formatRecord(rec: BunyanRecord, colorize = true): string { const level = colorize ? colorizedLevels[rec.level] : levels[rec.level]; const msg = `${indent(rec.msg)}`; const meta = getMeta(rec, colorize); const details = getDetails(rec); return util.format('%s: %s%s\n%s', level, msg, meta, details); } export class PrettyStdoutStream extends Writable { constructor() { super({ objectMode: true }); } override _write( data: BunyanRecord, _encoding: string, callback: (error?: Error | null) => void, ): void { process.stdout.write(formatRecord(data)); callback(); } }