1
0

feat: 运行时日志系统,Pino + pino-pretty + pino-roll,console/file 双输出,敏感信息 redaction

This commit is contained in:
2026-05-21 12:21:59 +08:00
parent 0d709c7681
commit 007d74934d
26 changed files with 1713 additions and 114 deletions

View File

@@ -15,6 +15,7 @@ import { checkerRegistry } from "../../src/server/checker/runner";
import { CommandChecker } from "../../src/server/checker/runner/cmd/execute";
import { HttpChecker } from "../../src/server/checker/runner/http/execute";
import { ProbeStore } from "../../src/server/checker/store";
import { createNoopLogger } from "../../src/server/logger";
import { startServer } from "../../src/server/server";
import { rmRetry } from "../helpers";
@@ -173,6 +174,7 @@ describe("API 路由", () => {
server = startServer({
config: { host: "127.0.0.1", port: 0 },
logger: createNoopLogger(),
mode: "test",
store,
version: "0.1.0",
@@ -410,6 +412,7 @@ describe("API 路由", () => {
test("生产响应包含安全 headers", async () => {
const prodServer = startServer({
config: { host: "127.0.0.1", port: 0 },
logger: createNoopLogger(),
mode: "production",
store,
version: "0.1.0",
@@ -424,35 +427,29 @@ describe("API 路由", () => {
});
test("损坏的 failure JSON 返回 null 而不崩溃", async () => {
const originalWarn = console.warn;
console.warn = () => undefined;
try {
const targets = store.getTargets();
const t1Id = targets[0]!.id;
const targets = store.getTargets();
const t1Id = targets[0]!.id;
store.insertCheckResult({
durationMs: 100,
failure: { kind: "error", message: "test", path: "$", phase: "body" },
matched: false,
observation: null,
targetId: t1Id,
timestamp: "2025-06-01T00:00:00.000Z",
});
store.insertCheckResult({
durationMs: 100,
failure: { kind: "error", message: "test", path: "$", phase: "body" },
matched: false,
observation: null,
targetId: t1Id,
timestamp: "2025-06-01T00:00:00.000Z",
});
(store as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db
.prepare("UPDATE check_results SET failure = ? WHERE target_id = ? AND timestamp = ?")
.run("{invalid json!!!", t1Id, "2025-06-01T00:00:00.000Z");
(store as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db
.prepare("UPDATE check_results SET failure = ? WHERE target_id = ? AND timestamp = ?")
.run("{invalid json!!!", t1Id, "2025-06-01T00:00:00.000Z");
const from = "2025-06-01T00:00:00.000Z";
const to = "2025-06-01T23:59:59.999Z";
const response = await fetch(`${baseUrl}/api/targets/${t1Id}/history?from=${from}&to=${to}`);
const body = (await response.json()) as HistoryResponse;
const from = "2025-06-01T00:00:00.000Z";
const to = "2025-06-01T23:59:59.999Z";
const response = await fetch(`${baseUrl}/api/targets/${t1Id}/history?from=${from}&to=${to}`);
const body = (await response.json()) as HistoryResponse;
expect(response.status).toBe(200);
expect(body.items).toHaveLength(1);
expect(body.items[0]!.failure).toBeNull();
} finally {
console.warn = originalWarn;
}
expect(response.status).toBe(200);
expect(body.items).toHaveLength(1);
expect(body.items[0]!.failure).toBeNull();
});
});

View File

@@ -7,6 +7,7 @@ import type { ProbeStore } from "../../src/server/checker/store";
import type { ResolvedTargetBase } from "../../src/server/checker/types";
import { bootstrap, type BootstrapDependencies } from "../../src/server/bootstrap";
import { createNoopLogger } from "../../src/server/logger";
type ShutdownSignal = "SIGINT" | "SIGTERM";
@@ -27,6 +28,15 @@ function createHarness(overrides: BootstrapDependencies = {}) {
configDir: "/tmp",
dataDir: "/tmp/dial-data",
host: "127.0.0.1",
logging: {
consoleLevel: "info",
fileLevel: "info",
filePath: "/tmp/dial-data/logs/dial.log",
rotationFrequency: "daily",
rotationMaxFiles: 14,
rotationSizeBytes: 52428800,
rotationSizeRaw: "50MB",
},
maxConcurrentChecks: 3,
port: 3000,
retentionMs: 1000,
@@ -49,12 +59,19 @@ function createHarness(overrides: BootstrapDependencies = {}) {
},
} as unknown as ProbeEngine;
const noopLogger = createNoopLogger();
const dependencies: BootstrapDependencies = {
createEngine(actualStore, targets, maxConcurrentChecks, retentionMs) {
createEngine(actualStore, targets, maxConcurrentChecks, retentionMs, logger) {
expect(actualStore).toBe(store);
expect(logger).toBe(noopLogger);
calls.push(`createEngine:${targets.length}:${maxConcurrentChecks}:${retentionMs}`);
return engine;
},
createLogger() {
calls.push("createLogger");
return Promise.resolve(noopLogger);
},
createStore(dbPath) {
calls.push(`createStore:${dbPath}`);
return store;
@@ -83,7 +100,7 @@ function createHarness(overrides: BootstrapDependencies = {}) {
...overrides,
};
return { calls, dependencies, shutdownHandlers };
return { calls, dependencies, noopLogger, shutdownHandlers };
}
describe("bootstrap", () => {
@@ -94,6 +111,7 @@ describe("bootstrap", () => {
expect(calls).toEqual([
"loadConfig:/tmp/probes.yaml",
"createLogger",
`createStore:${join("/tmp/dial-data", "probe.db")}`,
"syncTargets:1",
"createEngine:1:3:1000",

View File

@@ -2086,4 +2086,259 @@ targets:
"expect.status 是未知字段",
);
});
describe("logging 配置", () => {
test("logging 全部缺省时使用默认值", async () => {
const configPath = join(tempDir, "logging-default.yaml");
await writeFile(
configPath,
`targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
);
const config = await loadConfig(configPath);
expect(config.logging.consoleLevel).toBe("info");
expect(config.logging.fileLevel).toBe("info");
expect(config.logging.filePath).toBe(join(config.dataDir, "logs/dial.log"));
expect(config.logging.rotationFrequency).toBe("daily");
expect(config.logging.rotationMaxFiles).toBe(14);
expect(config.logging.rotationSizeRaw).toBe("50MB");
expect(config.logging.rotationSizeBytes).toBe(52428800);
});
test("logging.level 设置全局等级继承到 console 和 file", async () => {
const configPath = join(tempDir, "logging-global-level.yaml");
await writeFile(
configPath,
`logging:
level: "debug"
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
);
const config = await loadConfig(configPath);
expect(config.logging.consoleLevel).toBe("debug");
expect(config.logging.fileLevel).toBe("debug");
});
test("logging.console.level 覆盖全局等级", async () => {
const configPath = join(tempDir, "logging-console-level.yaml");
await writeFile(
configPath,
`logging:
level: "warn"
console:
level: "trace"
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
);
const config = await loadConfig(configPath);
expect(config.logging.consoleLevel).toBe("trace");
expect(config.logging.fileLevel).toBe("warn");
});
test("logging.file.level 独立覆盖", async () => {
const configPath = join(tempDir, "logging-file-level.yaml");
await writeFile(
configPath,
`logging:
level: "info"
file:
level: "error"
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
);
const config = await loadConfig(configPath);
expect(config.logging.consoleLevel).toBe("info");
expect(config.logging.fileLevel).toBe("error");
});
test("logging.file.path 绝对路径保持不变", async () => {
const configPath = join(tempDir, "logging-abs-path.yaml");
await writeFile(
configPath,
`logging:
file:
path: "/var/log/dial/app.log"
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
);
const config = await loadConfig(configPath);
expect(config.logging.filePath).toBe("/var/log/dial/app.log");
});
test("logging.file.path 相对路径基于配置文件目录解析", async () => {
const configPath = join(tempDir, "logging-rel-path.yaml");
await writeFile(
configPath,
`logging:
file:
path: "custom-logs/app.log"
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
);
const config = await loadConfig(configPath);
expect(config.logging.filePath).toBe(join(tempDir, "custom-logs/app.log"));
});
test("logging.file.rotation 自定义参数", async () => {
const configPath = join(tempDir, "logging-rotation.yaml");
await writeFile(
configPath,
`logging:
file:
rotation:
size: "100MB"
frequency: "hourly"
maxFiles: 30
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
);
const config = await loadConfig(configPath);
expect(config.logging.rotationSizeRaw).toBe("100MB");
expect(config.logging.rotationSizeBytes).toBe(104857600);
expect(config.logging.rotationFrequency).toBe("hourly");
expect(config.logging.rotationMaxFiles).toBe(30);
});
test("logging.level 非法等级抛出错误", async () => {
await expectConfigError(
"logging-bad-level.yaml",
`logging:
level: "verbose"
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
"logging.level",
);
});
test("logging.console.level 非法等级抛出错误", async () => {
await expectConfigError(
"logging-bad-console-level.yaml",
`logging:
console:
level: "nope"
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
"logging.console.level",
);
});
test("logging.file.level 非法等级抛出错误", async () => {
await expectConfigError(
"logging-bad-file-level.yaml",
`logging:
file:
level: 123
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
"logging.file.level",
);
});
test("logging.file.rotation.size 非法格式抛出错误", async () => {
await expectConfigError(
"logging-bad-rotation-size.yaml",
`logging:
file:
rotation:
size: "100TB"
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
"无效的 size 格式",
);
});
test("logging.file.rotation.frequency 非法值抛出错误", async () => {
await expectConfigError(
"logging-bad-frequency.yaml",
`logging:
file:
rotation:
frequency: "monthly"
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
"logging.file.rotation.frequency",
);
});
test("logging.file.rotation.maxFiles 非整数抛出错误", async () => {
await expectConfigError(
"logging-bad-maxfiles.yaml",
`logging:
file:
rotation:
maxFiles: 3.5
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
"logging.file.rotation.maxFiles",
);
});
test("logging.file.path 空字符串抛出错误", async () => {
await expectConfigError(
"logging-empty-path.yaml",
`logging:
file:
path: ""
targets:
- id: "t"
type: http
http:
url: "http://example.com"
`,
"logging.file.path",
);
});
});
});

View File

@@ -9,6 +9,7 @@ import { ProbeEngine } from "../../../src/server/checker/engine";
import { checkerRegistry } from "../../../src/server/checker/runner";
import { CommandChecker } from "../../../src/server/checker/runner/cmd/execute";
import { HttpChecker } from "../../../src/server/checker/runner/http/execute";
import { createMemoryLogger } from "../../../src/server/logger";
const processEnv = Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
@@ -20,6 +21,9 @@ function createMockStore(targetNames: string[]) {
return {
_results: results,
getLatestChecksMap() {
return new Map();
},
getTargets() {
return targets.map(({ id, name }) => ({
config: "",
@@ -165,38 +169,32 @@ describe("ProbeEngine", () => {
return originalExecute(target, ctx);
};
const originalWarn = console.warn;
console.warn = () => undefined;
try {
const rejectTarget = makeCommandTarget("reject-cmd");
const goodTarget = makeCommandTarget("good-cmd");
const mockStore = createMockStore(["reject-cmd", "good-cmd"]) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [rejectTarget, goodTarget]);
const rejectTarget = makeCommandTarget("reject-cmd");
const goodTarget = makeCommandTarget("good-cmd");
const mockStore = createMockStore(["reject-cmd", "good-cmd"]) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [rejectTarget, goodTarget]);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([rejectTarget, goodTarget]);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([rejectTarget, goodTarget]);
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
expect(results.length).toBe(2);
expect(results[0]!["targetId"]).toBe("reject-cmd");
expect(results[0]!["matched"]).toBe(false);
expect(results[0]!["durationMs"]).toBeNull();
expect(results[0]!["observation"]).toBeNull();
expect(results[0]!["failure"]).toEqual({
kind: "error",
message: "boom",
path: "engine",
phase: "internal",
});
expect(typeof results[0]!["timestamp"]).toBe("string");
expect(results[1]!["targetId"]).toBe("good-cmd");
expect(results[1]!["matched"]).toBe(true);
} finally {
console.warn = originalWarn;
checker.execute = originalExecute;
}
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
expect(results.length).toBe(2);
expect(results[0]!["targetId"]).toBe("reject-cmd");
expect(results[0]!["matched"]).toBe(false);
expect(results[0]!["durationMs"]).toBeNull();
expect(results[0]!["observation"]).toBeNull();
expect(results[0]!["failure"]).toEqual({
kind: "error",
message: "boom",
path: "engine",
phase: "internal",
});
expect(typeof results[0]!["timestamp"]).toBe("string");
expect(results[1]!["targetId"]).toBe("good-cmd");
expect(results[1]!["matched"]).toBe(true);
checker.execute = originalExecute;
});
test("并发限制 maxConcurrentChecks", async () => {
@@ -347,4 +345,193 @@ describe("ProbeEngine", () => {
expect(pruneCalled).toBe(false);
engine.stop();
});
describe("日志与状态变化", () => {
test("checker rejected 时 logger 记录 error", async () => {
ensureRegistered();
const checker = checkerRegistry.get("cmd");
const originalExecute = checker.execute.bind(checker);
checker.execute = async (target, ctx) => {
if (target.id === "fail-target") throw new Error("explode");
return originalExecute(target, ctx);
};
const logger = createMemoryLogger();
const mockStore = createMockStore(["fail-target", "ok-target"]) as unknown as ProbeStore;
const engine = new ProbeEngine(
mockStore,
[makeCommandTarget("fail-target"), makeCommandTarget("ok-target")],
20,
0,
logger,
);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget("fail-target"), makeCommandTarget("ok-target")]);
const errorLogs = logger.entries.filter((e) => e.level === "error");
expect(errorLogs.length).toBeGreaterThanOrEqual(1);
expect(errorLogs[0]!.msg).toContain("探针执行失败");
checker.execute = originalExecute;
});
test("状态变化 UP→DOWN 记录 warn 日志", async () => {
ensureRegistered();
const logger = createMemoryLogger();
const targetId = "state-test";
const mockStore = {
...createMockStore([targetId]),
getLatestChecksMap() {
return new Map([[targetId, { matched: 1 }]]);
},
} as unknown as ProbeStore;
const checker = checkerRegistry.get("cmd");
const originalExecute = checker.execute.bind(checker);
checker.execute = async (target) => {
if (target.id === targetId) {
return {
detail: null,
durationMs: 10,
failure: { kind: "error", message: "fail", path: "exitCode", phase: "body" },
matched: false,
observation: null,
targetId,
timestamp: new Date().toISOString(),
};
}
return originalExecute(target, { signal: new AbortController().signal });
};
const engine = new ProbeEngine(mockStore, [makeCommandTarget(targetId)], 20, 0, logger);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget(targetId)]);
const stateLogs = logger.entries.filter((e) => e.level === "warn" && e.msg.includes("UP → DOWN"));
expect(stateLogs.length).toBe(1);
expect(stateLogs[0]!.msg).toContain(targetId);
checker.execute = originalExecute;
});
test("状态变化 DOWN→UP 记录 info 日志", async () => {
ensureRegistered();
const logger = createMemoryLogger();
const targetId = "recover-test";
const mockStore = {
...createMockStore([targetId]),
getLatestChecksMap() {
return new Map([[targetId, { matched: 0 }]]);
},
} as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [makeCommandTarget(targetId)], 20, 0, logger);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget(targetId)]);
const recoverLogs = logger.entries.filter((e) => e.level === "info" && e.msg.includes("DOWN → UP"));
expect(recoverLogs.length).toBe(1);
expect(recoverLogs[0]!.msg).toContain(targetId);
});
test("稳态 UP→UP 不产生状态变化日志", async () => {
ensureRegistered();
const logger = createMemoryLogger();
const targetId = "steady-up";
const mockStore = {
...createMockStore([targetId]),
getLatestChecksMap() {
return new Map([[targetId, { matched: 1 }]]);
},
} as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [makeCommandTarget(targetId)], 20, 0, logger);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget(targetId)]);
const stateChangeLogs = logger.entries.filter(
(e) => e.msg.includes("UP → DOWN") || e.msg.includes("DOWN → UP") || e.msg.includes("首次 DOWN"),
);
expect(stateChangeLogs.length).toBe(0);
});
test("首次检查 DOWN 记录 warn 日志", async () => {
ensureRegistered();
const logger = createMemoryLogger();
const targetId = "first-down";
const mockStore = createMockStore([targetId]) as unknown as ProbeStore;
const checker = checkerRegistry.get("cmd");
const originalExecute = checker.execute.bind(checker);
checker.execute = async (target) => {
if (target.id === targetId) {
return {
detail: null,
durationMs: 10,
failure: { kind: "error", message: "fail", path: "exitCode", phase: "body" },
matched: false,
observation: null,
targetId,
timestamp: new Date().toISOString(),
};
}
return originalExecute(target, { signal: new AbortController().signal });
};
const engine = new ProbeEngine(mockStore, [makeCommandTarget(targetId)], 20, 0, logger);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget(targetId)]);
const firstDownLogs = logger.entries.filter((e) => e.level === "warn" && e.msg.includes("首次 DOWN"));
expect(firstDownLogs.length).toBe(1);
checker.execute = originalExecute;
});
test("每次检查产出 debug 日志", async () => {
ensureRegistered();
const logger = createMemoryLogger();
const targetId = "debug-target";
const mockStore = createMockStore([targetId]) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [makeCommandTarget(targetId)], 20, 0, logger);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget(targetId)]);
const debugLogs = logger.entries.filter((e) => e.level === "debug");
expect(debugLogs.length).toBeGreaterThanOrEqual(1);
const first = debugLogs[0]!;
expect(first.obj).toBeDefined();
expect(first.obj!["targetId"]).toBe(targetId);
});
test("无 logger 时不抛错noop logger 兜底)", async () => {
ensureRegistered();
const mockStore = createMockStore(["no-log"]) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [makeCommandTarget("no-log")]);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([makeCommandTarget("no-log")]);
});
});
});

View File

@@ -143,14 +143,8 @@ describe("mapCheckResult", () => {
});
test("损坏 observation JSON 返回 null observation", () => {
const originalWarn = console.warn;
console.warn = () => undefined;
try {
const result = mapCheckResult(makeRow({ observation: "{invalid json" }), "http");
expect(result.detail).toBeNull();
expect(result.observation).toBeNull();
} finally {
console.warn = originalWarn;
}
const result = mapCheckResult(makeRow({ observation: "{invalid json" }), "http");
expect(result.detail).toBeNull();
expect(result.observation).toBeNull();
});
});

117
tests/server/logger.test.ts Normal file
View File

@@ -0,0 +1,117 @@
import { describe, expect, test } from "bun:test";
import type { Logger } from "../../src/server/logger";
import { createConsoleFallback, createMemoryLogger, createNoopLogger, REDACT_PATHS } from "../../src/server/logger";
describe("NoopLogger", () => {
test("所有方法不抛异常", () => {
const logger = createNoopLogger();
logger.trace("trace");
logger.debug("debug");
logger.info("info");
logger.warn("warn");
logger.error("error");
logger.fatal("fatal");
logger.flush();
const child = logger.child({ component: "test" });
expect(child).toBeDefined();
});
});
describe("MemoryLogger", () => {
test("记录所有等级日志", () => {
const logger = createMemoryLogger();
logger.trace("trace-msg");
logger.debug("debug-msg");
logger.info("info-msg");
logger.warn("warn-msg");
logger.error("error-msg");
logger.fatal("fatal-msg");
expect(logger.entries).toHaveLength(6);
expect(logger.entries[0]).toEqual({ level: "trace", msg: "trace-msg" });
expect(logger.entries[5]).toEqual({ level: "fatal", msg: "fatal-msg" });
});
test("记录结构化日志", () => {
const logger = createMemoryLogger();
logger.info({ matched: true, targetId: "abc" }, "check complete");
expect(logger.entries).toHaveLength(1);
expect(logger.entries[0]!.level).toBe("info");
expect(logger.entries[0]!.msg).toBe("check complete");
expect(logger.entries[0]!.obj).toEqual({ matched: true, targetId: "abc" });
});
test("child 返回自身", () => {
const logger = createMemoryLogger();
const child = logger.child({ component: "engine" });
child.info("child-msg");
expect(logger.entries).toHaveLength(1);
});
test("flush 不抛异常", () => {
const logger = createMemoryLogger();
logger.flush();
});
});
describe("ConsoleFallbackLogger", () => {
test("不抛异常", () => {
const logger = createConsoleFallback();
logger.trace("trace");
logger.debug("debug");
logger.info("info");
logger.warn("warn");
logger.error("error");
logger.fatal("fatal");
logger.flush();
const child = logger.child({ component: "test" });
expect(child).toBeDefined();
});
});
describe("Logger 接口契约", () => {
function assertLogger(logger: Logger): void {
logger.trace("trace");
logger.debug("debug");
logger.info("info");
logger.warn("warn");
logger.error("error");
logger.fatal("fatal");
logger.info({ key: "value" }, "structured");
logger.child({ component: "test" }).info("child");
logger.flush();
}
test("NoopLogger 满足 Logger 接口", () => {
expect(() => assertLogger(createNoopLogger())).not.toThrow();
});
test("MemoryLogger 满足 Logger 接口", () => {
expect(() => assertLogger(createMemoryLogger())).not.toThrow();
});
test("ConsoleFallbackLogger 满足 Logger 接口", () => {
expect(() => assertLogger(createConsoleFallback())).not.toThrow();
});
});
describe("redaction 敏感信息保护", () => {
test("MemoryLogger 不做 redaction测试用途仅 Pino 运行时 redact", () => {
const logger = createMemoryLogger();
logger.info({ authorization: "Bearer secret", password: "hunter2" }, "test");
const entry = logger.entries[0]!;
expect(entry.obj!["authorization"]).toBe("Bearer secret");
expect(entry.obj!["password"]).toBe("hunter2");
});
test("REDACT_PATHS 覆盖所有敏感字段键名", () => {
const sensitiveKeys = ["authorization", "cookie", "set-cookie", "authToken", "key", "password", "token", "apiKey"];
for (const key of sensitiveKeys) {
expect(REDACT_PATHS).toContain(key);
expect(REDACT_PATHS).toContain(`*.${key}`);
}
});
});