Next.jsアプリケーションを本番環境で運用する際、分散したログの収集と分析は開発者にとって大きな課題となる。特にSSR・ISR・SSGといった複数のレンダリングモードが混在する環境では、サーバーサイドとクライアントサイドのログが別々の場所に記録され、トラブルシューティングが困難になる。本記事では、構造化されたログを一元的に収集・可視化し、リアルタイムで問題を検出できる堅牢なログ管理基盤の構築方法を解説する。
ログ分散の根本的課題
- スタックごとの出力先の違い:APIルートやミドルウェアのログはNode.jsプロセスに出力される一方、フロントエンドエラーはユーザーのブラウザに閉じ込められる
- ホスティング制約:VercelやNetlifyなどのマネージドサービスでは、ログの永続化や詳細なフィルタリングが制限されることが多い
- トレースコンテキストの欠如:リクエストIDやユーザー識別子がログに含まれていないと、単一のユーザージャーニーを再現できない
推奨アーキテクチャ
効果的なログ管理には以下の4層構造を採用することが望ましい:
- 収集層:アプリケーション内に埋め込まれたSDKと、ホスト上で動作するエージェント
- 転送層:TLSで保護されたHTTPまたはSyslogプロトコルによる安全な送信
- ストレージ層:ElasticsearchやLokiなど、時系列データに最適化されたバックエンド
- 可視化層:KibanaやGrafanaによるダッシュボードとアラート設定
サーバーサイドでの構造化ログ実装
Winstonを用いた型安全なログ出力例:
// lib/logger.ts
import winston from 'winston';
const createLogger = () => {
return winston.createLogger({
level: process.env.LOG_LEVEL ?? 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { app: 'nextjs-production' },
transports: [
new winston.transports.Console(),
// ファイル出力はコンテナ環境では不要な場合あり
],
});
};
export const appLogger = createLogger();
APIルートでの利用例(リクエストコンテキストを含む):
// pages/api/user.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { appLogger } from '../../lib/logger';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const startTime = Date.now();
const requestId = req.headers['x-request-id'] as string | undefined;
try {
const user = await fetchUser(req.query.id as string);
const duration = Date.now() - startTime;
appLogger.info('user.fetched', {
userId: user.id,
requestId,
path: req.url,
method: req.method,
statusCode: 200,
durationMs: duration,
});
res.status(200).json(user);
} catch (err) {
appLogger.error('user.fetch.failed', {
error: err instanceof Error ? err.message : 'Unknown error',
stack: err instanceof Error ? err.stack : undefined,
requestId,
path: req.url,
});
res.status(500).json({ error: 'Internal Server Error' });
}
}
クライアント側のエラーハンドリング
Sentryを用いてフロントエンド例外をキャプチャ:
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [Sentry.replayIntegration()],
});
ReactエラーバウンダリによるUIエラーの捕捉:
// components/SentryErrorBoundary.tsx
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
export function SentryErrorBoundary({ children }: { children: React.ReactNode }) {
useEffect(() => {
const errorHandler = (event: ErrorEvent) => {
Sentry.captureException(event.error);
};
window.addEventListener('error', errorHandler);
return () => window.removeEventListener('error', errorHandler);
}, []);
return <Sentry.ErrorBoundary fallback=<p>An error occurred.</p>>{children}</Sentry.ErrorBoundary>;
}
ログの可視化と監視
KibanaやGrafana Lokiで使用可能なクエリ例:
status_code >= 500 and app:"nextjs-production"— サーバーエラーの検出durationMs > 1000 | line_format "{{.path}} took {{.durationMs}}ms"— 遅延レスポンスの特定count by (path) (rate({app="nextjs-production"} |= "error" [5m]))— エラーレートのモニタリング
運用上の考慮点
- 機密情報のマスキング:ログ出力前にパスワードやトークンをフィルタリングするミドルウェアを導入
- サンプリング戦略:高頻度エンドポイントでは、10%程度のトレースサンプリングを適用
- ストレージのライフサイクル:重要度に応じてログの保持期間を設定(例:エラーログは30日、アクセスログは7日)
- アラート連携:SlackやPagerDutyと連携し、重大エラーを即時通知
コンテナ環境での設定例
Dockerfileでログディレクトリを明示的に定義:
FROM node:18-alpine AS base
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
FROM base AS runner
USER root
RUN mkdir -p /var/log/app && chown nextjs:nodejs /var/log/app
USER nextjs
Logrotateによるログローテーション(Linux環境向け):
/var/log/app/*.log {
daily
rotate 10
compress
delaycompress
missingok
notifempty
su nextjs nodejs
}