C#入門編です。今回はC#におけるロギング(ログ出力)について解説します。

Console.WriteLineでデバッグしてるけど、本番環境ではどうすればいいの?」と思ったことはありませんか?

本記事は以下の方に役立つ内容となっています。

  • ロギングの基本概念を理解したい方
  • ILoggerの使い方を学びたい方
  • Serilogでファイル出力を実装したい方

ロギングは、アプリの動作状況を記録・追跡するための基盤技術です。開発中のデバッグはもちろん、本番環境での障害調査にも欠かせません。

今回は設定管理、DIに続き、C#モダン開発における以下の構成要素の1つであるロギングについて解説します。

  • 設定管理:appsettings.jsonとIConfigurationで設定を外部化
  • DI(依存性注入):依存関係を外から渡す設計手法
  • ログ:ILoggerによる構造化ログ(←今回紹介!)
  • Host:設定・DI・ログを統合するアプリの骨格
プロ太

セキュリティ監査のための証跡という意味でも、ログの保存が非常に重要ですね。

ロギングはDIと組み合わせて使うことが多いです。前回のDIの知識を活かしながら、一緒に学んでいきましょう!

演習コードをGitHubで公開してます。

講義1:ロギングとは

なぜログが必要か

開発中、動作確認のためにConsole.WriteLineを使ったことがある方は多いでしょう。

Console.WriteLine("ここまで来た");
Console.WriteLine($"値: {value}");

これは手軽ですが、以下のような問題があります。

  • 本番環境で使えない:本番サーバのコンソールを常に監視するわけにはいかない
  • 情報が残らない:プログラムを終了すると消える
  • 重要度がわからない:エラーも通常メッセージも同じ見た目

本番環境では、障害やセキュリティインシデントが発生したときに「いつ、何が起きたか」を後から調査できる必要があります。そのための仕組みがロギングです。

プロ美

たしかに、システムで何か問題が起きたとき「ログを見せて」って言われるよね。

ロギングの基本的な仕組み

ロギングは、プログラムの動作状況を記録する仕組みです。基本的な流れは以下の通りです。

  • ログを出力する:プログラム内で「ここで何が起きたか」を記録
  • 出力先を選ぶ:コンソール表示、ファイル/DBへ保存、クラウド上のログ収集基盤へ記録など
  • ログを確認する:問題が起きたとき、ログを見て原因を調査

C#では、ILoggerというインターフェイスを使ってログを出力します。

ILogger logger = ...; // ロガーを取得(後で詳しく説明)

logger.LogInformation("ユーザーがログインしました");
logger.LogError("データベース接続に失敗しました");

このとき、すべてのログを記録すると量が膨大になるため、「どのレベルのログを記録するか」を制御する必要があります。それが次で説明する「ログレベル」の概念です。

ログレベルの概念

ロギングでは、メッセージの重要度を「ログレベル」で分類します。.NETでは以下の6段階が定義されています。(参考:公式の定義

ベル用途(公式定義に準拠)
Trace最も詳細な情報。機密情報を含む可能性があり、本番環境では通常無効変数の値、メソッドの入出力
Debug開発中の調査・デバッグ用。処理の開始・終了、中間状態
Informationアプリの一般的な動作フロー。ユーザログイン、購入処理の完了
Warning異常または予期しない事象だが、アプリは継続実行可能設定が見つからずデフォルト使用
Error現在の処理フローが失敗により停止するが、アプリ全体は停止しないAPI呼び出し失敗(リトライ上限到達)、ファイル保存失敗
Critical回復不能な致命的障害。アプリまたはシステム停止レベル起動時のDB接続不可、設定ファイル破損(起動不能)

「Trace<…<Critical」の順で重要度が高くなります。Debug以下は長期保存を想定していません。Information以上についてはアプリ運用時に長期保存をします。

ログレベルを使い分けることで、本番環境ではInformation以上のみ出力し、問題調査時だけDebugを有効にする、といった制御ができます。

プロ太

ログレベルは「フィルタ」のようなものです。出力レベルを設定すれば、それより低いレベルのログは出力されません。

構造化ログとは

ログをファイルやログ収集基盤に出力するようになると、次にこんな課題に直面します。

  • エラーが多すぎて、欲しいログが見つけづらい
  • 特定のユーザの操作がみたいといった条件検索がしにくい
  • 後から集計・分析しようとすると、ログが単なる文字列で扱いづらい

これらの問題を解決するための考え方が「構造化ログ」です。

従来のログは単なる文字列でした。

2025-01-15 10:30:00 ユーザー user123 がログインしました

構造化ログでは、データをキーと値のペアで記録します。

{
  "Timestamp": "2024-01-15T10:30:00",
  "Level": "Information",
  "Message": "ユーザーがログインしました",
  "UserId": "user123",
  "Action": "Login"
}

構造化ログのメリットは以下の通りです。

  • 検索しやすい:`UserId = “user123″`で絞り込める
  • 集計できる:ログイン回数をユーザ別にカウント

.NETのILoggerは構造化ログをサポートしています。

プロ太

構造化ログの恩恵を受けるには、構造化ログに対応したログビューアが必須です。(例:Azure Monitor Application Insights、Seqなど)

講義2:ILoggerと標準ロギング

ILoggerとILoggerFactoryの役割

.NETには標準のロギング用の抽象化の仕組みが用意されています。主要なインターフェイスは以下の2つです。

インターフェイス役割
ILogger<T>実際にログを出力するインターフェイス
ILoggerFactoryILoggerのインスタンスを生成するファクトリ

これらはMicrosoft.Extensions.Logging名前空間にあります。

LoggerFactoryで基本的なコンソール出力

LoggerFactoryを使う例を見てみましょう。ここではILogger<T>ILoggerFactoryの実体として、Microsoft.Extensions.Loggingの標準で用意されたものを使います。

using Microsoft.Extensions.Logging;

// LoggerFactoryを作成(コンソール出力を設定)
using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddConsole();  // コンソールへの出力を追加
    builder.SetMinimumLevel(LogLevel.Debug);  // Debugレベル以上を出力
});

