Files
renovate/lib/logger/pretty-stdout.ts
Jamie Tanna e456197add fix(logger): only truncate metaFields when they're string values (#45478)
When certain meta fields are logged, we pull them out of the meta fields
and into the log line, such as:

    DEBUG: Branch is not pending, removing pending upgrades (branch="renovate/express-4.x")

However, this also would affect non-string values such as:

    DEBUG: Git operations statistics (branch="[object Object]")

This only affects the "pretty" logs, but would mean no `branch` metadata
appears.

Instead, we can only include them when they're a string.

Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
2026-08-26 12:13:22 +00:00

138 lines
3.8 KiB
TypeScript

// 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<number, string> = {
10: 'TRACE',
20: 'DEBUG',
30: ' INFO',
40: ' WARN',
50: 'ERROR',
60: 'FATAL',
};
const colorizedLevels: Record<number, string> = {
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();
}
}