feat(logging): add pino logger with createModuleLogger
This commit is contained in:
87
src/shared/lib/logger.test.ts
Normal file
87
src/shared/lib/logger.test.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest"
|
||||||
|
import { pino, type Logger } from "pino"
|
||||||
|
import { Writable } from "node:stream"
|
||||||
|
|
||||||
|
// mock @/env.mjs:避免 jsdom 环境下 @t3-oss/env 拦截 server-side env 访问。
|
||||||
|
// logger.ts 模块顶层访问 env.LOG_LEVEL / env.NODE_ENV,必须先 mock 才能导入。
|
||||||
|
vi.mock("@/env.mjs", () => ({
|
||||||
|
env: {
|
||||||
|
LOG_LEVEL: "info",
|
||||||
|
NODE_ENV: "test",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { createModuleLogger } from "./logger"
|
||||||
|
import { getRequestContext, requestContextStorage } from "./request-context"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建独立 pino 实例(无 transport、同步写入内存流),
|
||||||
|
* 用于验证 level 行为,避免 transport 异步写入导致 stdout 捕获不稳定。
|
||||||
|
*/
|
||||||
|
function createMemoryLogger(): { logInstance: Logger; lines: string[] } {
|
||||||
|
const lines: string[] = []
|
||||||
|
const stream = new Writable({
|
||||||
|
write(chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
|
||||||
|
if (chunk instanceof Uint8Array) {
|
||||||
|
lines.push(Buffer.from(chunk).toString())
|
||||||
|
} else if (typeof chunk === "string") {
|
||||||
|
lines.push(chunk)
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const logInstance = pino(
|
||||||
|
{
|
||||||
|
level: "info",
|
||||||
|
base: { service: "cicd-app" },
|
||||||
|
formatters: {
|
||||||
|
level: (label: string): { level: string } => ({ level: label }),
|
||||||
|
},
|
||||||
|
mixin: (): Record<string, unknown> => getRequestContext(),
|
||||||
|
},
|
||||||
|
stream,
|
||||||
|
)
|
||||||
|
return { logInstance, lines }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("logger", () => {
|
||||||
|
describe("createModuleLogger", () => {
|
||||||
|
it("返回的 child logger 包含 module 字段", () => {
|
||||||
|
const moduleLogger = createModuleLogger("audit")
|
||||||
|
// bindings 继承父 logger 的 base(service),故用 toMatchObject 检查 module 字段
|
||||||
|
expect(moduleLogger.bindings()).toMatchObject({ module: "audit" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("不同 module 返回不同 child logger", () => {
|
||||||
|
const a = createModuleLogger("a")
|
||||||
|
const b = createModuleLogger("b")
|
||||||
|
expect(a).not.toBe(b)
|
||||||
|
expect(a.bindings()).toMatchObject({ module: "a" })
|
||||||
|
expect(b.bindings()).toMatchObject({ module: "b" })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("mixin 注入 requestId", () => {
|
||||||
|
it("在 requestContextStorage.run 内 mixin 返回 requestId", () => {
|
||||||
|
// logger.ts 的 mixin 调用 getRequestContext(),直接验证其返回值
|
||||||
|
const ctx = requestContextStorage.run({ requestId: "req-mixin-test" }, () => {
|
||||||
|
return getRequestContext()
|
||||||
|
})
|
||||||
|
expect(ctx).toEqual({ requestId: "req-mixin-test" })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("在 requestContextStorage.run 外 mixin 返回空对象", () => {
|
||||||
|
const ctx = getRequestContext()
|
||||||
|
expect(ctx).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("日志级别", () => {
|
||||||
|
it("默认级别 info 下 debug 不输出", () => {
|
||||||
|
const { logInstance, lines } = createMemoryLogger()
|
||||||
|
logInstance.debug("debug message")
|
||||||
|
const matched = lines.find((l) => l.includes("debug message"))
|
||||||
|
expect(matched).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
42
src/shared/lib/logger.ts
Normal file
42
src/shared/lib/logger.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import pino, { type Logger } from "pino"
|
||||||
|
|
||||||
|
import { env } from "@/env.mjs"
|
||||||
|
import { getRequestContext } from "./request-context"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局 logger 实例。
|
||||||
|
*
|
||||||
|
* - 生产环境:JSON 输出到 stdout(docker logs 友好)
|
||||||
|
* - 开发环境:pino-pretty 彩色文本
|
||||||
|
* - 自动从 AsyncLocalStorage 混入 requestId / userId(若存在)
|
||||||
|
*/
|
||||||
|
export const logger = pino({
|
||||||
|
level: env.LOG_LEVEL,
|
||||||
|
base: { service: "cicd-app" },
|
||||||
|
formatters: {
|
||||||
|
level: (label) => ({ level: label }),
|
||||||
|
},
|
||||||
|
mixin: () => getRequestContext(),
|
||||||
|
...(env.NODE_ENV === "development" && {
|
||||||
|
transport: {
|
||||||
|
target: "pino-pretty",
|
||||||
|
options: { colorize: true, translateTime: "SYS:standard" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建模块级子 logger,自动绑定 module 字段。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const log = createModuleLogger("audit")
|
||||||
|
* log.info({ userId }, "User action logged")
|
||||||
|
* // 输出: {"level":"info","module":"audit","msg":"User action logged", ...}
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createModuleLogger(module: string): Logger {
|
||||||
|
return logger.child({ module })
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Logger }
|
||||||
Reference in New Issue
Block a user