// ロガーを取得
var logger = loggerFactory.CreateLogger<Program>();

// ※ <Program> は「どのクラスから出力されたログか」を示すカテゴリ名になります。
//   LoggerFactory.CreateLogger<T>() とすると、
//   ログには T のクラス名が自動的に含まれます。

// 各レベルでログを出力
logger.LogTrace("これはTraceです(表示されない)");
logger.LogDebug("これはDebugです");
logger.LogInformation("これはInformationです");
logger.LogWarning("これはWarningです");
logger.LogError("これはErrorです");

実行すると、Debug以上のログがコンソールに出力されます。

プロ太

Program[0]の[0]はEventIdというログを識別子なのですが、今回は明示的に指定していないため、全て[0]となっています。

構造化ログの書き方

ILoggerでは、メッセージテンプレートを使って構造化ログを記録できます。

var userId = "user123";
var itemCount = 5;

// プレースホルダーを使用(構造化ログ)
logger.LogInformation("ユーザー {UserId} が {ItemCount} 件の商品を購入しました", userId, itemCount);

{UserId}{ItemCount}はプレースホルダーです。単なる文字列置換ではなく、キーと値のペアとして記録されます。

プロ太

文字列補間($"...")ではなく、プレースホルダーを使うのがポイントです。これにより構造化ログとして記録されます。

DIとの統合

前回学んだDIコンテナと組み合わせると、より実践的な形になります。DIやDIコンテナについては以下の記事を参考にしてください。

C#入門編(24)依存性注入(DI)を徹底解説 ~DIの本質からDIコンテナの使い方まで~ C#入門編です。今回はC#における依存性注入(DI: Dependency Injection)について解説します。 「依存性注...

サービスクラスでは、コンストラクタでILogger<T>を受け取ります。

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

    // DIでILoggerを受け取る
    public OrderService(ILogger<OrderService> logger)
    {
        _logger = logger;
    }

    public void ProcessOrder(string userId, int itemCount)
    {
        _logger.LogInformation("注文処理を開始します: ユーザー {UserId}", userId);

        // 注文処理...

        _logger.LogInformation("注文処理が完了しました: {ItemCount} 件", itemCount);
    }
}

以下のようにILogger、ILoggerFactory、OrderServiceの配線を行い、DIコンテナを作成します。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// DIコンテナの設定
var services = new ServiceCollection();

// ロギングをDIコンテナに登録
// (AddLoggingがロギングライブラリの拡張メソッドであり、内部でログイン関連の配線を行っている)
services.AddLogging(builder =>
{
    builder.AddConsole();
    builder.SetMinimumLevel(LogLevel.Debug);
});

// サービスを登録
services.AddTransient<OrderService>();

using var provider = services.BuildServiceProvider();

// サービスを取得して実行
var orderService = provider.GetRequiredService<OrderService>();
orderService.ProcessOrder("user123", 3);

実行すると、以下のようにログが表示されます。

info: OrderService[0]
      注文処理を開始します: ユーザー user123
info: OrderService[0]
      注文処理が完了しました: 3 件
プロ美

なるほど!こうやって、アプリの各サービスにロガーを注入して、それを使ってログ出力するんだね!

プロ太

その通りですね。ロガーの設定はコンポジションルートで行い、あとは配線しておけば、各サービスで自動で適切なロガーが注入されます。

