feat(logger): 日志系统升级 — 双通道输出 + caller/traceId + 脱敏
- 开发环境 pino-pretty 彩色输出,生产环境 pino-roll 滚动写入 /var/log/trade/data - mixin 自动附加调用位置(caller)和 traceId 到每条日志 - redact 自动脱敏 api_key/api_secret 等敏感字段 - 新增 withTrace() 支持异步上下文 traceId 传递 - 新增 pino-roll 依赖
This commit is contained in:
+96
-12
@@ -1,20 +1,104 @@
|
||||
// data/src/logger.ts
|
||||
import pino from "pino";
|
||||
import { logging } from "../config";
|
||||
import pino from "pino";
|
||||
|
||||
export const logger = 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,
|
||||
// 开发环境:使用 pino-pretty 彩色输出
|
||||
// 生产环境:JSON 格式,便于 ELK / Loki 采集
|
||||
...(logging.pretty
|
||||
? { transport: { target: "pino-pretty", options: { colorize: true } } }
|
||||
: {}),
|
||||
// 自动注入模块名
|
||||
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" },
|
||||
// 序列化 Error 对象
|
||||
serializers: {
|
||||
err: pino.stdSerializers.err,
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user