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

Logging & Observability (ログと可観測性)

本番環境でアプリケーションがどのように動作しているかを把握するためには、適切なログ出力と可観測性(Observability)の確保が不可欠です。 Console.WriteLine はデバッグには手軽ですが、本番運用には適していません。ASP.NET Core は強力なログ抽象化機構 ILogger<T> を提供しています。

ILogger<T> の抽象化とDI

.NET のログシステムは、ログの「出力先(Sink/Provider)」と「ログ出力コード」を分離しています。 アプリケーションコードは ILogger<T> インターフェースに依存し、具体的な出力先(コンソール、ファイル、クラウドなど)は起動時の設定で決定します。

public class OrderService
{
private readonly ILogger<OrderService> _logger;

// DIコンテナからロガーを受け取る
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}

public void ProcessOrder(int orderId)
{
_logger.LogInformation("Processing order {OrderId}", orderId);

try
{
// 処理ロジック...
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
throw;
}
}
}

カテゴリ

ILogger<T>T はログの「カテゴリ」として使用されます。通常はクラス名を指定します。これにより、ログ出力時にどのクラスから出力されたかが明確になり、名前空間ごとのフィルタリングが可能になります。

構造化ログ (Structured Logging)

現代のログ管理において最も重要な概念が 構造化ログ です。 文字列連結でログメッセージを作成するのではなく、プレースホルダーとパラメータを使用します。

悪い例 (文字列連結):

// 検索しにくい、解析しにくい
_logger.LogInformation("User " + userId + " purchased item " + itemId);

良い例 (構造化ログ):

// テンプレートとデータを分離して保存
_logger.LogInformation("User {UserId} purchased item {ItemId}", userId, itemId);

構造化ログを使用すると、ログバックエンド(Application Insights, Elasticsearch, Datadogなど)は UserIdItemId を個別のフィールドとして保存します。 これにより、「ItemId が 123 のログを全て検索」といったクエリが高速かつ正確に行えるようになります。

ログレベルの適切な使い分け

ログレベルを適切に使い分けることで、重要な情報を見逃さず、かつログのノイズを減らすことができます。

レベルメソッド用途本番環境での扱い
TraceLogTrace最も詳細な情報。機密情報を含む可能性がある詳細なペイロードなど。通常は無効
DebugLogDebug開発中のデバッグに有用な情報。フローの確認など。通常は無効
InformationLogInformationアプリケーションの正常な動作フロー。リクエストの開始/終了、主要な操作の完了。有効 (標準)
WarningLogWarningエラーではないが注意が必要な状況。リトライ発生、非推奨APIの使用など。有効
ErrorLogError現在のフローが失敗したエラー。例外発生時など。有効 (要監視)
CriticalLogCriticalアプリケーション全体がクラッシュするような致命的なエラー。有効 (即時対応)

appsettings.json でレベルごとのフィルタリングを設定できます。

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

Serilog との統合

ASP.NET Core 標準のロガーも優秀ですが、ファイル出力や高度な構造化ログ機能(Enrichmentなど)を利用するために、サードパーティライブラリである Serilog が広く使われています。

導入手順

  1. NuGet パッケージのインストール

    • Serilog.AspNetCore
  2. Program.cs での設定

using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Serilog の設定
builder.Host.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration) // appsettings.json から設定を読み込む
.ReadFrom.Services(services)
.Enrich.FromLogContext() // コンテキスト情報を含める
.WriteTo.Console()); // コンソールに出力

var app = builder.Build();

// リクエストログの出力 (HTTPリクエストの情報を詳細に記録)
app.UseSerilogRequestLogging();

app.Run();
  1. appsettings.json での設定
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": { "path": "logs/log-.txt", "rollingInterval": "Day" }
}
]
}
}

Serilog の利点

  • Sinks: ファイル、データベース、クラウドサービスなど、豊富な出力先プラグインがある。
  • Enrichers: ログに自動的にコンテキスト情報(スレッドID、マシン名、ユーザーIDなど)を付与できる。
  • Request Logging: UseSerilogRequestLogging() を使うことで、ASP.NET Core 標準のリクエストログよりも簡潔で情報量の多いログを出力できる。

ベストプラクティスまとめ

  1. ILogger<T> を使用する: Console.WriteLine は使用しない。
  2. 構造化ログを徹底する: 文字列連結ではなく、プレースホルダー {ParamName} を使用する。
  3. 適切なログレベルを設定する: Information を基準とし、ノイズが多いライブラリは Warning 以上にする。
  4. 例外は LogError で記録する: 例外オブジェクトを第一引数に渡すことで、スタックトレースが正しく記録される。
    • _logger.LogError(ex, "Message...");
  5. 構造化ログ対応のバックエンドを使用する: ログをテキストファイルとしてgrepするのではなく、クエリ可能なストア(Application Insights, Seq, ELK Stackなど)に送ることを検討する。