ロギング戦略

Clueを使用したロギングの設定

OpenTelemetryはClueにおける可観測性の主要なソースですが、ログは特定のシナリオで 重要な役割を果たします。Clueは、ログメッセージを効率的にバッファリングし フォーマットするスマートなロギングシステムを提供し、コストとパフォーマンスを 制御しながら可視性を維持するのに役立ちます。

主要な機能

  1. スマートバッファリング: Clueのスマートバッファリングシステムは、ロギングのコストとパフォーマンスの 最適化に役立ちます。エラー以外のログをメモリにバッファリングし、エラーが 発生した時にバッファをフラッシュしてエラーの周辺の完全なコンテキストを提供します。 トレースされたリクエストの場合、リクエストのライフサイクルの完全な可視性を 確保するためにログは自動的にフラッシュされます。また、特定のシナリオでログ出力を 強制する必要がある場合に、手動でフラッシュを制御することもできます。柔軟性を 維持するために、バッファリングの動作はコンテキストに基づいて設定でき、 異なる状況に応じてロギングパターンを適応させることができます。

  2. 構造化ロギング: Clueは、ログをより有用で保守しやすくするために構造化ロギングを使用します。 すべてのログフィールドはキーと値のペアとして保存され、ロギングツールで簡単に 解析および分析できることを保証します。すべてのログで一貫したフォーマットを 使用することで、監視および分析システムとの統合が改善されます。ログは環境の ニーズに応じてJSONやプレーンテキストなど異なる形式で出力できます。 さらに、コンテキストベースのフィールドが自動的に含まれ、ログをリクエストの トレースと相関付けることで、分散システム全体での問題のデバッグが容易になります。

  3. パフォーマンス: Clueのロギングシステムはパフォーマンスを考慮して設計されています。 I/Oオーバーヘッドを最小限に抑えるために効率的なバッファリング技術を使用し、 アロケーションオーバーヘッドを低く抑えるためにスマートなメモリ管理を採用しています。 システムは環境のニーズに基づいて異なる方法でログを出力するように設定できます。 また、条件付きロギングを通じてログ量を制御し、必要なログのみを生成することを 確保できます。

基本セットアップ

ロガーは使用前に設定する必要があります。以下は一般的なオプションでロガーを セットアップする方法です:

// オプション付きでロガーコンテキストを作成
ctx := log.Context(context.Background(),
    // トレースとの相関のためにログにスパンIDを含める
    log.WithFunc(log.Span),

    // 機械可読性のためにJSONフォーマットを使用
    log.WithFormat(log.FormatJSON),

    // 標準出力にログを送信
    log.WithOutput(os.Stdout),

    // リクエストがトレースされている場合はバッファリングを無効化
    log.WithDisableBuffering(log.IsTracing))

// すべてのログに含まれる共通フィールドを追加
ctx = log.With(ctx, 
    log.KV{"service", "myservice"},  // フィルタリング用のサービス名
    log.KV{"env", "production"})     // コンテキスト用の環境

この設定により、サービスの堅牢なロギング基盤が確立されます。ログにスパンIDを 含めることで、ログエントリを分散トレースと簡単に相関付けることができ、システム 全体でのリクエストフローの完全な全体像を把握できます。JSONフォーマットにより、 ログ集約および分析ツールでログを効率的に処理できることが保証されます。

ローカル開発の利便性のため、ログは端末で簡単に表示できるように標準出力に 出力されます。スマートバッファリングシステムは、リクエストがトレースされているかどうかに 基づいて自動的に調整され、パフォーマンスと可観測性の両方を最適化します。

最後に、セットアップには、すべてのログエントリに追加される共通フィールドが含まれ、 フィルタリングと分析のための一貫したコンテキストを提供します。サービス名や環境などの これらのフィールドにより、問題を調査する際に各ログエントリのソースとコンテキストを 簡単に特定できます。

ログレベル

Clueは4つの重要度レベルをサポートし、それぞれが特定の目的を持ち、異なるバッファリング ルールに従います:

// デバッグレベル - 詳細なトラブルシューティング用
// WithDebugを介してデバッグモードが有効な場合のみ出力
log.Debug(ctx, "リクエストの詳細",
    log.KV{"headers", req.Headers},
    log.KV{"body_size", len(req.Body)})

// 情報レベル - 通常の操作用
// これらのログはデフォルトでバッファリングされ、エラー時にフラッシュ
log.Info(ctx, "リクエストを処理中",
    log.KV{"requestID", req.ID},
    log.KV{"method", req.Method})

// 警告レベル - 潜在的な問題用
// これらのログは操作を妨げない問題を示す
log.Warn(ctx, "リソース使用量が高い",
    log.KV{"cpu_usage", cpuUsage},
    log.KV{"memory_usage", memUsage})

// エラーレベル - 失敗条件用
// これらのログは即座に書き込まれ、バッファをフラッシュ
log.Error(ctx, err, "リクエストが失敗",
    log.KV{"requestID", req.ID},
    log.KV{"status", http.StatusInternalServerError})

// 致命的レベル - 回復不可能なエラー用
// これらのログはログ記録後にプログラムを終了
log.Fatal(ctx, err, "サーバーを起動できません",
    log.KV{"port", config.Port},
    log.KV{"error", err.Error()})

