Nuxt3 ベストプラクティス01: ログ出力
1. はじめに
Nuxt 3 では、デフォルトで Nuxt Kit の useLogger を使用してログを出力します。基本的な機能は十分ですが、各 HTTP リクエストごとに一意のリクエスト ID を生成する機能がないため、ログの追跡が難しくなります。
本記事では、各 HTTP リクエストごとに一意の ID を生成し、ログ分析やデバッグをより効率的に行う方法を紹介します。
2. 実装方法
2.1 pino のインストール
ログ管理には pino を使用します。以下のコマンドでインストールしてください。
npm install pino pino-http pino-pretty
2.2 一意のリクエスト ID の生成
まず、各 HTTP リクエストごとに一意の ID を生成します。server/middleware/ ディレクトリに 01.tracing-id.ts を作成します。ファイル名の 01 は、このミドルウェアが最初に実行されるようにするためです。
注意: Nuxt 3 では、ミドルウェアの実行順序はファイル名のアルファベット/数字の順に決まります。
// server/middleware/01.tracing-id.ts
export default fromNodeMiddleware((req, _res, next) => {
const tracingId = req.headers['x-tracing-id'] || Math.random().toString(36).slice(2, 12);
req.headers['x-tracing-id'] = tracingId;
next();
});
ここでは Math.random() を使用して長さ10のランダムな文字列を生成し、それをリクエストヘッダーに保存します。
2.3 pino の設定
次に、server/middleware/ ディレクトリに 02.log-register.ts を作成し、pino-http ログミドルウェアを登録します。
// server/middleware/02.log-register.ts
import logger from 'pino-http';
export default fromNodeMiddleware(
logger({
level: 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: process.env.NODE_ENV !== 'production',
translateTime: 'SYS:standard',
ignore: 'req,res,responseTime,pid,hostname',
messageFormat: '[reqId:{req.id}] - {msg}',
},
},
genReqId: (req) => req.headers['x-tracing-id'] || req.id,
autoLogging: false,
}),
);
注意:
fromNodeMiddlewareを使用し、pino-httpを適用することでreqオブジェクトにlog(pino.Loggerインスタンス)を注入できます。
この設定により、ログ出力時に reqId が含まれるようになり、ログの関連付けが容易になります。
2.4 API でのログ利用
上記の設定を行うことで、API ハンドラー内で pino を使用してログを記録できます。
// server/api/test.ts
export default defineEventHandler((event) => {
const logger = event.node.req.log;
logger.info('Hello, world!');
return { message: 'Hello, world!' };
});
API にアクセスすると、次のようなログが出力されます。
[2025-01-31 09:52:11.217 +0900] INFO: [reqId:b0v9ldkiv1] - Hello, world!
3. カスタムログユーティリティの作成
pino によりログのフォーマットが整理されましたが、API コード内での logger の使用には以下の課題があります。
event.node.req.logからloggerを手動で取得する必要がある。- ユーティリティ関数内でログを使用する場合、
loggerを明示的に渡す必要がある。
理想的には、どこからでも logger.info('xxx'); を実行するだけでログ出力ができるようにしたいです。
この問題を解決するために、カスタムログユーティリティを作成します。
3.1 AsyncLocalStorage の活用
AsyncLocalStorage を利用し、logger をリクエストごとにスコープ化することで、どこからでも logger を取得可能にします。
まず、server/utils/ ディレクトリに async-storage.ts を作成します。
// server/utils/async-storage.ts
import { AsyncLocalStorage } from 'async_hooks';
import type pino from 'pino';
type StorageT = { logger: pino.Logger };
export const asyncLocalStorage = new AsyncLocalStorage<StorageT>();
これにより、リクエストごとに logger を保持できます。
3.2 グローバル logger の作成
次に、server/utils/ ディレクトリに logger.ts を作成します。
// server/utils/logger.ts
const getLogger = () => asyncLocalStorage.getStore()?.logger ?? console;
export const logger = {
info: (msg: string) => getLogger().info(msg),
error: (msg: string) => getLogger().error(msg),
warn: (msg: string) => getLogger().warn(msg),
debug: (msg: string) => getLogger().debug(msg),
};
これで、どこからでも logger.info('message') を呼び出すだけでログを出力できます。
3.3 defineEventHandler をラップする
server/utils/ ディレクトリに handler.ts を作成し、defineEventHandler をラップして logger をリクエストごとに適用します。
// server/utils/handler.ts
import type { EventHandler, EventHandlerRequest } from 'h3';
export const defineWrappedEventHandler = <T extends EventHandlerRequest, D>(
handler: EventHandler<T, D>,
): EventHandler<T, D> =>
defineEventHandler<T>(async (event) => {
const logger = event.node.req.log;
return asyncLocalStorage.run({ logger }, async () => handler(event));
});
3.4 API での利用
defineWrappedEventHandler を使用すると、API 内で logger を簡単に使用できます。
// server/api/test.ts
export default defineWrappedEventHandler(() => {
logger.info('Hello, world!');
return { message: 'Hello, world!' };
});
注意: ここで使用しているのは
defineEventHandlerではなくdefineWrappedEventHandlerです。
また、ユーティリティ関数の内部でも logger を直接使用できます。
// server/util/some-util.ts
export const someUtil = () => {
logger.info('use some util');
};
// server/api/test.ts
export default defineWrappedEventHandler(() => {
someUtil();
return { message: 'Hello, world!' };
});
このように、明示的に logger を取得せずに、どこでもログを出力できるようになります。
この方法を採用することで、ログ管理がシンプルになり、可読性やメンテナンス性が向上します。