<CodeLearn/>
モニタリング レッスン2

ログ設計

構造化ログ、ログレベル、Winstonによる実践的なログ設計を学ぼう

構造化ログ(Structured Logging)

構造化ログとは、ログをJSON形式で出力することで、 機械的に検索・集計しやすくする手法です。従来のテキストログと比較してみましょう。

// 非構造化ログ(従来のスタイル)
console.log("2024-01-15 10:30:00 ERROR User login failed for user@example.com from 192.168.1.1");
// → 検索しにくい、パースが困難

// 構造化ログ(推奨)
logger.error("User login failed", {
  email: "user@example.com",
  ip: "192.168.1.1",
  reason: "invalid_password",
  attemptCount: 3,
});
// 出力:
// {"timestamp":"2024-01-15T10:30:00Z","level":"error",
//  "message":"User login failed","email":"user@example.com",
//  "ip":"192.168.1.1","reason":"invalid_password","attemptCount":3}

構造化ログのメリット

JSON形式なので「email="user@example.com"のエラーログだけ検索」のようなフィルタリングが容易になります。 ELKやDatadogなどのログ管理ツールとの連携も簡単です。

ログレベル

ログには重要度に応じたレベルを設定します。 適切なレベルを使い分けることで、必要な情報だけを効率的に確認できます。

error

アプリケーションの正常動作を妨げるエラー。即座に対応が必要。

例: DB接続失敗、外部API障害、未処理の例外

warn

問題の予兆や、想定外だが動作は継続できる状態。

例: ディスク容量80%超え、APIレスポンスが遅い、リトライ発生

info

アプリケーションの正常な動作を記録。ビジネスイベントの追跡に有用。

例: ユーザーログイン、注文完了、サーバー起動

debug

開発・デバッグ時に詳細な情報を出力。本番環境では通常無効にする。

例: 関数の引数・戻り値、SQLクエリ、リクエスト/レスポンスの詳細

Winston によるログ設定

Winston はNode.jsで最も人気のあるロギングライブラリです。 複数の出力先(トランスポート)、ログレベル、フォーマットを柔軟に設定できます。

// npm install winston

import winston from "winston";

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || "info",
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: {
    service: "my-api",
    environment: process.env.NODE_ENV,
  },
  transports: [
    // エラーログは別ファイルに出力
    new winston.transports.File({
      filename: "logs/error.log",
      level: "error",
      maxsize: 5242880, // 5MB
      maxFiles: 5,
    }),
    // 全レベルのログを出力
    new winston.transports.File({
      filename: "logs/combined.log",
      maxsize: 5242880,
      maxFiles: 10,
    }),
  ],
});

// 開発環境ではコンソールにも出力
if (process.env.NODE_ENV !== "production") {
  logger.add(
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      ),
    })
  );
}

export default logger;

Correlation ID(相関ID)

1つのリクエストに関連するすべてのログを紐づけるために、Correlation ID(リクエストID)を付与します。 マイクロサービス環境では特に重要です。

import { randomUUID } from "crypto";
import { Request, Response, NextFunction } from "express";

// Correlation ID ミドルウェア
function correlationMiddleware(req: Request, res: Response, next: NextFunction) {
  // クライアントから送られたIDがあればそれを使う
  const correlationId = req.headers["x-correlation-id"] as string
    || randomUUID();

  // リクエストオブジェクトに付与
  req.correlationId = correlationId;

  // レスポンスヘッダーにも含める
  res.setHeader("x-correlation-id", correlationId);

  next();
}

// ログ出力時にCorrelation IDを含める
app.get("/api/users/:id", async (req, res) => {
  const correlationId = req.correlationId;

  logger.info("Fetching user", {
    correlationId,
    userId: req.params.id,
  });

  try {
    const user = await userService.findById(req.params.id);
    logger.info("User found", { correlationId, userId: user.id });
    res.json(user);
  } catch (error) {
    logger.error("Failed to fetch user", {
      correlationId,
      userId: req.params.id,
      error: error.message,
    });
    res.status(500).json({ error: "Internal server error" });
  }
});

ログの検索例

Correlation IDで検索すると、1つのリクエストに関連する全てのログが時系列で取得できます。 障害調査の時間を大幅に短縮できます。

ログローテーション

ログファイルを放置するとディスクを圧迫します。ログローテーションで定期的にファイルを切り替え、古いログを削除しましょう。

// npm install winston-daily-rotate-file

import DailyRotateFile from "winston-daily-rotate-file";

const rotateTransport = new DailyRotateFile({
  filename: "logs/app-%DATE%.log",
  datePattern: "YYYY-MM-DD",
  maxSize: "20m",       // 1ファイル最大20MB
  maxFiles: "14d",      // 14日分保持
  zippedArchive: true,  // 古いファイルはgzip圧縮
});

rotateTransport.on("rotate", (oldFilename, newFilename) => {
  logger.info("Log file rotated", { oldFilename, newFilename });
});

// Winstonに追加
logger.add(rotateTransport);

何をログに記録するか

適切な情報をログに記録することで、障害調査やビジネス分析に活用できます。 ただし、機密情報のログ出力には注意が必要です。

// 記録すべき情報
logger.info("Order created", {
  orderId: "ord-12345",
  userId: "usr-67890",
  totalAmount: 4980,
  itemCount: 3,
  paymentMethod: "credit_card",
  correlationId: req.correlationId,
});

// 記録してはいけない情報(セキュリティリスク)
// NG: パスワード、クレジットカード番号、個人情報の詳細
logger.info("User login", {
  email: "user@example.com",
  password: "secret123",       // 絶対にNG!
  creditCard: "4242-xxxx-xxxx", // 絶対にNG!
});

// OK: 機密情報をマスキング
logger.info("User login", {
  email: "u***@example.com",   // マスキング
  hasPassword: true,            // 存在の有無だけ記録
});

まとめ

  • 構造化ログ(JSON形式)を使うことで検索・集計が容易になる
  • ログレベル(error/warn/info/debug)を適切に使い分ける
  • Winstonで柔軟なログ設定を行い、環境ごとに出力を切り替える
  • Correlation IDでリクエスト単位のログ追跡を可能にする
  • ログローテーションでディスク容量を管理する
  • パスワードやカード番号などの機密情報はログに含めない