LESSON 30分

ストーリー

あなた
ログを見ればわかる、って言うけど…このログ、何を言ってるか全然わからない

あなたはサーバーのログファイルを眺めて途方に暮れていた。

[2025-01-15 10:30:45] ERROR: Something went wrong [2025-01-15 10:30:45] INFO: Processing request [2025-01-15 10:30:46] WARN: Timeout occurred

高橋アーキテクトが覗き込んだ。

高橋アーキテクト
これが”非構造化ログ”の限界だ。人間が読む分にはなんとなくわかるが、機械が検索・分析するには厳しい。構造化ログに変えるだけで、障害調査のスピードが劇的に変わるよ

非構造化ログ vs 構造化ログ

非構造化ログ(従来型)

[2025-01-15 10:30:45] ERROR: Payment failed for order ORD-001, user USR-042, amount 5000
[2025-01-15 10:30:45] INFO: Retrying payment for order ORD-001 (attempt 2/3)
[2025-01-15 10:30:46] ERROR: Payment timeout for order ORD-001 after 3 retries

問題点:

  • 「order ORD-001」のログだけを抽出するには正規表現が必要
  • フィールドの位置やフォーマットがログメッセージごとに異なる
  • 自動分析や集計が困難

構造化ログ(JSON形式)

{"timestamp":"2025-01-15T10:30:45.123Z","level":"ERROR","message":"Payment failed","service":"payment-service","traceId":"abc123","orderId":"ORD-001","userId":"USR-042","amount":5000,"errorCode":"GATEWAY_ERROR"}
{"timestamp":"2025-01-15T10:30:45.456Z","level":"INFO","message":"Retrying payment","service":"payment-service","traceId":"abc123","orderId":"ORD-001","attempt":2,"maxRetries":3}
{"timestamp":"2025-01-15T10:30:46.789Z","level":"ERROR","message":"Payment timeout","service":"payment-service","traceId":"abc123","orderId":"ORD-001","retriesExhausted":true,"totalDuration":1666}

利点:

  • orderId:"ORD-001" で即座にフィルタリング可能
  • フィールドが明確に定義されているため集計が容易
  • 機械的な分析・アラート設定が簡単

TypeScriptでの構造化ログ実装

import pino from 'pino';

// ロガーの初期化
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level(label) {
      return { level: label };
    },
  },
  timestamp: pino.stdTimeFunctions.isoTime,
});

// 子ロガーでコンテキストを付与
function createRequestLogger(req: Request) {
  return logger.child({
    requestId: req.headers['x-request-id'],
    traceId: req.headers['x-trace-id'],
    method: req.method,
    path: req.url,
    userAgent: req.headers['user-agent'],
  });
}

// 使用例
async function handleOrder(req: Request) {
  const log = createRequestLogger(req);

  log.info({ orderId: req.body.orderId }, 'Order processing started');

  try {
    const result = await processPayment(req.body);
    log.info(
      { orderId: req.body.orderId, amount: result.amount, duration: result.duration },
      'Order processed successfully'
    );
  } catch (error) {
    log.error(
      { orderId: req.body.orderId, error: error.message, stack: error.stack },
      'Order processing failed'
    );
  }
}

コンテキスト情報の設計

構造化ログに含めるべきコンテキスト情報を整理します。

interface LogContext {
  // 必須フィールド
  timestamp: string;       // ISO 8601形式
  level: string;           // ログレベル
  message: string;         // 人間が読めるメッセージ
  service: string;         // サービス名

  // トレーサビリティ
  traceId?: string;        // 分散トレースID
  spanId?: string;         // SpanID
  requestId?: string;      // リクエストID

  // ビジネスコンテキスト
  userId?: string;         // ユーザー識別子
  orderId?: string;        // 注文識別子
  action?: string;         // 実行中の操作

  // 技術コンテキスト
  hostname?: string;       // ホスト名
  pid?: number;            // プロセスID
  version?: string;        // アプリケーションバージョン

  // エラー情報
  error?: {
    name: string;
    message: string;
    stack?: string;
    code?: string;
  };

  // パフォーマンス
  duration?: number;       // 処理時間(ミリ秒)
  statusCode?: number;     // HTTPステータスコード
}
カテゴリフィールド例目的
必須timestamp, level, message, service基本的なログ識別
トレーサビリティtraceId, spanId, requestIdリクエスト追跡
ビジネスuserId, orderId業務観点での調査
技術hostname, pid, versionインフラ観点での調査
エラーerror.name, error.stackデバッグ
パフォーマンスduration, statusCode性能分析

ログフォーマットのベストプラクティス

ルール説明
JSON形式を使う機械可読性を確保{"level":"info",...}
タイムスタンプはISO 8601タイムゾーン問題を回避2025-01-15T10:30:45.123Z
メッセージは簡潔に詳細はコンテキストフィールドに"Payment failed"
機密情報を含めないPII、パスワード、トークンは除外ユーザー名ではなくIDを使う
一貫したフィールド名チーム内で命名規則を統一camelCase or snake_case

まとめ

ポイント内容
構造化ログJSON形式でフィールドを明確にしたログ
利点機械的な検索・分析・アラート設定が容易
コンテキストtraceId、userId等を含めて追跡性を確保
注意点機密情報の除外、フォーマットの一貫性

チェックリスト

  • 非構造化ログと構造化ログの違いを説明できる
  • pinoを使った構造化ログの実装方法を理解した
  • ログに含めるべきコンテキスト情報を設計できる
  • ログフォーマットのベストプラクティスを把握した

次のステップへ

次は「ログレベルとフィルタリング」を学びます。適切なログレベルの使い分けと、効率的なフィルタリング方法を見ていきましょう。


推定読了時間: 30分