
ログ設計とオブザーバビリティ入門:実務実装ガイド
「本番で障害が起きたけど、ログを見ても原因が分からない」「どのログを見ればいいか分からず、調査に時間がかかる」──こうした課題は、ログ設計とオブザーバビリティの不足から生じます。
本記事では、障害調査を効率化するログ設計の実践手法を、構造化ログ・ログレベル・分散トレーシング・メトリクス収集まで網羅して解説します。受託開発・自社開発の現場で即使える実務ガイドです。
1. ログ設計の基本方針
1.1 ログの目的を明確にする
ログは以下の目的で記録します:
| 目的 | 具体例 | 推奨ログレベル | |------|--------|----------------| | 障害調査 | エラースタックトレース、リクエスト ID | ERROR, WARN | | パフォーマンス分析 | クエリ実行時間、API レスポンスタイム | INFO | | セキュリティ監査 | ログイン試行、権限変更 | INFO, WARN | | ビジネス分析 | ユーザー行動、コンバージョン | INFO | | デバッグ | 変数の値、処理フロー | DEBUG |
1.2 構造化ログを採用する
非構造化ログ(避けるべき):
console.log('User login: user_id=123, status=success, ip=192.168.1.1');
構造化ログ(推奨):
import { logger } from './logger';
logger.info('User login', {
userId: 123,
status: 'success',
ip: '192.168.1.1',
timestamp: new Date().toISOString(),
});
構造化ログのメリット:
- 検索性: JSON フィールドで絞り込み可能
- 集計: ログ集約ツールで統計分析が容易
- パース不要: 文字列解析が不要
1.3 ログレベルの使い分け
| レベル | 用途 | 本番出力 | 例 | |--------|------|----------|----| | ERROR | 即対応が必要な異常 | ✓ | DB 接続失敗、API エラー | | WARN | 異常だが処理続行可能 | ✓ | リトライ成功、非推奨 API 使用 | | INFO | 重要なイベント | ✓ | ユーザーログイン、決済完了 | | DEBUG | 詳細な動作情報 | △ | 関数の入出力、条件分岐 | | TRACE | 最も詳細なデバッグ情報 | × | ループ内の変数値 |
実装例(Node.js + Winston):
import winston from 'winston';
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
2. リクエストトレーシングの実装
2.1 リクエスト ID の付与
リクエストごとに一意な ID を付与し、すべてのログに含めます。
Express ミドルウェア例:
import { v4 as uuidv4 } from 'uuid';
import { Request, Response, NextFunction } from 'express';
export function requestIdMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
req.id = uuidv4();
res.setHeader('X-Request-ID', req.id);
next();
}
// ログ出力時に含める
logger.info('Request received', {
requestId: req.id,
method: req.method,
path: req.path,
});
2.2 分散トレーシング
マイクロサービス間の呼び出しを追跡するため、OpenTelemetry を導入します。
Next.js での設定例:
// instrumentation.ts
import { registerOTel } from '@vercel/otel';
export function register() {
registerOTel({
serviceName: 'my-app',
traceExporter: 'otlp',
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
});
}
手動でスパンを作成:
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('my-service');
export async function fetchUserData(userId: string) {
const span = tracer.startSpan('fetchUserData');
span.setAttribute('user.id', userId);
try {
const data = await db.user.findUnique({ where: { id: userId } });
span.setStatus({ code: 1 }); // OK
return data;
} catch (error) {
span.setStatus({ code: 2, message: error.message }); // ERROR
span.recordException(error);
throw error;
} finally {
span.end();
}
}
3. コンテキスト情報の記録
3.1 必須コンテキスト項目
すべてのログに以下を含めます:
| 項目 | 説明 | 例 | |------|------|----| | timestamp | ISO 8601 形式 | 2026-06-09T12:34:56.789Z | | level | ログレベル | error, warn, info | | service | サービス名 | api-server, worker | | environment | 環境 | production, staging | | requestId | リクエスト ID | uuid | | userId | ユーザー ID(あれば) | 123 |
3.2 エラーログの詳細情報
try {
await processPayment(orderId);
} catch (error) {
logger.error('Payment processing failed', {
requestId: req.id,
userId: req.user.id,
orderId,
error: {
name: error.name,
message: error.message,
stack: error.stack,
code: error.code, // カスタムエラーコード
},
context: {
amount: order.amount,
currency: order.currency,
paymentMethod: order.paymentMethod,
},
});
throw error;
}
3.3 パフォーマンスログ
const startTime = Date.now();
try {
const result = await fetchData();
const duration = Date.now() - startTime;
logger.info('Data fetch completed', {
requestId: req.id,
duration,
recordCount: result.length,
});
if (duration > 1000) {
logger.warn('Slow query detected', {
requestId: req.id,
duration,
query: 'fetchData',
});
}
return result;
} catch (error) {
logger.error('Data fetch failed', { requestId: req.id, error });
throw error;
}
4. ログ収集・可視化の構成
4.1 ログ収集アーキテクチャ
[アプリ] → [ログ出力] → [ログ収集] → [ストレージ] → [可視化]
↓ ↓ ↓ ↓ ↓
Next.js stdout Fluentd S3/BQ Datadog
API ファイル Vector Elasticsearch Grafana
Worker CloudWatch Kibana
4.2 ツール比較表
| ツール | 用途 | 料金 | おすすめ度 | |--------|------|------|------------| | Datadog | ログ・メトリクス・APM 統合 | $15~/host | ★★★★★ | | Grafana Loki | ログ集約(軽量) | OSS | ★★★★☆ | | Elasticsearch + Kibana | ログ検索・可視化 | OSS | ★★★★☆ | | CloudWatch Logs | AWS 環境 | 従量課金 | ★★★☆☆ | | Google Cloud Logging | GCP 環境 | 従量課金 | ★★★☆☆ |
4.3 Grafana Loki 構成例
docker-compose.yml:
version: '3.8'
services:
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/local-config.yaml
promtail:
image: grafana/promtail:latest
volumes:
- /var/log:/var/log
- ./promtail-config.yaml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
アプリからの送信:
import winston from 'winston';
import LokiTransport from 'winston-loki';
const logger = winston.createLogger({
transports: [
new LokiTransport({
host: 'http://localhost:3100',
labels: { app: 'my-app', env: 'production' },
json: true,
}),
],
});
5. メトリクス収集の実装
5.1 主要メトリクス種別
| 種別 | 説明 | 例 | |------|------|----| | Counter | 増加のみ | リクエスト数、エラー数 | | Gauge | 増減する値 | CPU 使用率、接続数 | | Histogram | 分布 | レスポンスタイム | | Summary | パーセンタイル | p95, p99 レイテンシ |
5.2 Prometheus メトリクスの実装
import express from 'express';
import promClient from 'prom-client';
const app = express();
// デフォルトメトリクス(CPU、メモリなど)
promClient.collectDefaultMetrics();
// カスタムメトリクス
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5],
});
const httpRequestTotal = new promClient.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code'],
});
// ミドルウェア
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const labels = {
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode,
};
httpRequestDuration.observe(labels, duration);
httpRequestTotal.inc(labels);
});
next();
});
// メトリクスエンドポイント
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(await promClient.register.metrics());
});
6. アラート設計
6.1 アラート設定のベストプラクティス
| 原則 | 説明 | |------|------| | Actionable | 対応可能な異常のみ通知 | | Threshold | 適切な閾値設定(過検知を避ける) | | Context | 通知に十分なコンテキストを含める | | Escalation | 深刻度に応じた通知先を設定 |
6.2 推奨アラート項目チェックリスト
### 必須アラート
- [ ] エラー率が 5% 以上
- [ ] レスポンスタイム p95 が 3 秒以上
- [ ] DB 接続エラー
- [ ] ディスク使用率 90% 以上
- [ ] メモリ使用率 90% 以上
### 推奨アラート
- [ ] API エラー率が 1% 以上
- [ ] ログイン失敗率の急増
- [ ] 決済失敗率の上昇
- [ ] キューの滞留数増加
- [ ] SSL 証明書の有効期限(30 日前)
### 運用アラート
- [ ] デプロイ通知(成功・失敗)
- [ ] スケーリングイベント
- [ ] バックアップ失敗
6.3 Prometheus アラートルール例
groups:
- name: api_alerts
rules:
- alert: HighErrorRate
expr: |
rate(http_requests_total{status_code=~"5.."}[5m])
/ rate(http_requests_total[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value | humanizePercentage }}"
- alert: SlowResponse
expr: |
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m])
) > 3
for: 10m
labels:
severity: warning
annotations:
summary: "Slow API response"
description: "p95 latency is {{ $value }}s"
7. 個人情報とログの取り扱い
7.1 ログに含めてはいけない情報
| 種類 | 具体例 | 対策 | |------|--------|------| | 認証情報 | パスワード、API キー | マスキング | | 個人情報 | メールアドレス、電話番号 | ユーザー ID に置き換え | | 決済情報 | クレジットカード番号 | PCI DSS 準拠 | | 機密データ | 社内情報、顧客データ | 暗号化または除外 |
7.2 マスキング実装例
function maskSensitiveData(data: any): any {
if (typeof data !== 'object' || data === null) return data;
const sensitiveKeys = ['password', 'token', 'apiKey', 'creditCard'];
const masked = { ...data };
for (const key in masked) {
if (sensitiveKeys.some(k => key.toLowerCase().includes(k))) {
masked[key] = '***MASKED***';
} else if (typeof masked[key] === 'object') {
masked[key] = maskSensitiveData(masked[key]);
}
}
return masked;
}
// 使用例
logger.info('User data', maskSensitiveData({
userId: 123,
email: 'user@example.com', // そのまま
password: 'secret123', // マスク
apiKey: 'abc123', // マスク
}));
8. 実装チェックリスト
8.1 開発時
- [ ] 構造化ログを採用(JSON 形式)
- [ ] ログレベルを適切に使い分け
- [ ] リクエスト ID を全ログに含める
- [ ] エラー時にスタックトレースを記録
- [ ] 個人情報をマスキング
- [ ] 環境変数でログレベルを制御
- [ ] ローカルでログを確認できる
8.2 本番リリース前
- [ ] ログ収集基盤を構築(Loki / Datadog など)
- [ ] 分散トレーシングを設定(OpenTelemetry)
- [ ] メトリクス収集エンドポイントを公開
- [ ] 主要アラートを設定(エラー率、レスポンスタイム)
- [ ] ログ保持期間を決定(30 日〜6 ヶ月)
- [ ] ログローテーション設定
- [ ] オンコール体制を整備
8.3 運用時
- [ ] 週次でログを確認し、異常パターンを検出
- [ ] 月次でアラート閾値を見直し
- [ ] 四半期でログ設計をレビュー
- [ ] インシデント後にログを改善
- [ ] 新機能追加時に必要なログを追加
まとめ
本記事では、障害調査を効率化するログ設計とオブザーバビリティの実装方法を解説しました。
重要ポイント:
- 構造化ログで検索性・集計性を確保
- リクエスト ID で処理を追跡
- 分散トレーシングでマイクロサービス間を可視化
- メトリクス収集でシステム状態を監視
- 適切なアラートで異常を早期検知
- 個人情報のマスキングでコンプライアンス遵守
ログとオブザーバビリティは「後から追加する」では遅く、設計段階から組み込むことが重要です。本記事のチェックリストを活用し、障害に強いシステムを構築してください。
Yureate へのお問い合わせ
Yureate では、受託開発・技術顧問を通じて、ログ基盤の構築支援やオブザーバビリティ導入のコンサルティングを提供しています。「どこから手をつければいいか分からない」「既存システムに後付けしたい」といったご相談もお気軽にどうぞ。
- Web: https://yureate.com
- Email: contact@yureate.com
