Next.js アプリケーションの可観測性を向上させる Pino + OpenTelemetry の実装
この記事では、Next.js アプリケーションで構造化ログを実装する方法を学んだので記録として残します。
- Pino と OpenTelemetry の組み合わせによる構造化ログ実装
- trace_id 付きログの出力方法
https://github.com/kntks/blog-code/tree/main/2025/07/nextjs-structured-logging-with-pino-otel
なぜ構造化ログが必要か
Section titled “なぜ構造化ログが必要か”仕事で参画したプロジェクトにおいて、Next.js を使ったアプリケーションのログ設計について検討する機会がありました。従来のコンソールログでは以下の課題があります:
- エラーの原因特定が困難
- 処理の流れが追跡しにくい
- 監視・運用効率が低い
本記事のアプローチ
Section titled “本記事のアプローチ”この記事では、Pino と OpenTelemetry を組み合わせて trace_id
が付与されたログをコンソールに出力します。公式では @vercel/otel
を使用していますが、コンソール出力が目的のため、手動で OpenTelemetry の計装を行います。
ログ設計の基本方針
Section titled “ログ設計の基本方針”最重要目的:エラー監視・障害対応
Section titled “最重要目的:エラー監視・障害対応”ログ出力の最大の目的は、エラー監視・障害対応です。具体的には以下を実現します:
- アプリケーションの異常を早期発見
- 問題の根本原因を迅速に特定
- 処理の流れを追跡可能にする
なぜコンテキスト情報が必要か
Section titled “なぜコンテキスト情報が必要か”経験上、エラーのログだけでは原因特定が困難なケースが多くあります。さらに以下の問題もあります:
- 処理自体は成功しているが、要件の考慮漏れによる問題
- 複数のリクエストが並行処理される際の混在
ログにコンテキスト情報(trace_id
)を含めることで、問題の特定を容易にします。
使用バージョン
Section titled “使用バージョン”パッケージ | バージョン |
---|---|
Mac | Sequoia 15.5 |
Node.js | 24.1.0 |
pnpm | 10.11.1 |
Next.js | 15.3.3 |
pino | 9.7.0 |
1. プロジェクト作成
Section titled “1. プロジェクト作成”create-next-app を使ってプロジェクトを作成します:
pnpm dlx create-next-app nextjs-keycloak-login --ts --app --turbopack --import-alias "@/*" --tailwind --src-dir --no-eslint --use-pnpm
不要な画像データを削除します:
rm -f src/app/favicon.ico public/*
2. 必要パッケージのインストール
Section titled “2. 必要パッケージのインストール”Pino 関連パッケージ
Section titled “Pino 関連パッケージ”pnpm add pino pino-pretty
OpenTelemetry 関連パッケージ
Section titled “OpenTelemetry 関連パッケージ”Next.js のドキュメントでは @vercel/otel
が推奨されていますが、今回は OpenTelemetry の公式パッケージを使用します:
pnpm add @opentelemetry/sdk-node @opentelemetry/semantic-conventions @opentelemetry/sdk-trace-node @opentelemetry/resources @opentelemetry/instrumentation-pino
shadcn/ui のインストール(オプション)
Section titled “shadcn/ui のインストール(オプション)”簡単なコンポーネントを作成するために shadcn/ui を使用します:
pnpm dlx shadcn@latest initpnpm dlx shadcn@latest add button card budge input avatar separator
参考:https://ui.shadcn.com/docs/installation/next
3. OpenTelemetry の設定と初期化
Section titled “3. OpenTelemetry の設定と初期化”サーバーサイド計装の実装
Section titled “サーバーサイド計装の実装”手順 1:計装ファイルの作成
src/instrumentation.ts
ファイルを作成し、OpenTelemetry の計装を登録します:
export async function register() { if (process.env.NEXT_RUNTIME === 'nodejs') { await import('./instrumentation.node') }}
The instrumentation file should be in the root of your project and not inside the app or pages directory. If you’re using the src folder, then place the file inside src alongside pages and app.
instrumentation file はプロジェクトのルートに置くべきで、app や pages ディレクトリの中には置かないでください。src フォルダを使用している場合は、src の中に pages と app と一緒にファイルを置いてください。
引用:https://nextjs.org/docs/app/guides/open-telemetry#using-vercelotel
手順 2:Node.js 環境での計装設定
@opentelemetry/instrumentation-pino
を使用して、Pino ロガーが自動的にトレース情報をログに注入します:
import { NodeSDK } from '@opentelemetry/sdk-node';import { SimpleSpanProcessor, ConsoleSpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-node';import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'import {resourceFromAttributes} from '@opentelemetry/resources'import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino';
class FilterSpanProcessor extends SimpleSpanProcessor { override onEnd(span: ReadableSpan): void { if (span.instrumentationScope.name === 'next.js') { return } super.onEnd(span); }}const sdk = new NodeSDK({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'nextjs-structured-logging-with-pino-otel' }), spanProcessors: [ new FilterSpanProcessor(new ConsoleSpanExporter()) ], instrumentations: [new PinoInstrumentation()]});
sdk.start()
Next.js 固有の設定
Section titled “Next.js 固有の設定”重要:serverExternalPackages の設定
next.config.js
で以下の設定を行います。この設定により、サーバーサイドで使用する外部パッケージを明示的に指定し、バンドルから除外します:
import type { NextConfig } from "next";
const nextConfig: NextConfig = { serverExternalPackages: [ '@opentelemetry/sdk-node', 'pino', ]};
export default nextConfig;
実装により得られた効果
Section titled “実装により得られた効果”pnpm dev
でアプリケーションを起動し、http://localhost:3000
にアクセスすると、以下のようなログが出力されます
{"level":"info","time":"2025-07-05T13:47:28.482Z","trace_id":"c40fd8ea4d9ea0e5dff908acc58558af","span_id":"110fcd1d61c71ce2","trace_flags":"01","msg":"Starting server-side rendering with trace context"}{"level":"info","time":"2025-07-05T13:47:28.691Z","trace_id":"c40fd8ea4d9ea0e5dff908acc58558af","span_id":"110fcd1d61c71ce2","trace_flags":"01","msg":"Server-side async operation finished"}
以下のように curl を実行すると、サーバーサイドでの非同期処理が実行され、ログに trace_id
が含まれます
$ curl -X POST "http://localhost:3000/api/async-task" \ -H "Content-Type: application/json" \ -d '{ "tasks": [ {"name": "タスク1", "duration": 1000}, {"name": "タスク2", "duration": 1500}, {"name": "タスク3", "duration": 800} ], "parallel": false }'
✓ Compiled /api/async-task in 490ms{"level":"info","time":"2025-07-05T13:56:28.496Z","trace_id":"f73b97c1ca1b031a389ba20b902a0cc5","span_id":"b359f0b04d6ce671","trace_flags":"01","msg":"非同期タスク実行開始"}{"level":"info","time":"2025-07-05T13:56:28.496Z","trace_id":"f73b97c1ca1b031a389ba20b902a0cc5","span_id":"b359f0b04d6ce671","trace_flags":"01","msg":"順次実行モードで開始"}{"level":"info","time":"2025-07-05T13:56:28.496Z","trace_id":"f73b97c1ca1b031a389ba20b902a0cc5","span_id":"b359f0b04d6ce671","trace_flags":"01","msg":"タスク1 開始"}{"level":"info","time":"2025-07-05T13:56:29.497Z","trace_id":"f73b97c1ca1b031a389ba20b902a0cc5","span_id":"b359f0b04d6ce671","trace_flags":"01","msg":"タスク1 完了"}{"level":"info","time":"2025-07-05T13:56:29.497Z","trace_id":"f73b97c1ca1b031a389ba20b902a0cc5","span_id":"b359f0b04d6ce671","trace_flags":"01","msg":"タスク2 開始"}{"level":"info","time":"2025-07-05T13:56:30.999Z","trace_id":"f73b97c1ca1b031a389ba20b902a0cc5","span_id":"b359f0b04d6ce671","trace_flags":"01","msg":"タスク2 完了"}{"level":"info","time":"2025-07-05T13:56:31.000Z","trace_id":"f73b97c1ca1b031a389ba20b902a0cc5","span_id":"b359f0b04d6ce671","trace_flags":"01","msg":"タスク3 開始"}{"level":"info","time":"2025-07-05T13:56:31.801Z","trace_id":"f73b97c1ca1b031a389ba20b902a0cc5","span_id":"b359f0b04d6ce671","trace_flags":"01","msg":"タスク3 完了"} POST /api/async-task 200 in 3845ms{"level":"info","time":"2025-07-05T13:56:31.801Z","trace_id":"f73b97c1ca1b031a389ba20b902a0cc5","span_id":"b359f0b04d6ce671","trace_flags":"01","msg":"非同期タスク実行完了"}
Next.js アプリケーションで Pino と OpenTelemetry を組み合わせた構造化ログの実装により、以下を実現できました:
- ログに
trace_id
を自動付与 - アプリケーションの問題発生時にログから関連リクエストを追跡可能
構造化ログと分散トレーシングの組み合わせは、アプリケーションの可観測性を大幅に向上させる強力な手法です。今回はただ軽装だけして、trace_id
が付与されるところしかしませんでした。
今後は以下の取り組みにチャレンジしたいと考えています。
- メトリクス収集との連携
- アラート設定の自動化
- より包括的な監視システムの構築