Files
trade/data/utils/logger.ts
T
Rekey e252cbca9b feat(logger): 日志系统升级 — 双通道输出 + caller/traceId + 脱敏
- 开发环境 pino-pretty 彩色输出,生产环境 pino-roll 滚动写入 /var/log/trade/data
- mixin 自动附加调用位置(caller)和 traceId 到每条日志
- redact 自动脱敏 api_key/api_secret 等敏感字段
- 新增 withTrace() 支持异步上下文 traceId 传递
- 新增 pino-roll 依赖
2026-06-17 12:05:48 +08:00

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;