f4b404's picture
Upload folder using huggingface_hub
f55c62a verified
Raw
History Blame Contribute Delete
8.64 kB
import pino, { type DestinationStream, type Logger as PinoLogger } from 'pino';
import { prettyFactory } from 'pino-pretty';
import { Writable } from 'stream';
import { bootstrap } from '../config/bootstrap.js';
import { redactUrlParams } from './redact.js';
import { logRingBuffer } from './ring-buffer.js';
export interface Logger {
trace(...args: LogArgs): void;
debug(...args: LogArgs): void;
info(...args: LogArgs): void;
warn(...args: LogArgs): void;
error(...args: LogArgs): void;
fatal(...args: LogArgs): void;
/** @deprecated legacy winston level β€” alias for `debug`. */
verbose(...args: LogArgs): void;
/** @deprecated legacy winston level β€” alias for `trace`. */
silly(...args: LogArgs): void;
/** @deprecated legacy winston level β€” alias for `info`. */
http(...args: LogArgs): void;
child(bindings: Record<string, unknown>): Logger;
}
export type LogArg = unknown;
type LogArgs = LogArg[];
type Level = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
// --- Pino root setup ------------------------------------------------------
function normalizeLevel(raw: string | undefined): Level {
if (!raw) return 'info';
const v = raw.toLowerCase();
switch (v) {
case 'silly':
return 'trace';
case 'verbose':
return 'debug';
case 'http':
return 'info';
case 'warning':
return 'warn';
}
if (
v === 'trace' ||
v === 'debug' ||
v === 'info' ||
v === 'warn' ||
v === 'error' ||
v === 'fatal'
) {
return v;
}
return 'info';
}
function buildDestination(): DestinationStream | Writable {
const format = (bootstrap.logFormat || '').toLowerCase();
if (format !== 'text') {
return pino.destination({ dest: 1, sync: false });
}
// Text mode: pino emits NDJSON to this stream; we parse each line,
// redact URLs in msg, then re-emit via pino-pretty.
const prettify = prettyFactory({ colorize: true, sync: true });
return new Writable({
write(chunk: Buffer | string, _enc, cb) {
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
for (const line of text.split('\n')) {
if (!line) continue;
try {
const obj = JSON.parse(line) as Record<string, unknown>;
if (typeof obj.msg === 'string') {
obj.msg = redactUrlParams(obj.msg);
}
process.stdout.write(prettify(obj));
} catch {
process.stdout.write(line + '\n');
}
}
cb();
},
});
}
/**
* Stream B for the multistream: tees every NDJSON line into the in-memory
* ring buffer that backs the dashboard Logs page. Lines arrive already
* redacted (pino applies `redact` before any stream sees the record), so the
* dashboard can never leak secrets. Chunks are not guaranteed to be
* line-aligned, so we buffer a partial trailing fragment.
*/
function buildRingStream(): Writable {
let partial = '';
return new Writable({
write(chunk: Buffer | string, _enc, cb) {
const text =
partial + (typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
const lines = text.split('\n');
partial = lines.pop() ?? '';
for (const line of lines) {
if (line) logRingBuffer.push(line);
}
cb();
},
});
}
const root: PinoLogger = pino(
{
level: normalizeLevel(bootstrap.logLevel),
base: {
// Per `04-logging.md`, every record carries `instance` so the
// Logs dashboard can fan out across replicas. The ID is shared
// with the future replicas heartbeat table (see `06-dashboard.md`)
// so a log line and a heartbeat row referring to the same process
// carry the same `instance` value.
// instanceId: INSTANCE_ID,
},
formatters: {
// Emit `level` as the textual name, not pino's numeric code.
level(label) {
return { level: label };
},
},
timestamp: pino.stdTimeFunctions.isoTime,
serializers: {
err: pino.stdSerializers.err,
},
},
pino.multistream([
{ level: 'trace', stream: buildDestination() },
{ level: 'trace', stream: buildRingStream() },
])
);
// --- Wrapper that accepts both new- and legacy-style calls ----------------
/**
* Normalize args into pino's `(obj, msg)` shape.
*
* Cases:
* () β†’ ({}, undefined) (no-op skipped by caller)
* ('msg') β†’ ({}, 'msg')
* ('msg', err) β†’ ({ err }, 'msg')
* ('msg', { a, b }) β†’ ({ a, b }, 'msg') (legacy winston style)
* ('msg', { a }, { b }) β†’ ({ a, b }, 'msg') (legacy)
* ('msg', 'extra') β†’ ({}, 'msg extra') (legacy)
* ({ a }) β†’ ({ a }, undefined)
* ({ a }, 'msg') β†’ ({ a }, 'msg') (canonical)
* ({ a }, 'msg', extra) β†’ ({ a, ...extra }, 'msg')
*/
function normalizeArgs(args: LogArgs): {
obj: Record<string, unknown>;
msg?: string;
} {
if (args.length === 0) return { obj: {} };
const first = args[0];
const obj: Record<string, unknown> = {};
let msg: string | undefined;
if (typeof first === 'string') {
// Legacy-style or simple message.
msg = first;
const extraStrings: string[] = [];
for (let i = 1; i < args.length; i++) {
const a = args[i];
if (a == null) continue;
if (a instanceof Error) {
obj.err = a;
} else if (typeof a === 'object') {
Object.assign(obj, a as Record<string, unknown>);
} else if (typeof a === 'string') {
extraStrings.push(a);
} else {
// numbers/booleans dropped silently β€” they were noise in the legacy API
}
}
if (extraStrings.length) {
msg = `${msg} ${extraStrings.join(' ')}`;
}
return { obj, msg };
}
if (first instanceof Error) {
obj.err = first;
if (typeof args[1] === 'string') msg = args[1] as string;
return { obj, msg };
}
if (typeof first === 'object' && first !== null) {
Object.assign(obj, first as Record<string, unknown>);
// Honour the legacy `{formatted: '...'}` shortcut: callers used this
// to pass a pre-rendered table/summary. We surface it as `msg` so
// the line stays readable; the sweep removes these.
if (
typeof (first as { formatted?: unknown }).formatted === 'string' &&
typeof args[1] !== 'string'
) {
msg = (first as { formatted: string }).formatted;
delete obj.formatted;
}
if (typeof args[1] === 'string') msg = args[1] as string;
for (let i = 2; i < args.length; i++) {
const a = args[i];
if (a && typeof a === 'object' && !(a instanceof Error)) {
Object.assign(obj, a as Record<string, unknown>);
} else if (a instanceof Error) {
obj.err = a;
}
}
return { obj, msg };
}
return { obj };
}
const legacyLevelMap: Record<string, Level> = {
silly: 'trace',
verbose: 'debug',
http: 'info',
};
function deconflictReservedKeys(obj: Record<string, unknown>): void {
if ('time' in obj) {
if (!('timeTaken' in obj)) obj.timeTaken = obj.time;
delete obj.time;
}
if ('level' in obj) {
if (!('levelLabel' in obj)) obj.levelLabel = obj.level;
delete obj.level;
}
}
function wrap(pinoInstance: PinoLogger): Logger {
const emit =
(level: Level | keyof typeof legacyLevelMap) =>
(...args: LogArgs): void => {
const target =
level in legacyLevelMap
? legacyLevelMap[level as keyof typeof legacyLevelMap]
: (level as Level);
const { obj, msg } = normalizeArgs(args);
deconflictReservedKeys(obj);
if (msg === undefined && Object.keys(obj).length === 0) return;
if (msg === undefined) {
pinoInstance[target](obj);
} else {
pinoInstance[target](obj, msg);
}
};
return {
trace: emit('trace'),
debug: emit('debug'),
info: emit('info'),
warn: emit('warn'),
error: emit('error'),
fatal: emit('fatal'),
verbose: emit('verbose'),
silly: emit('silly'),
http: emit('http'),
child(bindings) {
return wrap(pinoInstance.child(bindings));
},
};
}
// --- Public API -----------------------------------------------------------
export const logger: Logger = wrap(root);
/**
* Create a logger pre-tagged with a module name. Equivalent to
* `logger.child({ module })`; kept as a function for source-level
* compatibility with v2.
*/
export function createLogger(module: string): Logger {
return wrap(root.child({ module }));
}