From 31aec6fc8094fb5b45ad6337805467a061f9e471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AD=94=E7=8E=8B=E5=B0=91=E5=B9=B4?= Date: Wed, 20 Nov 2024 06:14:34 +0800 Subject: [PATCH] feat: add dynamic pino options (#59) * feat: add dynamic pino options * add unit test * Merge remote-tracking branch 'origin/main' into add-dynamic-pino-options * resolve potential missing process error * remove unnecessary tests --- src/logger.ts | 4 +- src/middleware.test.ts | 83 +++++++++++++++++++++++++++++++++++++++++- src/middleware.ts | 55 ++++++++++++++++++++++++++-- src/types.ts | 75 +++++++++++++++++++++++++++++++++++++- 4 files changed, 210 insertions(+), 7 deletions(-) diff --git a/src/logger.ts b/src/logger.ts index 2f741b2..7bb0bb2 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -98,9 +98,9 @@ export class PinoLogger { resLevel?: pino.Level | null; } = {}; - constructor(rootLogger: pino.Logger) { + constructor(rootLogger: pino.Logger, childOptions?: pino.ChildLoggerOptions) { // Use a child logger to prevent unintended behavior from changes to the provided logger - this._rootLogger = rootLogger.child({}); + this._rootLogger = rootLogger.child({}, childOptions); this._logger = rootLogger; } diff --git a/src/middleware.test.ts b/src/middleware.test.ts index 16cc450..04f8305 100644 --- a/src/middleware.test.ts +++ b/src/middleware.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach, vi } from "vitest"; import { Hono } from "hono"; import { pinoLogger } from "./middleware"; import type { Options } from "./types"; @@ -19,6 +19,7 @@ describe("middleware", () => { ); beforeEach(() => { + vi.unstubAllEnvs(); logs = []; }); @@ -161,6 +162,86 @@ describe("middleware", () => { }); }); + describe("pino option", () => { + const createApp = (opts?: Options) => { + const app = new Hono().use(pinoLogger(opts)); + app.get("/hello", async (c) => { + c.var.logger.info("hello"); + return c.text("ok"); + }); + app.get("/bindings", async (c) => c.json(c.var.logger.bindings())); + app.get("/log-level", async (c) => + c.json(c.var.logger._rootLogger.level), + ); + return app; + }; + + it("default with set env", async () => { + const app = createApp(); + + vi.stubEnv("LOG_LEVEL", "debug"); + const res = await app.request("/log-level"); + expect(res.status).toBe(200); + expect(await res.json()).toBe("debug"); + }); + + it("default with default value", async () => { + const app = createApp(); + + const res = await app.request("/log-level"); + expect(res.status).toBe(200); + expect(await res.json()).toBe("info"); + }); + + it("a pino logger", async () => { + const app = createApp({ + pino: pino({ + name: "pino", + }), + }); + + const res = await app.request("/bindings"); + expect(res.status).toBe(200); + expect(await res.json()).toStrictEqual({ name: "pino" }); + }); + + it("a pino options", async () => { + const app = createApp({ + pino: { + name: "pino", + }, + }); + + const res = await app.request("/bindings"); + expect(res.status).toBe(200); + expect(await res.json()).toStrictEqual({ name: "pino" }); + }); + + it("dynamic pino logger", async () => { + const app = createApp({ + pino: (c) => pino({ level: c.env.LOG_LEVEL }), + }); + + const res = await app.request("/log-level", undefined, { + LOG_LEVEL: "debug", + }); + expect(res.status).toBe(200); + expect(await res.json()).toBe("debug"); + }); + + it("dynamic pino child options", async () => { + const app = createApp({ + pino: (c) => ({ level: c.env.LOG_LEVEL }), + }); + + const res = await app.request("/log-level", undefined, { + LOG_LEVEL: "debug", + }); + expect(res.status).toBe(200); + expect(await res.json()).toBe("debug"); + }); + }); + describe("on request", () => { it("basic", async () => { const { logs } = await mockRequest({ diff --git a/src/middleware.ts b/src/middleware.ts index 93f8385..5d4c327 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,10 +1,11 @@ -import type { MiddlewareHandler } from "hono"; +import type { Context, MiddlewareHandler } from "hono"; import { pino } from "pino"; import { defu } from "defu"; import { isPino } from "./utils"; import type { Env, Options } from "./types"; import { httpCfgSym, PinoLogger } from "./logger"; import type { LiteralString } from "./utils"; +import { env } from "hono/adapter"; /** * hono-pino middleware @@ -12,11 +13,17 @@ import type { LiteralString } from "./utils"; export const pinoLogger = ( opts?: Options>, ): MiddlewareHandler> => { - const rootLogger = isPino(opts?.pino) ? opts.pino : pino(opts?.pino); const contextKey = opts?.contextKey ?? ("logger" as ContextKey); + let rootLogger = createStaticRootLogger(opts?.pino); return async (c, next) => { - const logger = new PinoLogger(rootLogger); + const [dynamicRootLogger, loggerChildOptions] = parseDynamicRootLogger( + opts?.pino, + c, + ); + // set rootLogger to 1.static, 2.dynamic 3.default + rootLogger ??= dynamicRootLogger ?? getDefaultRootLogger(); + const logger = new PinoLogger(rootLogger, loggerChildOptions); c.set(contextKey, logger); // disable http logger @@ -90,3 +97,45 @@ export const logger = pinoLogger; let defaultReqId = 0; const defaultReqIdGenerator = () => (defaultReqId += 1); + +/** + * create static rootLogger, + * in dynamic rootLogger mode, is null + */ +const createStaticRootLogger = (opt: Options["pino"]): pino.Logger | null => { + if (typeof opt === "function") return null; + if (isPino(opt)) return opt; + return pino(opt); +}; + +/** + * parse dynamic rootLogger + * + * @returns [dynamicRootLogger, loggerChildOptions] + */ +const parseDynamicRootLogger = ( + opt: Options["pino"], + c: Context, +): [pino.Logger | undefined, pino.ChildLoggerOptions | undefined] => { + // default + if (opt === undefined) { + const { LOG_LEVEL } = env<{ LOG_LEVEL?: string }>(c); + return [ + undefined, + { + level: LOG_LEVEL ?? "info", + }, + ]; + } + + if (typeof opt !== "function") return [undefined, undefined]; + const v = opt(c); + if (isPino(v)) return [v, undefined]; + return [undefined, v]; +}; + +/** + * get default rootLogger (lazy initialization) + */ +const getDefaultRootLogger = (): pino.Logger => (_defaultRootLogger ??= pino()); +let _defaultRootLogger: pino.Logger | undefined = undefined; diff --git a/src/types.ts b/src/types.ts index 523ac23..2c785d3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,8 +52,81 @@ export interface Options { /** * a pino instance or pino options + * + * @example + * + * ### default + * + * ```ts + * { + * pino: (c) => ({ + * level: env(c).LOG_LEVEL ?? "info", + * }) + * } + * ``` + * + * @example + * + * ### a pino logger instance + * + * ```ts + * { + * pino: pino({ level: "info" }) + * } + * ``` + * + * @example + * + * ### a pino options + * + * ```ts + * { + * pino: { level: "info" } + * } + * ``` + * + * @example + * + * ### a pino destination + * + * ```ts + * { + * pino: pino.destination("path/to/log.json") + * } + * ``` + * + * @example + * + * ### dynamic pino logger instance + * + * this method creates a complete pino logger for each request, + * which results in relatively lower performance, + * if possible, recommended to use `dynamic pino child options`. + * + * ```ts + * { + * pino: (c) => pino({ level: c.env.LOG_LEVEL }) + * } + * ``` + * + * @example + * + * ### dynamic pino child options + * + * ```ts + * { + * pino: (c) => ({ + * level: c.env.LOG_LEVEL + * } satisfies pino.ChildLoggerOptions) + * } + * ``` */ - pino?: pino.Logger | pino.LoggerOptions | pino.DestinationStream; + pino?: + | pino.Logger + | pino.LoggerOptions + | pino.DestinationStream + | ((c: Context) => pino.Logger) + | ((c: Context) => pino.ChildLoggerOptions); /** * http request log options