標準ロギングの限界

これまでの例で使ってきた.NET標準のロギングには、以下のプロバイダ(出力先)が用意されています(参考)。

  • Console:コンソール出力
  • Debug:デバッグ出力(Visual Studioの出力ウィンドウなど)
  • EventSource:ETW(Event Tracing for Windows)
  • EventLog:Windowsイベントログ

実務ではファイル含め様々な出力先を使いたくなるのですが、標準ロギングでは対応している範囲が狭いという問題があります。

また、ログ出力時にマシン名・スレッドIDを必ず含めたいといったときに、標準ロギングだとカスタマイズがやや煩雑になります。

プロ太

標準ロギングは抽象レイヤー(ILogger・ILoggerFactory)が主な役目であり、具体的なロギング実装の機能は最小限なのです。

そこで、実務においてはサードパーティ製のライブラリを活用することも多いです。その代表格がSerilogです。

講義3:Serilog

Serilogとは?

Serilogは、.NETで最も人気のあるロギングライブラリの1つです。以下の特徴があります。

  • 豊富な出力先(Sink):ファイル、DB、クラウドサービスなど100以上
  • 構造化ログに最適化:最初から構造化ログを前提に設計
  • Enrichmentによる情報自動付加:スレッドID、マシン名、例外情報などを各ログに自動追加
  • ILoggerとの統合:Microsoft.Extensions.Loggingと連携可能

Sink(シンク)の概念

Serilogでは、ログの出力先を「Sink(シンク)」と呼びます。

Sinkパッケージ名用途
ConsoleSerilog.Sinks.Consoleコンソール出力
FileSerilog.Sinks.Fileファイル出力
SeqSerilog.Sinks.SeqSeqサーバーへ送信
ElasticsearchSerilog.Sinks.ElasticsearchElasticsearchへ送信

複数のSinkを同時に使うことで、「コンソールにも出力しつつファイルにも保存」といった構成が簡単に実現できます。

ILoggerとの統合

SerilogはSerilog.Extensions.Loggingパッケージを使うことで、.NET標準のILoggerインターフェイス経由で利用できます。

つまり、アプリコードはILoggerを使い続け、裏側でSerilogが動作する形になります。

これにより、将来的に別のロギングライブラリに切り替える場合も、アプリコードの変更は最小限で済みます。

プロ太

アプリがDIの考え方できちんと実装されていれば、コンポジションルートで少し配線を変えるだけで、Serilogへ切り替え可能です。

演習:ILoggerとSerilogを使ってみよう

それでは、Serilogを使ってコンソールとファイルの両方にログを出力するアプリを作ってみましょう。

前回のDIコンテナ演習で作成したコード(DataServiceとApiClient)に、ロギング機能を追加していきます。以下の手順で進めます。

  • 手順1:Serilog関連パッケージの追加
  • 手順2:Serilogの設定とDIコンテナへの登録
  • 手順3:DataServiceでILoggerを使う

手順1:Serilog関連パッケージの追加

以下のパッケージをプロジェクトに追加します。

  • Serilog.Extensions.Logging – Microsoft.Extensions.Loggingとの統合
  • Serilog.Sinks.Console – コンソールへのログ出力
  • Serilog.Sinks.File – ファイルへのログ出力(日次ローテーション機能付き)
  • Serilog.Enrichers.Environment – 環境情報付与のEnrichment
プロ太

Serilog本体、Sink(Console、File)、Enrichmentをそれぞれ追加します。

手順2:Serilogの設定とDIコンテナへの登録

Program.csでSerilogの設定を行い、DIコンテナに登録します。ファイル出力のときには、マシン名もログへ付与するように指定しています。

using Microsoft.Extensions.DependencyInjection;
using Serilog;
using DISample;

// Serilogの設定(この設定がDIで注入される全てのILogger<T>に引き継がれる)
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()  // Informationレベル以上を出力
    .Enrich.WithMachineName()  // ログにマシン名を追加
    .WriteTo.Console(outputTemplate:
        "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    .WriteTo.File("logs/app-.log",
        rollingInterval: RollingInterval.Day,  // 日付ごとにファイルを分割
        outputTemplate:
        "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [Machine: {MachineName}] {Message:lj}{NewLine}{Exception}")
    .CreateLogger();

// DIコンテナの設定
var services = new ServiceCollection();

// ILogger<T>でSerilogが使えるようにDIに登録(Log.Loggerの設定を使用)
services.AddLogging(builder =>
{
    //Dispose:trueでサービスプロバイダDispose時にSerilogもDispose
    builder.AddSerilog(dispose: true);  
});

