e252cbca9b
- 开发环境 pino-pretty 彩色输出,生产环境 pino-roll 滚动写入 /var/log/trade/data - mixin 自动附加调用位置(caller)和 traceId 到每条日志 - redact 自动脱敏 api_key/api_secret 等敏感字段 - 新增 withTrace() 支持异步上下文 traceId 传递 - 新增 pino-roll 依赖
105 lines
3.0 KiB
TypeScript
105 lines
3.0 KiB
TypeScript
import { logging } from "../config";
|
|
import pino from "pino";
|
|
|
|
const targets: pino.TransportTargetOptions[] = [
|
|
{
|
|
target: "pino/file",
|
|
level: logging.level,
|
|
options: { destination: 1 },
|
|
},
|
|
];
|
|
|
|
if (logging.nodeEnv === "production") {
|
|
targets.push({
|
|
target: "pino-roll",
|
|
level: "info",
|
|
options: {
|
|
file: "/var/log/trade/data",
|
|
frequency: "daily",
|
|
mkdir: true,
|
|
size: "50m",
|
|
limit: { count: 30 },
|
|
},
|
|
});
|
|
}
|
|
|
|
/** 从 Bun 的 Error.stack 中解析调用位置,返回 "file:line" */
|
|
function resolveCaller(): string | undefined {
|
|
const stack = new Error().stack;
|
|
if (!stack) return undefined;
|
|
const lines = stack.split("\n");
|
|
// Bun stack format:
|
|
// 0: "Error"
|
|
// 1: " at resolveCaller (...)"
|
|
// 2: " at mixin (...)"
|
|
// 3-N: pino internal frames
|
|
// find first frame NOT matching pino/node_modules/logger.ts
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const line = lines[i]!;
|
|
if (
|
|
line.includes("pino") ||
|
|
line.includes("node_modules") ||
|
|
line.includes("utils/logger.ts") ||
|
|
line.includes("resolveCaller")
|
|
) {
|
|
continue;
|
|
}
|
|
const match = line.match(/at\s+(?:.*?\s+\()?(.+?):(\d+):\d+\)?$/);
|
|
if (match) {
|
|
const file = match[1]!;
|
|
const ln = match[2]!;
|
|
// 截取项目内相对路径
|
|
const idx = file.indexOf("/data/");
|
|
const short = idx >= 0 ? file.slice(idx + 6) : file;
|
|
return `${short}:${ln}`;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
const baseLogger = pino({
|
|
level: logging.level,
|
|
transport: logging.pretty
|
|
? {
|
|
targets: [
|
|
{
|
|
target: "pino-pretty",
|
|
level: logging.level,
|
|
options: { colorize: true, translateTime: "SYS:HH:MM:ss" },
|
|
},
|
|
...targets.filter((t) => t.target !== "pino/file"),
|
|
],
|
|
}
|
|
: { targets },
|
|
base: { module: "trade-data" },
|
|
serializers: { err: pino.stdSerializers.err },
|
|
redact: {
|
|
paths: ["api_key", "api_secret", "password", "apiKey", "apiSecret"],
|
|
censor: "***",
|
|
},
|
|
mixin() {
|
|
const tid = (globalThis as Record<string, unknown>).__traceId;
|
|
const caller = resolveCaller();
|
|
return { ...(tid ? { traceId: tid } : {}), ...(caller ? { caller } : {}) };
|
|
},
|
|
formatters: {
|
|
bindings(bindings) {
|
|
return { ...bindings, pid: undefined, hostname: undefined };
|
|
},
|
|
},
|
|
});
|
|
|
|
export const logger = baseLogger;
|
|
|
|
/** 设置当前异步上下文的 traceId,自动附加到后续所有日志 */
|
|
export function withTrace<T>(traceId: string, fn: () => T): T {
|
|
(globalThis as Record<string, unknown>).__traceId = traceId;
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
(globalThis as Record<string, unknown>).__traceId = undefined;
|
|
}
|
|
}
|
|
|
|
export default logger;
|