EXERCISE 90分

ストーリー

高橋アーキテクト
座学はここまでだ。実際にログ基盤を設計してみよう

高橋アーキテクトがシナリオを用意してくれた。

高橋アーキテクト
ECサイトのマイクロサービス群がある。これに対して、構造化ログの設計からログ集約基盤、アラート設計まで一通り作ってくれ。段階的に進めていこう

演習の概要

ECサイト(マイクロサービス構成)に対して、ログ基盤を設計してください。

システム構成

[Client] → [API Gateway] → [User Service]
                          → [Order Service] → [Inventory Service]
                          → [Payment Service] → [外部決済API]
                          → [Notification Service]

ミッション一覧

#ミッション難易度
1構造化ログのスキーマ設計基本
2TypeScriptでのロガー実装基本
3ログレベルのガイドライン策定基本
4ログ集約アーキテクチャの設計応用
5障害調査用クエリの作成応用
6ログベースアラートの設計応用

ミッション1: 構造化ログのスキーマ設計

ECサイトの全サービスで共通して使用するログスキーマを設計してください。

要件

  • 必須フィールドとオプションフィールドを定義する
  • トレーサビリティ用のフィールドを含める
  • ビジネスコンテキスト(orderId, userId等)を含める
  • 機密情報を含めないルールを定める
解答例(自分で実装してから確認しよう)
// 共通ログスキーマ
interface BaseLogSchema {
  // 必須フィールド
  timestamp: string;         // ISO 8601 (UTC)
  level: 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
  message: string;           // 人間が読めるメッセージ
  service: string;           // サービス名 (e.g., "order-service")
  environment: string;       // 環境 (e.g., "production")
  version: string;           // アプリケーションバージョン

  // トレーサビリティ(必須)
  traceId: string;           // 分散トレースID
  spanId: string;            // SpanID
  requestId: string;         // リクエスト固有ID

  // インフラ情報
  hostname: string;          // ホスト名 / Pod名
  pid: number;               // プロセスID
}

interface ExtendedLogSchema extends BaseLogSchema {
  // ビジネスコンテキスト(オプション)
  userId?: string;           // ユーザーID(メールアドレスではなくIDを使用)
  orderId?: string;
  paymentId?: string;
  productId?: string;

  // パフォーマンス情報(オプション)
  duration?: number;         // 処理時間(ms)
  statusCode?: number;       // HTTPステータスコード

  // エラー情報(オプション)
  error?: {
    name: string;
    message: string;
    code?: string;
    stack?: string;          // 本番ではサンプリング
  };
}

// 機密情報の除外ルール
const SENSITIVE_FIELDS = [
  'password', 'token', 'secret', 'creditCard',
  'email', 'phoneNumber', 'address',
];

ミッション2: TypeScriptでのロガー実装

pinoを使って、共通ロガーモジュールを実装してください。

要件

  • 共通フィールドの自動付与(service, hostname, version)
  • リクエストごとの子ロガー生成
  • 機密情報のマスキング
解答例(自分で実装してから確認しよう)
import pino, { Logger } from 'pino';
import { IncomingMessage } from 'http';

const SENSITIVE_KEYS = ['password', 'token', 'secret', 'creditCard', 'authorization'];

function redactSensitive(obj: Record<string, unknown>): Record<string, unknown> {
  const result = { ...obj };
  for (const key of Object.keys(result)) {
    if (SENSITIVE_KEYS.some((s) => key.toLowerCase().includes(s))) {
      result[key] = '[REDACTED]';
    }
  }
  return result;
}

export function createLogger(serviceName: string): Logger {
  return pino({
    level: process.env.LOG_LEVEL || 'info',
    timestamp: pino.stdTimeFunctions.isoTime,
    base: {
      service: serviceName,
      environment: process.env.NODE_ENV || 'development',
      version: process.env.APP_VERSION || 'unknown',
      hostname: process.env.HOSTNAME || 'localhost',
      pid: process.pid,
    },
    redact: {
      paths: ['password', '*.password', 'authorization', '*.token'],
      censor: '[REDACTED]',
    },
    formatters: {
      level(label) {
        return { level: label.toUpperCase() };
      },
    },
  });
}

