ストーリー
佐
佐藤CTO
3本柱の最後 — ログだ。ログは最も古くからあるテレメトリだが、最も誤用されやすい
あなた
今のシステムでも大量のログを出力していますが、障害時に探すのが大変です
あ
佐
佐藤CTO
それは非構造化ログだからだ。console.log('error happened') のような文字列ログでは、検索も集約も困難だ。構造化ログ — JSON形式のログに統一することで、ログの価値が劇的に変わる
非構造化ログ vs 構造化ログ
比較
// 非構造化ログ(アンチパターン)
console.log('Error: Payment failed for order 12345, amount 5000, user abc');
console.log(`[${new Date().toISOString()}] WARN: Retry attempt 3/5 for payment`);
console.error('Database connection timeout after 30s');
// 構造化ログ(推奨)
logger.error('Payment processing failed', {
orderId: '12345',
amount: 5000,
currency: 'JPY',
userId: 'abc',
errorCode: 'PAYMENT_TIMEOUT',
provider: 'stripe',
traceId: 'abc-123-def-456',
spanId: 'span-789',
});
| 観点 | 非構造化ログ | 構造化ログ |
|---|
| フォーマット | 自由テキスト | JSON |
| 検索性 | 全文検索のみ | フィールド指定検索 |
| 集約 | 正規表現が必要 | フィールドでグループ化 |
| パース | 手動パース | 自動パース |
| トレース相関 | 困難 | traceId/spanIdで直接紐付け |
構造化ログの実装
ロガーライブラリの設定
import pino from 'pino';
// ロガーの初期化
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
formatters: {
level: (label) => ({ level: label }),
bindings: (bindings) => ({
service: 'payment-service',
version: process.env.APP_VERSION ?? 'unknown',
environment: process.env.NODE_ENV ?? 'development',
hostname: bindings.hostname,
pid: bindings.pid,
}),
},
timestamp: pino.stdTimeFunctions.isoTime,
// 本番ではJSON、開発では整形表示
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
});
// 出力例(本番JSON):
// {
// "level": "error",
// "time": "2026-02-14T10:30:00.000Z",
// "service": "payment-service",
// "version": "1.2.0",
// "environment": "production",
// "hostname": "payment-pod-abc123",
// "pid": 1,
// "msg": "Payment processing failed",
// "orderId": "12345",
// "traceId": "abc-123-def-456"
// }
ログレベルの使い分け
| レベル | 用途 | 出力先 | 例 |
|---|
| FATAL | システム停止レベル | アラート即時通知 | DB接続完全喪失 |
| ERROR | 処理失敗、要対応 | アラート通知 | 決済処理失敗 |
| WARN | 潜在的問題 | ダッシュボード監視 | リトライ3回目 |
| INFO | 重要なイベント | 通常ログ | リクエスト完了、デプロイ |
| DEBUG | 詳細情報 | 開発/トラブルシュート | SQLクエリ、変数値 |
| TRACE | 最詳細 | 開発環境のみ | 関数の入出力 |
Trace IDとの相関
import { context, trace } from '@opentelemetry/api';
// OpenTelemetryのTrace IDをログに自動付与するミドルウェア
function traceContextMiddleware(req: Request, res: Response, next: NextFunction): void {
const activeSpan = trace.getSpan(context.active());
const spanContext = activeSpan?.spanContext();
// リクエストスコープのロガーを作成
req.log = logger.child({
traceId: spanContext?.traceId ?? 'no-trace',
spanId: spanContext?.spanId ?? 'no-span',
requestId: req.headers['x-request-id'] ?? crypto.randomUUID(),
method: req.method,
path: req.path,
userAgent: req.headers['user-agent'],
});
req.log.info('Request started');
res.on('finish', () => {
req.log.info('Request completed', {
statusCode: res.statusCode,
responseTime: Date.now() - req.startTime,
});
});
next();
}
ログ集約基盤
ELK Stack vs Grafana Loki
| 観点 | ELK (Elasticsearch) | Grafana Loki |
|---|
| インデックス | 全フィールドインデックス | ラベルのみインデックス |
| ストレージコスト | 高い | 低い(元ログはObject Storage) |
| 検索速度 | 高速(全文検索) | ラベル検索は高速、本文検索は遅い |
| 運用コスト | 高い(クラスタ管理) | 低い(シンプルな構成) |
| Grafana統合 | Kibana or Grafana | ネイティブ統合 |
| 推奨ケース | ログの全文検索が多い場合 | Grafanaスタックを使う場合 |
Loki設定例
# loki-config.yaml
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /loki
storage:
s3:
endpoint: s3.amazonaws.com
bucketnames: loki-logs
region: ap-northeast-1
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: s3
schema: v13
index:
prefix: loki_index_
period: 24h
limits_config:
retention_period: 30d
max_query_length: 24h
max_entries_limit_per_query: 10000
# ログパイプラインでの加工
ruler:
alertmanager_url: http://alertmanager:9093
LogQL(Lokiのクエリ言語)
# 基本的なログ検索
{service="payment-service"} |= "error"
# JSONパース + フィルタ
{service="payment-service"}
| json
| level = "error"
| orderId != ""
| line_format "{{.orderId}}: {{.msg}}"
# エラー率の計算(メトリクスクエリ)
sum(rate(
{service="payment-service"} | json | level = "error" [5m]
))
/
sum(rate(
{service="payment-service"} [5m]
))
# 特定のTrace IDに関連するログ
{service=~".*"} | json | traceId = "abc-123-def-456"
# 遅いリクエストのログ
{service="payment-service"}
| json
| responseTime > 3000
| line_format "Slow request: {{.path}} took {{.responseTime}}ms"
ログ設計のベストプラクティス
ログに含めるべき情報
# 共通フィールド(全ログに含む)
common_fields:
timestamp: "ISO 8601形式"
level: "error/warn/info/debug"
service: "サービス名"
version: "アプリバージョン"
environment: "production/staging/development"
traceId: "OpenTelemetry Trace ID"
spanId: "OpenTelemetry Span ID"
requestId: "リクエスト固有ID"
# イベント固有フィールド
event_fields:
http_request:
- method
- path
- statusCode
- responseTime
- userAgent
- clientIp
payment:
- orderId
- amount
- currency
- provider
- transactionId
error:
- errorCode
- errorMessage
- stackTrace
ログのアンチパターンと改善
| アンチパターン | 問題 | 改善 |
|---|
| 機密情報のログ出力 | クレジットカード番号、パスワード | マスキング処理を実装 |
| 大量のDEBUGログ | ストレージコスト増大 | 本番ではINFO以上に制限 |
| 非構造化文字列 | 検索・集約が困難 | JSON構造化ログに統一 |
| ログ出力のない例外処理 | 障害の原因追跡が不能 | catch節でエラーログを必ず出力 |
| Trace IDの欠落 | ログとトレースの紐付け不可 | ミドルウェアで自動付与 |
| ログローテーションなし | ディスク溢れ | ログ管理ツール or stdout + 外部収集 |
// 機密情報のマスキング例
function maskSensitiveData(data: Record<string, unknown>): Record<string, unknown> {
const sensitiveFields = ['password', 'creditCard', 'ssn', 'token'];
const masked = { ...data };
for (const field of sensitiveFields) {
if (field in masked) {
masked[field] = '***MASKED***';
}
}
return masked;
}
まとめ
| ポイント | 内容 |
|---|
| 構造化ログ | JSON形式で統一し、フィールド検索・集約を可能にする |
| ログレベル | FATAL/ERROR/WARN/INFO/DEBUG/TRACEの適切な使い分け |
| Trace相関 | traceId/spanIdをログに含め、トレースと紐付ける |
| ログ集約 | ELK(全文検索重視)vs Loki(コスト重視・Grafana統合) |
| ベストプラクティス | 共通フィールドの標準化、機密情報マスキング |
チェックリスト
次のステップへ
次は「演習:オブザーバビリティ基盤を構築しよう」です。メトリクス、トレース、ログを統合したオブザーバビリティ基盤の設計と構築に挑戦しましょう。
推定読了時間: 40分