各レベルにはprintf形式のフォーマットを受け入れる対応するバージョンもあります:

// フォーマット付きデバッグ
log.Debugf(ctx, "アイテム %d / %d を処理中", current, total)

// フォーマット付き情報
log.Infof(ctx, "リクエストが %dms で完了", duration.Milliseconds())

// フォーマット付き警告
log.Warnf(ctx, "高レイテンシーを検出: %dms", latency.Milliseconds())

// エラーオブジェクトとフォーマット付きエラー
log.Errorf(ctx, err, "リクエストの処理に失敗: %s", req.ID)

// エラーオブジェクトとフォーマット付き致命的エラー
log.Fatalf(ctx, err, "初期化に失敗: %s", component)

ログレベルのベストプラクティス:

  1. DEBUG (SeverityDebug):

    • 詳細なトラブルシューティング情報に使用
    • デバッグモードが有効な場合のみ出力
    • 開発とデバッグセッションに最適
    • 詳細なリクエスト/レスポンスデータを含めることが可能
  2. INFO (SeverityInfo):

    • 監査が必要な通常の操作に使用
    • パフォーマンスを最適化するためにデフォルトでバッファリング
    • 重要だが予期される事象を記録
    • ビジネス関連の情報を含める
  3. WARN (SeverityWarn):

    • 潜在的に有害な状況に使用
    • 操作を妨げない問題を示す
    • リソース制限に近づいていることを強調
    • 非推奨機能の使用をフラグ付け
  4. ERROR (SeverityError):

    • 注意が必要な任意のエラー条件に使用
    • 自動的にログバッファをフラッシュ
    • エラーの詳細とコンテキストを含める
    • 利用可能な場合はスタックトレースを追加
  5. FATAL (SeverityError + Exit):

    • 回復不可能なエラーにのみ使用
    • ステータス1でプログラムを終了
    • 事後分析のための関連コンテキストをすべて含める
    • 慎重に使用 - ほとんどのエラーは回復可能であるべき

特別な動作:

  • デバッグログはデバッグモードが有効な場合のみ出力
  • 情報ログはデフォルトでバッファリング
  • 警告ログは注意が必要な問題を示す
  • エラーログはバッファをフラッシュしバッファリングを無効化
  • 致命的ログはエラーと同様だがプログラムも終了

カラーコード(端末フォーマット使用時):

  • デバッグ:グレー (37m)
  • 情報:青 (34m)
  • 警告:黄 (33m)
  • エラー/致命的:明るい赤 (1;31m)

構造化ロギング

構造化ロギングにより、ログの解析と分析が容易になります。以下は異なる方法で ログを構造化する方法です:

// 順序付きキーと値のペアにlog.KVを使用
// フィールドの順序を維持するため、これが推奨される方法
log.Print(ctx,
    log.KV{"action", "user_login"},      // 何が起きたか
    log.KV{"user_id", user.ID},          // 誰に起きたか
    log.KV{"ip", req.RemoteAddr},        // 追加のコンテキスト
    log.KV{"duration_ms", duration.Milliseconds()})  // パフォーマンスデータ

// マップスタイルのロギングにlog.Fieldsを使用
// 既存のデータマップを扱う場合に有用
log.Print(ctx, log.Fields{
    "action":     "user_login",
    "user_id":    user.ID,
    "ip":         req.RemoteAddr,
    "duration_ms": duration.Milliseconds(),
})

// 以降のすべてのログに含まれるコンテキストフィールドを追加
// リクエストスコープの情報に有用
ctx = log.With(ctx,
    log.KV{"tenant", tenant.ID},     // マルチテナントコンテキスト
    log.KV{"region", "us-west"},     // 地理的コンテキスト
    log.KV{"request_id", reqID})     // リクエストの相関

構造化ロギングのベストプラクティス:

  • アプリケーション全体で一貫したフィールド名を使用
  • 圧倒的な詳細を避けつつ関連するコンテキストを含める
  • 関連するフィールドを論理的にグループ化
  • フィールド名を選択する際にログの解析と分析を考慮

出力フォーマット

環境とツールに合わせて出力フォーマットを選択:

// プレーンテキストフォーマット(logfmt)
// ローカル開発と人間の可読性に最適
ctx := log.Context(context.Background(),
    log.WithFormat(log.FormatText))
// 出力: time=2024-02-24T12:34:56Z level=info msg="hello world"

// カラー付き端末フォーマット
// ローカル開発とデバッグに最適
ctx := log.Context(context.Background(),
    log.WithFormat(log.FormatTerminal))
// 出力: INFO[0000] msg="hello world"

// JSONフォーマット
// 本番環境とログ集約システムに最適
ctx := log.Context(context.Background(),
    log.WithFormat(log.FormatJSON))
// 出力: {"time":"2024-02-24T12:34:56Z","level":"info","msg":"hello world"}

// カスタムフォーマット
// 特別なフォーマットが必要な場合に使用
ctx := log.Context(context.Background(),
    log.WithFormat(func(entry *log.Entry) []byte {
        return []byte(fmt.Sprintf("[%s] %s: %s\n",
            entry.Time.Format(time.RFC3339),
            entry.Level,
            entry.Message))
    }))
// 出力: [2024-02-24T12:34:56Z] info: hello world