Skip to content

Commit

Permalink
feat: add dynamic pino options (#59)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
maou-shonen authored Nov 19, 2024
1 parent 5ccf878 commit 31aec6f
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 7 deletions.
4 changes: 2 additions & 2 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
83 changes: 82 additions & 1 deletion src/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -19,6 +19,7 @@ describe("middleware", () => {
);

beforeEach(() => {
vi.unstubAllEnvs();
logs = [];
});

Expand Down Expand Up @@ -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({
Expand Down
55 changes: 52 additions & 3 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
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
*/
export const pinoLogger = <ContextKey extends string = "logger">(
opts?: Options<LiteralString<ContextKey>>,
): MiddlewareHandler<Env<ContextKey>> => {
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
Expand Down Expand Up @@ -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;
75 changes: 74 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,81 @@ export interface Options<ContextKey extends string = "logger"> {

/**
* 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
Expand Down

0 comments on commit 31aec6f

Please sign in to comment.