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