ログ設計
構造化ログ、ログレベル、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でリクエスト単位のログ追跡を可能にする
- ログローテーションでディスク容量を管理する
- パスワードやカード番号などの機密情報はログに含めない