Files
renovate/lib/logger/renovate-logger.ts
Sebastian Poxhofer a473e73b3e fix(linters): forbid inert counts in v8 coverage-ignore comments (#45287)
Adds a custom oxlint rule renovate/v8-ignore-no-count that flags the
count form '/* v8 ignore next <N> */'. The count is not honored by
vitest's ast-v8-to-istanbul hint parser, which captures only the
keyword (if/else/next/file); 'next' always exempts exactly the next
AST node. Trailing counts therefore suggest a line range that is never
applied and mislead readers.

The rule is autofixable (drops the count, preserving actual behavior)
and has been applied to all existing occurrences.
2026-08-22 11:25:31 +00:00

223 lines
6.1 KiB
TypeScript

import { isString } from '@sindresorhus/is';
import { LOGGER_NOT_INITIALIZED } from '../constants/error-messages.ts';
import { once, reset as onceReset } from './once.ts';
import { getRemappedLevel } from './remap.ts';
import type {
BunyanLogLevel,
BunyanLogger,
BunyanSerializers,
BunyanStream,
Logger,
} from './types.ts';
import { getMessage, toMeta } from './utils.ts';
import { withSanitizer } from './with-sanitizer.ts';
const loggerLevels: BunyanLogLevel[] = [
'trace',
'debug',
'info',
'warn',
'error',
'fatal',
];
type LoggerFunction = (p1: string | Record<string, any>, p2?: string) => void;
export class RenovateLogger implements Logger {
private readonly queue: (() => void)[] = [];
readonly logger: Logger = { once: { reset: onceReset } } as any;
readonly once = this.logger.once;
private bunyanLogger: BunyanLogger | undefined;
private uninitializedWarningFired: boolean;
private context: string;
private meta: Record<string, unknown>;
constructor(
context: string,
meta: Record<string, unknown>,
bunyanLogger?: BunyanLogger,
) {
this.bunyanLogger = bunyanLogger;
this.context = context;
this.meta = meta;
for (const level of loggerLevels) {
this.logger[level] = this.logFactory(level) as never;
this.logger.once[level] = this.logOnceFn(level);
}
this.uninitializedWarningFired = false;
}
trace(p1: string): void;
trace(p1: Record<string, any>, p2?: string): void;
trace(p1: string | Record<string, any>, p2?: string): void {
this.log('trace', p1, p2);
}
debug(p1: string): void;
debug(p1: Record<string, any>, p2?: string): void;
debug(p1: string | Record<string, any>, p2?: string): void {
this.log('debug', p1, p2);
}
info(p1: string): void;
info(p1: Record<string, any>, p2?: string): void;
info(p1: string | Record<string, any>, p2?: string): void {
this.log('info', p1, p2);
}
warn(p1: string): void;
warn(p1: Record<string, any>, p2?: string): void;
warn(p1: string | Record<string, any>, p2?: string): void {
this.log('warn', p1, p2);
}
error(p1: string): void;
error(p1: Record<string, any>, p2?: string): void;
error(p1: string | Record<string, any>, p2?: string): void {
this.log('error', p1, p2);
}
fatal(p1: string): void;
fatal(p1: Record<string, any>, p2?: string): void;
fatal(p1: string | Record<string, any>, p2?: string): void {
this.log('fatal', p1, p2);
}
addSerializers(serializers: BunyanSerializers): void {
this.ensureLogger().addSerializers(serializers);
}
addStream(stream: BunyanStream): void {
this.ensureLogger().addStream(withSanitizer(stream));
}
childLogger(): RenovateLogger {
return new RenovateLogger(
this.context,
this.meta,
this.ensureLogger().child({}),
);
}
levels(name: 'stdout' | 'logfile', level: BunyanLogLevel): void {
this.ensureLogger().levels(name, level);
}
get logContext(): string {
return this.context;
}
set logContext(context: string) {
this.context = context;
}
/**
* For internal initialization only
*/
set bunyan(bunyanLogger: BunyanLogger) {
this.bunyanLogger = bunyanLogger;
// flush any logs that were queued before bunyan logger was initialized
for (const logFn of this.queue) {
logFn();
}
this.queue.length = 0;
}
setMeta(obj: Record<string, unknown>): void {
this.meta = { ...obj };
}
addMeta(obj: Record<string, unknown>): void {
this.meta = { ...this.meta, ...obj };
}
removeMeta(fields: string[]): void {
for (const key of Object.keys(this.meta)) {
if (fields.includes(key)) {
delete this.meta[key];
}
}
}
private ensureLogger(): BunyanLogger {
if (!this.bunyanLogger) {
throw new Error(LOGGER_NOT_INITIALIZED);
}
return this.bunyanLogger;
}
private logFactory(_level: BunyanLogLevel): LoggerFunction {
return (p1: string | Record<string, any>, p2?: string): void => {
const meta: Record<string, unknown> = {
logContext: this.context,
...this.meta,
...toMeta(p1),
};
const msg = getMessage(p1, p2);
let level = _level;
if (isString(msg)) {
const remappedLevel = getRemappedLevel(msg);
/* v8 ignore next -- not easily testable */
if (remappedLevel) {
meta.oldLevel = level;
level = remappedLevel;
}
this.ensureLogger()[level](meta, msg);
} else {
this.ensureLogger()[level](meta);
}
};
}
private logOnceFn(level: BunyanLogLevel): LoggerFunction {
// Must stay an arrow function: the returned function is stored on
// `this.logger.once[level]` and invoked from there, so it relies on
// lexically-bound `this` (this RenovateLogger instance) rather than
// whatever `this` the call site would otherwise supply to a plain
// `function` declaration.
// oxlint-disable-next-line func-style -- see comment above
const logOnceFn = (p1: string | Record<string, any>, p2?: string): void => {
once(
() => {
const logFn = this[level].bind(this); // bind to the instance.
if (isString(p1)) {
logFn(p1);
} else {
logFn(p1, p2);
}
},
logOnceFn,
p1,
p2,
);
};
return logOnceFn;
}
private log(
level: BunyanLogLevel,
p1: string | Record<string, any>,
p2?: string,
): void {
if (!this.bunyanLogger) {
// defer logging until bunyan logger is initialized, to avoid losing logs during initialization
this.queue.push(() => this.log(level, p1, p2));
if (!this.uninitializedWarningFired) {
// oxlint-disable-next-line no-console -- intentional: display warning when bunyan isn't initialized
console.warn(
`⚠️ NOTE ⚠️: Renovate's logger has not yet been initialized. If you see no other output, this is a bug`,
);
this.uninitializedWarningFired = true;
}
return;
}
const logFn = this.logger[level];
if (isString(p1)) {
logFn(p1);
} else {
logFn(p1, p2);
}
}
}