import { inspect as nodeInspect } from "node:util";
import * as winston from "winston";
import "winston-daily-rotate-file";
import objectInspect from "object-inspect";
/**
* Utility functions for logging.
* @module Log
*/
/**
* 记录信息性消息到控制台
* @param {...*} args - 要记录的参数列表
*/
export function logInfo(...args) {
// eslint-disable-next-line no-console
console.log(...args);
}
/**
* 记录警告消息到控制台
* @param {...*} args - 要记录的参数列表
*/
export function logWarn(...args) {
// eslint-disable-next-line no-console
console.warn(...args);
}
/**
* 记录错误消息到控制台
* @param {...*} args - 要记录的参数列表
*/
export function logError(...args) {
// eslint-disable-next-line no-console
console.error(...args);
}
/**
* Converts symbol keys to string keys in an object.
* @param {Record<string | symbol, unknown>} obj
* @returns {Record<string, unknown>}
* @ignore
*/
function convertSymbolKeys(obj) {
const result = { ...obj };
const symbols = Object.getOwnPropertySymbols(obj);
symbols.forEach((sym) => {
const keyDescription = sym.description;
if (keyDescription) {
result[keyDescription] = obj[sym];
delete result[sym];
}
});
return result;
}
/**
*
* @returns {string}
*/
function getCallSite() {
const stack = new Error().stack; // 我们只要 stack,不要抛异常
if (!stack) return "unknown";
// 0: Error
// 1: getCallSite 本身
// 2: 真实调用者(我们要的)
const lines = stack.split("\n");
const callSiteLines = lines.slice(2);
const matchLines = [];
for (const line of callSiteLines) {
// 把带有node_modules的行去掉
if (line.indexOf("node_modules") !== -1) {
continue;
}
matchLines.push(line.replace(/^\s*at\s*/, ""));
}
return matchLines.join(" <= ");
}
/**
* Creates a Winston logger with a daily rotating file transport and optional console transport.
*
* @param {Object} options - Configuration options for the logger.
* @param {string} [options.level="info"] - The log level.
* @param {Record<string, string>} [options.meta={}] - The name of the server.
* @param {string} [options.filename="./logs/application-%DATE%.log"] - The filename pattern for the log files.
* @param {number} [options.maxLength=1000] - The maximum length of the log message.
* @param {boolean} [options.zippedArchive=false] - Whether to zip old log files.
* @param {boolean} [options.enableConsole=false] - Whether to enable console logging.
* @param {boolean} [options.includeCallSite=false] - Whether to include the call site in the log message.
* @param {string} [options.inspector='nodeInspect'] - The inspector function to use for formatting log messages. Support 'nodeInspect' or 'objectInspect'
* @returns {winston.Logger} - The configured Winston logger instance.
*
* @example
* import { createLogger } from "nsuite";
* export const logger = createLogger({
* level: "info",
* meta: {
* serverName: "your-server-name",
* NODE_ENV: process.env.NODE_ENV,
* MODE: process.env.MODE,
* },
* maxLength: 1000,
* filename: "./logs/application-%DATE%.log",
* zippedArchive: false,
* enableConsole: process.env.NODE_ENV !== "production",
* });
*/
export function createLogger({
level = "info",
meta = {},
filename = "./logs/application-%DATE%.log",
maxLength = 1000,
zippedArchive = false,
enableConsole = false,
includeCallSite = false,
inspector = "nodeInspect",
}) {
const transport = new winston.transports.DailyRotateFile({
filename,
datePattern: "YYYY-MM-DD-HH",
zippedArchive,
maxSize: "20m",
maxFiles: "14d",
});
transport.on("error", (error) => {
// log or handle errors here
logError("transport on error");
logError(error);
});
transport.on("rotate", (oldFilename, newFilename) => {
// do something fun
logInfo(`log file rotated from ${oldFilename} to ${newFilename}`);
});
const inspect = inspector === "nodeInspect" ? nodeInspect : objectInspect;
const logger = winston.createLogger({
level,
format: winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.printf(({ timestamp, level, message, ...args }) => {
const argsObj = convertSymbolKeys(args);
const ignoreMetaKeys = ["level", "splat", "message"];
const metaDataStringArr = [];
for (const key in argsObj) {
const metaValue = argsObj[key];
if (!ignoreMetaKeys.includes(key)) {
metaDataStringArr.push(`${key}=${String(metaValue)}`);
}
}
/** @type {unknown[]} */
const splat = Array.isArray(argsObj.splat)
? argsObj.splat
: argsObj.splat
? [argsObj.splat]
: [];
const msgArr = [`${timestamp} ${level}`];
if (typeof message === "string") {
msgArr.push(message);
} else {
msgArr.push(inspect(message));
}
if (splat.length > 0) {
msgArr.push(...splat.map((item) => inspect(item)));
}
msgArr.push(`#meta=>${metaDataStringArr.join(",")}`);
if (includeCallSite && level !== "info") {
msgArr.push(`#callSite=>${getCallSite()}`);
}
const msg = msgArr.join(" ");
if (msg.length <= maxLength) {
return msg;
}
return `${msg.substring(0, maxLength)}...`;
}),
// 确保错误堆栈信息被捕获
winston.format.errors({ stack: true }),
),
defaultMeta: meta,
transports: [transport],
});
if (enableConsole) {
logger.add(
new winston.transports.Console({
// format: winston.format.simple(),
}),
);
}
return logger;
}