Skip to content

Next.js アプリケーションの可観測性を向上させる Pino + OpenTelemetry の実装

この記事では、Next.js アプリケーションで構造化ログを実装する方法を学んだので記録として残します。

  • Pino と OpenTelemetry の組み合わせによる構造化ログ実装
  • trace_id 付きログの出力方法

demo

https://github.com/kntks/blog-code/tree/main/2025/07/nextjs-structured-logging-with-pino-otel

仕事で参画したプロジェクトにおいて、Next.js を使ったアプリケーションのログ設計について検討する機会がありました。従来のコンソールログでは以下の課題があります:

  • エラーの原因特定が困難
  • 処理の流れが追跡しにくい
  • 監視・運用効率が低い

この記事では、Pino と OpenTelemetry を組み合わせて trace_id が付与されたログをコンソールに出力します。公式では @vercel/otel を使用していますが、コンソール出力が目的のため、手動で OpenTelemetry の計装を行います

最重要目的:エラー監視・障害対応

Section titled “最重要目的:エラー監視・障害対応”

ログ出力の最大の目的は、エラー監視・障害対応です。具体的には以下を実現します:

  • アプリケーションの異常を早期発見
  • 問題の根本原因を迅速に特定
  • 処理の流れを追跡可能にする

なぜコンテキスト情報が必要か

Section titled “なぜコンテキスト情報が必要か”

経験上、エラーのログだけでは原因特定が困難なケースが多くあります。さらに以下の問題もあります:

  • 処理自体は成功しているが、要件の考慮漏れによる問題
  • 複数のリクエストが並行処理される際の混在

ログにコンテキスト情報(trace_id)を含めることで、問題の特定を容易にします

パッケージバージョン
MacSequoia 15.5
Node.js24.1.0
pnpm10.11.1
Next.js15.3.3
pino9.7.0

create-next-app を使ってプロジェクトを作成します:

Terminal window
pnpm dlx create-next-app nextjs-keycloak-login --ts --app --turbopack --import-alias "@/*" --tailwind --src-dir --no-eslint --use-pnpm

不要な画像データを削除します:

Terminal window
rm -f src/app/favicon.ico public/*

2. 必要パッケージのインストール

Section titled “2. 必要パッケージのインストール”
Terminal window
pnpm add pino pino-pretty

Next.js のドキュメントでは @vercel/otel が推奨されていますが、今回は OpenTelemetry の公式パッケージを使用します:

Terminal window
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 を使用します:

Terminal window
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button card budge input avatar separator

参考:https://ui.shadcn.com/docs/installation/next

手順 1:計装ファイルの作成

src/instrumentation.ts ファイルを作成し、OpenTelemetry の計装を登録します:

src/instrumentation.ts
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 ロガーが自動的にトレース情報をログに注入します:

src/instrumentation.node.ts
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()

重要:serverExternalPackages の設定

next.config.js で以下の設定を行います。この設定により、サーバーサイドで使用する外部パッケージを明示的に指定し、バンドルから除外します

next.config.js
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: [
'@opentelemetry/sdk-node',
'pino',
]
};
export default nextConfig;

pnpm dev でアプリケーションを起動し、http://localhost:3000 にアクセスすると、以下のようなログが出力されます

Terminal window
{"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 が含まれます

Terminal window
$ 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
}'
Terminal window
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 が付与されるところしかしませんでした。

今後は以下の取り組みにチャレンジしたいと考えています。

  • メトリクス収集との連携
  • アラート設定の自動化
  • より包括的な監視システムの構築