refactor: normalize logger call sites for winston

This commit is contained in:
MythEclipse
2026-05-19 02:39:18 +07:00
parent 2ab097a0c3
commit 1aa8c6a370
2 changed files with 191 additions and 23 deletions
+189 -21
View File
@@ -1,28 +1,196 @@
import pino from "pino"; import fs from "node:fs";
import path from "node:path";
import winston from "winston";
const isDev = process.env.NODE_ENV !== "production"; const isDev = process.env.NODE_ENV !== "production";
const logLevel = process.env.LOG_LEVEL || (isDev ? "debug" : "info");
const logsDir = path.resolve(process.cwd(), "logs");
export const logger = pino({ fs.mkdirSync(logsDir, { recursive: true });
level: process.env.LOG_LEVEL || (isDev ? "debug" : "info"),
serializers: { type LogMetadata = Record<string, unknown>;
error: pino.stdSerializers.err,
err: pino.stdSerializers.err, type SerializedError = {
reason: (value) => name: string;
value instanceof Error ? pino.stdSerializers.err(value) : value, message: string;
}, stack?: string;
transport: isDev code?: unknown;
? { statusCode?: unknown;
target: "pino-pretty", } & Record<string, unknown>;
options: {
colorize: true, const serializeError = (error: Error): SerializedError => {
translateTime: "SYS:standard", const serialized: SerializedError = {
ignore: "pid,hostname", name: error.name,
}, message: error.message,
} };
: undefined,
timestamp: pino.stdTimeFunctions.isoTime, if (error.stack) {
serialized.stack = error.stack;
}
const errorWithFields = error as Error & {
code?: unknown;
statusCode?: unknown;
[key: string]: unknown;
};
if (errorWithFields.code !== undefined) {
serialized.code = errorWithFields.code;
}
if (errorWithFields.statusCode !== undefined) {
serialized.statusCode = errorWithFields.statusCode;
}
for (const [key, value] of Object.entries(errorWithFields)) {
if (serialized[key] === undefined) {
serialized[key] = value;
}
}
return serialized;
};
const serializeLogValue = (value: unknown): unknown => {
if (value instanceof Error) {
return serializeError(value);
}
if (Array.isArray(value)) {
return value.map(serializeLogValue);
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, nestedValue]) => [
key,
serializeLogValue(nestedValue),
]),
);
}
return value;
};
const formatLogMetadata = (metadata: LogMetadata): LogMetadata => {
return Object.fromEntries(
Object.entries(metadata).map(([key, value]) => [key, serializeLogValue(value)]),
);
};
const metadataFormat = winston.format((info) => {
const { level, message, timestamp, ...metadata } = info;
return {
level,
message,
timestamp,
...formatLogMetadata(metadata),
};
}); });
export const createChildLogger = (context: string) => { const consoleFormat = winston.format.printf((info) => {
const { level, message, timestamp, context, ...metadata } = info;
const contextLabel = context ? ` [${String(context)}]` : "";
const metadataText = Object.keys(metadata).length
? ` ${JSON.stringify(metadata)}`
: "";
return `${timestamp} ${level}${contextLabel}: ${message}${metadataText}`;
});
export interface CustomLogger {
error: (msgOrObj: any, msgOrArgs?: any, ...args: any[]) => void;
warn: (msgOrObj: any, msgOrArgs?: any, ...args: any[]) => void;
info: (msgOrObj: any, msgOrArgs?: any, ...args: any[]) => void;
debug: (msgOrObj: any, msgOrArgs?: any, ...args: any[]) => void;
trace: (msgOrObj: any, msgOrArgs?: any, ...args: any[]) => void;
fatal: (msgOrObj: any, msgOrArgs?: any, ...args: any[]) => void;
silent: (msgOrObj: any, msgOrArgs?: any, ...args: any[]) => void;
child(options: { context: string } & Record<string, any>): CustomLogger;
[key: string]: any;
}
const winstonLogger = winston.createLogger({
level: logLevel,
levels: winston.config.npm.levels,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
metadataFormat(),
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
metadataFormat(),
consoleFormat,
),
}),
new winston.transports.File({
filename: path.join(logsDir, "app.log"),
format: winston.format.json(),
}),
new winston.transports.File({
filename: path.join(logsDir, "error.log"),
level: "error",
format: winston.format.json(),
}),
],
});
function wrapLogger(wLogger: winston.Logger): CustomLogger {
const logAtLevel = (level: string) => {
return (arg1: any, arg2?: any) => {
if (typeof arg1 === "object" && arg1 !== null) {
// arg1 is object, arg2 is message string
const msg = typeof arg2 === "string" ? arg2 : "";
wLogger.log(level, msg, { ...arg1 });
} else {
// arg1 is message string, arg2 is metadata (or nothing)
const msg = typeof arg1 === "string" ? arg1 : String(arg1);
const meta = typeof arg2 === "object" && arg2 !== null ? arg2 : {};
wLogger.log(level, msg, meta);
}
};
};
const wrapped: CustomLogger = {
error: logAtLevel("error"),
warn: logAtLevel("warn"),
info: logAtLevel("info"),
debug: logAtLevel("debug"),
trace: logAtLevel("debug"), // Map trace to debug in Winston npm levels
fatal: logAtLevel("error"), // Map fatal to error in Winston npm levels
silent: () => {},
child: (options: any) => {
const childWinston = wLogger.child(options);
return wrapLogger(childWinston);
}
};
// Add all winston.Logger properties/methods to custom logger to make typescript happy for standard properties
const proxy = new Proxy(wrapped, {
get(target, prop) {
if (prop in target) {
return (target as any)[prop];
}
const val = (wLogger as any)[prop];
if (typeof val === "function") {
return val.bind(wLogger);
}
return val;
}
});
return proxy;
}
export const logger: CustomLogger = wrapLogger(winstonLogger);
export const createChildLogger = (context: string): CustomLogger => {
return logger.child({ context }); return logger.child({ context });
}; };
export const serializeLogValueForTest = serializeLogValue;
export const formatLogMetadataForTest = formatLogMetadata;
+2 -2
View File
@@ -1,12 +1,12 @@
import pRetry from "p-retry"; import pRetry from "p-retry";
import type { Logger } from "pino"; import type { CustomLogger } from "./logger.ts";
export interface RetryOptions { export interface RetryOptions {
retries?: number; retries?: number;
minTimeout?: number; minTimeout?: number;
maxTimeout?: number; maxTimeout?: number;
factor?: number; factor?: number;
logger?: Logger; logger?: CustomLogger;
} }
export async function retryWithBackoff<T>( export async function retryWithBackoff<T>(