ストーリー
あなたはサーバーのログファイルを眺めて途方に暮れていた。
[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分