// サービスの登録
// ここでMockApiClientに変更すれば、実装を簡単に差し替えられる
services.AddSingleton<IApiClient>(sp =>
    new ApiClient("https://api.example.com", 30));

services.AddTransient<DataService>();

// サービスプロバイダーの構築(usingで自動Dispose → Serilogがバッファをフラッシュ)
using var provider = services.BuildServiceProvider();

// サービスの取得と実行
var dataService = provider.GetRequiredService<DataService>();
dataService.Execute();

コンポジションルートで、Serilogの設定を含めた配線をすべて行ってDIコンテナを構築しています。

そして、DIコンテナからDataService(コンストラクタ引数のILoggerなど自動で解決される)を取得しています。

プロ太

Serilogの設定は、Fluent形式(メソッドチェーン)で今回記述しましたが、appsettings.jsonへ記述することも可能です。

設定の外部化については以下も参考にしてください。

C#入門編(23)モダンなC#の設定管理を徹底解説 ~appsettings.jsonとIConfigurationの使い方~ C#入門編です。今回はC#におけるモダンな設定管理の方法について解説します。 API の URL やアプリの動作モードなど、環境...

手順3:DataServiceでILoggerを使う

DataServiceクラスにILogger<DataService>を注入し、各処理でログを出力します。

using Microsoft.Extensions.Logging;

namespace DISample;

public class DataService
{
    private readonly IApiClient _client;
    private readonly ILogger<DataService> _logger;

    // DIでIApiClientとILoggerを受け取る
    public DataService(IApiClient client, ILogger<DataService> logger)
    {
        _client = client;
        _logger = logger;
    }

    public void Execute()
    {
        _logger.LogInformation("データ取得処理を開始します");

        try
        {
            var data = _client.GetData();
            _logger.LogDebug("APIからのレスポンス: {Response}", data);

            // 実際のアプリでは、取得したデータを加工・保存・表示などする
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 取得結果: {data}");

            _logger.LogInformation("データ取得処理が正常に完了しました");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "データ取得処理でエラーが発生しました");
            throw;
        }
    }
}
プロ太

コンストラクタで ILogger<DataService> を受け取ります。

ILogger<T> のジェネリック型引数にクラス名を指定することで、ログにクラス名が自動的に含まれます。

アプリを実行

アプリを実行すると、コンソールには以下のように表示されます。(2行目はロガーではなく、Console.Writelineで出力したものです)

[22:43:46 INF] データ取得処理を開始します
[22:43:46] 取得結果: [https://api.example.com] からデータ取得(タイムアウト: 30秒)
[22:43:46 INF] データ取得処理が正常に完了しました

実行ファイルのフォルダで「logs\app-20251229.log」といったログファイルも生成されていて、ロガーを使って出力した内容(マシン名も付与)が記録されています。

2025-12-29 23:01:47.694 [INF] [Machine: PROTA_MACHINE] データ取得処理を開始します
2025-12-29 23:01:47.758 [INF] [Machine: PROTA_MACHINE] データ取得処理が正常に完了しました
プロ美

これで、実行中はコンソールでリアルタイムにログをみて、あとでファイルで内容を確認するってこともできるね!

ログで出力する情報・出力先を簡単・柔軟にカスタマイズできるね!

プロ太

ログレベルを変えてみたり、Sinkをいろいろと試してみたりとすると理解が深まるかと思います!Serilogは実務でも役立ちます。

構造化ログの活用(柔軟なログの検索など)についても、例えばSeqなどのツールを使うと手軽に試せますよ!

まとめ

今回はロギングについて学びました。ロギングの基本として、Console.WriteLineの限界と、本番環境でのログの重要性を確認しました。

ログレベル(Trace~Critical)を使い分けることで、状況に応じた情報の出し分けが可能になります。

ILoggerと標準ロギングを用い、.NET標準のILoggerインターフェイスとDIを統合する方法を学びました。ただし、標準ロギングではカスタマイズ性に限界があります。

Serilogを使うことで、ファイル出力を含む豊富な出力先(Sink)が利用可能す。ILoggerインターフェイス経由で使えるため、アプリコードはロギングライブラリに依存しません。

演習ではSerilogを使い、SinkやEnrichmentを使った柔軟なロギングについて学びました。

次回は、設定管理・DI・ロギングを統合する「Host」について解説します。

プロ太

ロギングは地味ですが、本番運用では必須の技術です。

引き続き、モダンなC#アプリ開発の基盤を一緒に学んでいきましょう!

ABOUT ME
プロ太
ソフトウェア開発を楽しく、効率的に行う方法を追求しています。 開発者の視点から技術的課題に向き合い、「純粋な技術的興味に基づく探求」と「実践的な課題解決」という二つの柱を両輪として活動しています。