export function createRequestLogger(
  baseLogger: Logger,
  req: IncomingMessage
): Logger {
  return baseLogger.child({
    traceId: req.headers['x-trace-id'] || generateId(),
    spanId: generateId(),
    requestId: req.headers['x-request-id'] || generateId(),
    method: req.method,
    path: req.url,
  });
}

function generateId(): string {
  return Math.random().toString(36).substring(2, 15);
}

ミッション3: ログレベルのガイドライン策定

チーム内で共有するログレベルのガイドラインを作成してください。

要件

  • 各レベルの定義と使用例を記述
  • 「こういう場合はこのレベル」の具体例を5つ以上
  • アンチパターン(やってはいけないこと)を3つ以上
解答例(自分で実装してから確認しよう)
# ログレベルガイドライン

## レベル定義

| レベル | 定義 | 本番出力 |
|--------|------|---------|
| TRACE | メソッドの入出力、変数の値 | No |
| DEBUG | 内部処理の詳細(キャッシュヒット等) | No(障害時のみ一時的に有効化) |
| INFO | 正常な業務イベントの記録 | Yes |
| WARN | 注意が必要だが処理は継続可能 | Yes |
| ERROR | エラー発生、個別リクエストが失敗 | Yes |
| FATAL | プロセス停止を伴う致命的エラー | Yes |

## 具体例

1. INFO: 注文の作成成功 → `logger.info({ orderId }, 'Order created')`
2. INFO: ユーザーのログイン成功 → `logger.info({ userId }, 'User logged in')`
3. WARN: リトライ発生 → `logger.warn({ orderId, attempt }, 'Payment retry')`
4. WARN: 在庫残り少 → `logger.warn({ productId, stock }, 'Low stock')`
5. ERROR: 決済タイムアウト → `logger.error({ orderId, err }, 'Payment timeout')`
6. ERROR: DB接続失敗(個別クエリ) → `logger.error({ query, err }, 'DB query failed')`
7. FATAL: DB接続プール枯渇 → `logger.fatal({ err }, 'DB pool exhausted')`

## アンチパターン

1. すべてのログをINFOで出力する(レベルの意味がなくなる)
2. ERRORログにスタックトレースを含めない(デバッグが困難になる)
3. ループ内でINFOログを出力する(ログ量が爆発する)
4. 機密情報(パスワード、トークン)をログに含める
5. メッセージだけのログ(コンテキスト情報なし)

ミッション4: ログ集約アーキテクチャの設計

ECサイトのログ集約アーキテクチャを設計してください。

要件

  • 5つのマイクロサービスからログを収集
  • コスト効率を考慮したストレージ設計
  • 保持ポリシーを定義
解答例(自分で実装してから確認しよう)
# ログ集約アーキテクチャ設計書

architecture:
  shipper: Fluent Bit  # 軽量なログ転送エージェント
  storage: Grafana Loki  # 低コストなログストレージ
  visualization: Grafana  # ダッシュボード

flow:
  - サービスがstdoutにJSON構造化ログを出力
  - Fluent Bit(DaemonSet)がコンテナログを収集
  - Fluent Bitがラベル(service, level)を付与してLokiに送信
  - GrafanaでLogQLクエリを使って検索・可視化

retention_policy:
  - level: ERROR/FATAL
    retention: 90日
    storage_class: hot
  - level: WARN
    retention: 30日
    storage_class: warm
  - level: INFO
    retention: 14日
    storage_class: warm
  - level: DEBUG
    retention: 3日
    storage_class: cold

