はじめに:例外ログ共通化は「どこで落ちても、同じ形で記録する」ための仕組み
業務システムが大きくなると、あちこちで例外をログに書くコードが散らばりがちになります。catch (Exception ex) { logger.LogError(ex, "エラー"); } が、サービス層にもコントローラにもバッチにも点在していくイメージです。
そうなると、ログのフォーマットがバラバラになり、
「どのログがどの画面・どの処理のものか」が追いづらくなります。
そこで効いてくるのが 例外ログの共通化 です。
「例外が発生したときのログ出力」を、ひとつの場所(ユーティリティや共通ハンドラ)に集約してしまう考え方です。
ここでは、初心者向けに
例外ログ共通化の狙い
ILogger を使った共通メソッドの形
ASP.NET Core でのグローバル例外ハンドリング(ミドルウェア)
コンソールアプリやバッチでの共通ハンドラ
を、例題付きでかみ砕いて説明します。
例外ログ共通化の狙いを整理する
「どこで落ちても、同じ情報が必ず残る」状態を作る
例外ログを共通化する目的は、ざっくり言うと次のようなものです。
どの層で例外が起きても、最低限必要な情報(日時、場所、例外の種類、スタックトレースなど)が必ず残る
ログのフォーマットが統一され、後から検索・分析しやすくなる
各所で同じような try-catch-LogError を書かなくて済む
特に重要なのは、「例外ログの“抜け漏れ”を防ぐ」という視点です。
画面 A ではログを書いているのに、画面 B では書き忘れていた、という状態を避けたいわけです。
そのために、「例外が最終的にたどり着く場所」を決めて、
そこで必ずログを書くようにします。
まずは小さな共通メソッドから始める
ILogger を受け取って「例外を標準フォーマットで書く」メソッド
一番シンプルな共通化は、「例外をログに書くときのフォーマット」をひとつに決めることです。
ILogger を使って、こんなユーティリティを用意してみます。
using Microsoft.Extensions.Logging;
public static class ExceptionLogger
{
public static void LogException(ILogger logger, Exception ex, string contextMessage)
{
// contextMessage には「どの処理中か」を入れる
logger.LogError(
ex,
"{ContextMessage} | ExceptionType={ExceptionType}",
contextMessage,
ex.GetType().FullName);
}
}
C#使う側は、こう書くだけです。
try
{
DoSomething();
}
catch (Exception ex)
{
ExceptionLogger.LogException(_logger, ex, "ユーザー登録処理中にエラー");
throw; // 必要なら再スロー
}
C#ここでのポイントは、「ログのメッセージテンプレートを共通化している」ことです。ContextMessage や ExceptionType のようなキー名を統一しておくと、後でログ検索やダッシュボード作成がしやすくなります。
ASP.NET Core でのグローバル例外ハンドリングと共通ログ
ミドルウェアで「アプリ全体の最後の砦」を作る
Web API や Web アプリでは、グローバル例外ハンドラ(ミドルウェア) を使って、
「どこで例外が起きても最後にここを通る」というポイントを作るのが定番です。
シンプルな例外ハンドリングミドルウェアは、こんな形になります。
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
ExceptionLogger.LogException(_logger, ex, $"Unhandled exception. Path={context.Request.Path}");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsync("An error occurred.");
}
}
}
C#登録は Program.cs などで行います。
app.UseMiddleware<ExceptionHandlingMiddleware>();
C#これで、コントローラやサービス層でキャッチされなかった例外は、
最終的にこのミドルウェアに到達し、必ず共通フォーマットでログが書かれます。
ここでの重要ポイントは、「“最後の砦”で必ず例外ログを書く場所をひとつ決める」ことです。
個々のコントローラでログを書き忘れても、最悪ここで拾えます。
コンソールアプリ・バッチでの「メイン関数の共通ハンドラ」
Main の一番外側で例外をまとめてログに書く
バッチやコンソールツールでは、Main の一番外側で例外をキャッチして、
共通の例外ログを書いてから終了する、というパターンが使えます。
using Microsoft.Extensions.Logging;
class Program
{
static int Main(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
// 必要ならファイルや他のプロバイダも追加
});
var logger = loggerFactory.CreateLogger<Program>();
try
{
Run(args, logger);
return 0;
}
catch (Exception ex)
{
ExceptionLogger.LogException(logger, ex, "Unhandled exception in batch");
return 1;
}
}
static void Run(string[] args, ILogger logger)
{
// ここから先は、必要なところだけ個別に try-catch
// キャッチされなかったものは Main でまとめてログ
}
}
C#こうしておくと、「どこかで取りこぼした例外」があっても、
必ず最後に共通フォーマットでログが残ります。
ここでの重要ポイントは、「アプリの“入口”または“出口”で、例外ログの共通ハンドラを置く」ことです。
Web ならミドルウェア、バッチなら Main、というイメージです。
例外ログ共通化で「必ず残したい情報」を決める
何をログに入れるかを先に設計しておく
共通化するなら、「例外ログには必ずこれを入れる」という項目を決めておくと強いです。
例えば、次のような情報が典型的です。
日時(これはロガー側が自動で付けてくれることが多い)
例外の型(ex.GetType().FullName)
メッセージ(ex.Message)
スタックトレース(ex.ToString() を渡せば含まれることが多い)
処理の文脈(どの画面・どの API・どのバッチ・どの ID か)
ILogger の構造化ログを使うと、キー付きで残せます。
public static void LogException(ILogger logger, Exception ex, string context, string? userId = null)
{
logger.LogError(
ex,
"Unhandled exception. Context={Context}, UserId={UserId}, ExceptionType={ExceptionType}",
context,
userId ?? "(unknown)",
ex.GetType().FullName);
}
C#ここでの重要ポイントは、「“例外ログのフォーマット”をチームで合意し、それをユーティリティに閉じ込める」ことです。
あとからログ分析基盤(Elasticsearch、Application Insights など)に流すときにも、この一貫性が効いてきます。
例外ログ共通化と「例外ラップ」「再スロー」の関係
ラップや再スローと組み合わせて、責任の流れをきれいにする
これまで話してきた他のテクニックとも、例外ログ共通化は相性が良いです。
リポジトリ層
低レベル例外を業務例外にラップする(例外ラップ)。
サービス層
業務例外をキャッチして、文脈付きでログを書き、必要なら再スロー。
最上位層(ミドルウェアや Main)
キャッチされなかった例外を共通ハンドラでログ。
例えば、サービス層ではこう書けます。
public void ProcessOrder(int orderId)
{
try
{
_orderRepository.Save(orderId);
}
catch (OrderRepositoryException ex)
{
ExceptionLogger.LogException(_logger, ex, $"注文処理中のリポジトリエラー。OrderId={orderId}");
throw; // さらに上(UI やミドルウェア)に任せる
}
}
C#最上位のミドルウェアや Main では、
「Unhandled exception」として共通ログを書きます。
ここでの重要ポイントは、「例外ラップで“意味付け”、再スローで“責任のバトンリレー”、共通ログで“記録の一貫性”」という役割分担を意識することです。
まとめ:例外ログ共通化は“落ち方がバラバラでも、記録の形は揃える”ための基盤
例外ログ共通化の本質は、
アプリのどこで例外が起きても
最低限必要な情報が
同じフォーマットで必ず残るようにする
ことです。
押さえておきたいポイントをコンパクトにまとめると、こうなります。
例外ログのフォーマット(含める項目)を先に決めて、ユーティリティに閉じ込める。
Web ならミドルウェア、バッチなら Main など、「最後の砦」で必ず共通ログを書く。
個々の層では、必要に応じて文脈付きでログを書きつつ、取りこぼしは最上位で拾う。
ILogger の構造化ログを使うと、後からの検索・分析が圧倒的に楽になる。
ここまでイメージできていれば、
「その場その場でバラバラに LogError する」段階から抜けて、
“例外ログがきれいに揃ったシステム”を設計できるようになります。
