Next.jsにてpinoに入門し、ログ設計を見直したいがしかし

2025/02/10に公開

はじめに

ログ設計について何もわからない。

https://github.com/pinojs/pino
pinoを利用する。

"pino": "^9.2.0",

個人開発で利用しているpinoのログ設定ファイルは以下なのだが、やっつけ感が否めない。

logger.ts
import pino from "pino";

/**
 * error: アプリケーションのクラッシュや致命的なエラーが発生した場合のログ出力に利用されます。 \
 * warn: アプリケーションのクラッシュや致命的なエラーが発生した場合のログ出力に利用されます。 \
 * info: 一般的な動作ログやAPIリクエストの正常な処理についてのログ出力に利用されます。 \
 * debug: 開発時のみ役立つ細かい情報についてのログ出力に利用されます。
 */
type LogLevel = "error" | "warn" | "info" | "debug";

type ErrorOptions = {
  /**
   *エラーオブジェクト(スタックトレース含む、エラー時のみ)
   */
  error: Error;
};

type DebugOptions = {
  /**
   *デバッグ用の追加データ(配列やオブジェクト)
   */
  debugData?: unknown;
  /**
   *リクエストヘッダ(デバッグ用)
   */
  requestHeaders?: Record<string, string>;
  /**
   * リクエストボディ(デバッグ用)
   */
  requestBody?: Record<string, unknown>;
  /**
   * レスポンスボディ(デバッグ用)
   */
  responseBody?: Record<string, unknown>;
};

type LogOptions = {
  /**
   *ログメッセージ
   */
  message: string;
  /**
   *呼び出し元の関数名+ファイル名
   */
  caller: string;
  /**
   *同一リクエストの識別ID
   */
  requestId?: string;
  /**
   *ユーザー情報(オプション)
   */
  user?: {
    /**
     *ユーザーのIPアドレス(マスク済み)
     */
    ip: string;
    /**
     *認証済みのユーザーID(オプション)
     */
    userId?: string;
  };
  /**
   *リクエスト情報(オプション)
   */
  route?: {
    /**
     *リクエストパス
     */
    path: string;
    /**
     *HTTPメソッド
     */
    method: string;
  };
  /**
   *タイミング情報(オプション)
   */
  timing?: {
    /**
     *処理時間(ミリ秒)
     */
    durationMs?: number;
  };
  /**
   *HTTPステータスコード(オプション)
   */
  status?: number;
  /**
   *レスポンス情報(オプション)
   */
  response?: {
    /**
     *ステータスコード
     */
    statusCode: number;
    /**
     *レスポンスボディ(オプション)
     */
    body?: Record<string, unknown>;
  };
};

type LogOptionsGenerics<T extends LogLevel = "info"> =
  T extends "error"
    ? LogOptions & ErrorOptions
    : T extends "debug"
      ? LogOptions & DebugOptions
      : LogOptions;

const formatters = {
  level: (label: string) => {
    return {
      level: label.toUpperCase(),
    };
  },
};
const pinoConfig = {
  level:
    process.env.NODE_ENV === "production"
      ? "info"
      : "debug", // 環境によりログレベルを切り替え
  timestamp: pino.stdTimeFunctions.isoTime,
  browser: {
    asObject: true,
    formatters,
    write: (o: object) => {
      const typedO = o as LogOptions & {
        level: LogLevel;
      } & { time: string };
      const { level, time, ...rest } = typedO;
      const sortedO = {
        level,
        time,
        ...rest,
      };
      // eslint-disable-next-line no-console
      console.log(JSON.stringify(sortedO));
    },
  },
  formatters,
};

const logger = pino(pinoConfig);

const unpackError = (
  option: LogOptionsGenerics<"error">
) => {
  option.error = {
    name: option.error.name,
    message: option.error.message,
    stack: option.error.stack,
    cause: option.error.cause,
  };

  return option;
};

export const loggerError = (
  option: LogOptionsGenerics<"error">
) => {
  return logger.error(unpackError(option));
};

export const loggerWarn = (
  option: LogOptionsGenerics<"warn">
) => {
  return logger.warn(option);
};

export const loggerInfo = (
  option: LogOptionsGenerics<"info">
) => {
  return logger.info(option);
};

export const loggerDebug = (
  option: LogOptionsGenerics<"debug">
) => {
  return logger.debug(option);
};

以下のような形式のログが出力される。

{"level":"INFO","time":"2025-02-10T13:05:38.051Z","message":"middleware start.","caller":"middleware.ts","requestId":"5f0c8321-5c43-445e-b047-23e5ea5bb9dc","user":{"ip":"::1"},"route":{"path":"/ja-JP","method":"GET"}}

ちなみに
ブラウザにおけるformattersは
https://github.com/pinojs/pino/releases/tag/v8.19.0
から対応となったので注意。
Edge RuntimeであるMiddlewareでのフォーマットに必要。

そもそも、ログ設計とは?

https://qiita.com/tadashiro_ninomiya/items/19c774898c68add6185e

終わりに

なにもわからない。フロントエンドにおけるログ設計の調査を継続しなければ。

参考文献

https://qiita.com/tadashiro_ninomiya/items/19c774898c68add6185e
https://zenn.dev/noko_noko/articles/27a1e00f4d914e

Discussion