cost_optimization:
  - 正常系INFOログは10%サンプリング
  - ヘルスチェック(/health)のログは除外
  - DEBUG/TRACEは本番では原則出力しない

ミッション5: 障害調査用クエリの作成

以下のシナリオで使用するログクエリを作成してください。

シナリオ

「POST /api/orders エンドポイントのエラー率が急増している」

解答例(自分で実装してから確認しよう)
# 1. エラースパイクの確認(5分単位で集計)
{service=~".*"} | json | level="ERROR"
  | line_format "{{.service}}" | rate([5m])

# 2. エラーが多いサービスの特定
{service=~".*"} | json | level="ERROR"
  | stats count by (service)

# 3. エラーコード別の内訳
{service="order-service"} | json | level="ERROR"
  | stats count by (errorCode)

# 4. 特定のエラーの詳細ログ
{service="payment-service"} | json | level="ERROR"
  | line_format "{{.timestamp}} {{.message}} orderId={{.orderId}}"

# 5. 特定リクエストの全経路追跡
{service=~".*"} | json | traceId="abc123def456"
  | line_format "{{.timestamp}} [{{.service}}] {{.level}} {{.message}}"

# 6. デプロイイベントとの相関
{service="deployment"} | json
  | line_format "{{.timestamp}} {{.message}} version={{.version}}"

ミッション6: ログベースアラートの設計

ECサイトに必要なログベースアラートを設計してください。

要件

  • 最低5つのアラートルールを定義
  • 重要度(critical/warning)を分類
  • アラート疲れを避ける工夫を含める
解答例(自分で実装してから確認しよう)
const alertRules = [
  {
    name: 'Critical: Payment Error Spike',
    condition: 'payment-serviceのERRORが1分間に10件以上',
    severity: 'critical',
    notification: ['PagerDuty', 'Slack #oncall'],
    cooldown: '5分',  // 5分以内の再通知を抑制
  },
  {
    name: 'Critical: API Gateway 5xx Spike',
    condition: 'api-gatewayの5xxレスポンスが5分間で全体の5%以上',
    severity: 'critical',
    notification: ['PagerDuty', 'Slack #oncall'],
    cooldown: '5分',
  },
  {
    name: 'Warning: Order Service High Latency',
    condition: 'order-serviceのduration P95が3秒以上が10分間継続',
    severity: 'warning',
    notification: ['Slack #monitoring'],
    cooldown: '30分',
  },
  {
    name: 'Warning: Retry Rate High',
    condition: 'WARNログにretryが含まれるログが5分間に50件以上',
    severity: 'warning',
    notification: ['Slack #monitoring'],
    cooldown: '15分',
  },
  {
    name: 'Critical: FATAL Log Detected',
    condition: 'FATALレベルのログが1件でも発生',
    severity: 'critical',
    notification: ['PagerDuty', 'Slack #oncall'],
    cooldown: '1分',
  },
];

達成度チェック

ミッション内容完了
1構造化ログのスキーマ設計[ ]
2TypeScriptでのロガー実装[ ]
3ログレベルのガイドライン策定[ ]
4ログ集約アーキテクチャの設計[ ]
5障害調査用クエリの作成[ ]
6ログベースアラートの設計[ ]

まとめ

ポイント内容
スキーマ設計必須フィールド + コンテキスト + 機密情報除外
ロガー実装pino + 子ロガー + レダクション
ガイドラインチーム共通のルールで一貫性を確保
アーキテクチャ収集 → 集約 → 検索 → 可視化のパイプライン

チェックリスト

  • 構造化ログのスキーマを設計できた
  • pinoを使ったロガーを実装できた
  • ログレベルのガイドラインを策定できた
  • ログ集約アーキテクチャを設計できた
  • 障害調査用のクエリを作成できた
  • ログベースのアラートを設計できた

次のステップへ

演習が完了したら、Step 2 のチェックポイントクイズに挑戦しましょう。


推定所要時間: 90分