メインコンテンツまでスキップ

ログ設計・ログ方針

Bazbiiシステムのログ設計と運用方針を説明します。

ログ設計の概要

ログアーキテクチャ

アプリケーション

slog (Go標準ロガー)

┌─────────┬──────────────┐
│ stdout │ OpenTelemetry │
└─────────┴──────────────┘
↓ ↓
Loki OTel Collector

ログの出力先

  1. stdout: 開発環境、デバッグ用
  2. OpenTelemetry: 本番環境、ログ集約システムへ送信

ログレベル

レベルと使い分け

  • Debug: 詳細なデバッグ情報(開発環境のみ)
  • Info: 正常系の重要な処理(リクエスト完了、イベント発生)
  • Warn: 警告(回復可能なエラー、非推奨機能の使用)
  • Error: エラー(処理失敗、例外)

レベル選択のガイドライン

// ✅ 正しい使い方
logger.Debug("processing request", "request_id", reqID) // 詳細な処理ログ
logger.Info("request completed", "duration_ms", duration) // 正常完了
logger.Warn("rate limit approaching", "remaining", remaining) // 警告
logger.Error("database connection failed", "error", err) // エラー

// ❌ 避けるべき使い方
logger.Info("error occurred", "error", err) // エラーはErrorレベル
logger.Error("request received") // 正常系はInfoレベル

構造化ログ

ログフォーマット

Bazbiiでは構造化ログ(JSON形式)を使用します。

// 構造化ログの例
logger.Info("post created",
slog.String("log_id", "EVT_POST_001"),
slog.String("post_id", postID.String()),
slog.String("actor_id", actorID.String()),
slog.String("h3_index", h3Index.String()),
slog.Time("created_at", time.Now()),
)

ログメッセージの管理

ログメッセージはserver-platform/observability/logmsg/で一元管理します。

// logmsg/events.go
var domainEventMapping = map[string]LogMsg{
"post.created": {
ID: "EVT_POST_001",
Template: "post created",
Level: slog.LevelInfo,
},
"post.deletion_failed": {
ID: "EVT_POST_003",
Template: "post deletion failed",
Level: slog.LevelError,
},
}

ログID体系

ID形式

[種類]_[カテゴリ]_[連番]

種類

  • REQ_: リクエストライフサイクル(REQ_001, REQ_002, ...)
  • EVT_: ドメインイベント(EVT_POST_001, EVT_AUTH_001, ...)
  • API_: API呼び出し(API_CALL_001, API_ERROR_001, ...)
  • DB_: データベース操作(DB_QUERY_001, DB_ERROR_001, ...)
  • OBS_: オブザーバビリティ初期化(OBS_INIT_001, OBS_FAILED_001, ...)

カテゴリ

  • POST: 投稿関連
  • AUTH: 認証関連
  • FEED: フィード関連
  • HEATMAP: ヒートマップ関連

EVT_POST_001  → Post created
EVT_POST_002 → Post deleted
EVT_POST_003 → Post creation failed
REQ_001 → Request received
REQ_012 → API service call failed

ログ出力の実装

Contextベースのロガー

// Contextからロガーを取得
logger := logger.FromContext(ctx)
logger.Info("processing request", "request_id", reqID)

ログメッセージの出力

// ログメッセージマスタを使用
logmsg.Provisioned.Emit(ctx,
slog.String("actor_id", actorID.String()),
slog.String("device_id", deviceID.String()),
)

// エラーを含むログ
logmsg.ProvisioningFailed.EmitError(ctx, err,
slog.String("actor_id", actorID.String()),
)

ドメインイベントのログ化

// イベントパブリッシャーでログに変換
publisher.Publish(ctx, event) // → ログに自動変換

ログに含めるべき情報

必須情報

  • log_id: ログメッセージID
  • timestamp: タイムスタンプ(自動付与)
  • service: サービス名
  • trace_id: トレースID(OpenTelemetry連携時)

コンテキスト情報

  • request_id: リクエストID(分散トレーシング)
  • actor_id: アクターID(認証済みユーザー)
  • user_agent: ユーザーエージェント
  • ip_address: IPアドレス(必要に応じて)

エラー情報

  • error: エラーメッセージ
  • error_type: エラーの種類
  • stack_trace: スタックトレース(開発環境のみ)

ログに含めない情報

機密情報

  • ❌ パスワード、APIキー、トークン
  • ❌ 個人情報(メールアドレス、電話番号)※必要最小限に
  • ❌ クレジットカード情報

回避方法

// ❌ 避けるべき
logger.Info("user logged in", "password", password)

// ✅ 正しい
logger.Info("user logged in", "actor_id", actorID.String())

ログの集約と監視

開発環境

  • 出力先: stdout(JSON形式)
  • 確認方法: make compose/logs

本番環境

  • 出力先: OpenTelemetry → Grafana Cloud Loki
  • 可視化: Grafana Cloud
  • 検索: LogQLクエリ

Grafanaでのログ検索例

# エラーログを検索
{service="gateway"} | json | level="error"

# 特定のログIDで検索
{service="api"} | json | log_id="EVT_POST_003"

# 時間範囲で検索
{service="gateway"} | json | timestamp > now() - 1h

ログのベストプラクティス

1. 適切なログレベルの使用

  • デバッグ情報はDebugレベル
  • 正常系の重要な処理はInfoレベル
  • エラーはErrorレベル

2. 構造化ログの活用

  • キー・バリューペアで情報を構造化
  • 後で検索・集計しやすい形式

3. ログメッセージIDの使用

  • ログメッセージマスタで一元管理
  • ログIDで検索・集計が容易

4. コンテキスト情報の付与

  • リクエストID、トレースIDを付与
  • 分散トレーシングで全体像を把握

関連ドキュメント