C#入門編(25)ロギングの基本 ~ILoggerの使い方とSerilogでファイル出力する方法~
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> | 実際にログを出力するインターフェイス |
ILoggerFactory | ILoggerのインスタンスを生成するファクトリ |
これらは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コンテナについては以下の記事を参考にしてください。
サービスクラスでは、コンストラクタで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 | パッケージ名 | 用途 |
|---|---|---|
| Console | Serilog.Sinks.Console | コンソール出力 |
| File | Serilog.Sinks.File | ファイル出力 |
| Seq | Serilog.Sinks.Seq | Seqサーバーへ送信 |
| Elasticsearch | Serilog.Sinks.Elasticsearch | Elasticsearchへ送信 |
複数の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へ記述することも可能です。
設定の外部化については以下も参考にしてください。
手順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#アプリ開発の基盤を一緒に学んでいきましょう!





