Windowsイベントログ・ETW入門 ── 業務アプリのログをOS標準の仕組みに乗せる

· · CSharp, .NET, イベントログ, ETW, EventSource, ログ設計, 不具合調査, Windows開発

C#で業務アプリやWindowsサービスを開発していると、たいてい自作のファイルロガーか、Serilog・NLogのようなライブラリで日々のログを出力しています。ではWindowsイベントログやETW(Event Tracing for Windows)はもう不要かというと、そうではありません。この2つは「ファイルログの代わり」ではなく、運用担当者やOS標準ツールから見える別レイヤーの記録です。

この記事では、ファイルログ・イベントログ・ETWの使い分けから、.NETでの実装方法、ETWの最小限の勘所、収集・調査の実務、そして現場で踏みがちな落とし穴までを整理します。

1. まず結論

  • 3つの手段は代替関係ではなく、役割分担です。 ファイルログは開発者が後から詳細を追うための記録、イベントログは運用担当者やOS標準のイベントビューアー・監視ツールが異常に気づくための記録、ETWは性能調査や再現困難な不具合の解析のためにオンデマンドで有効化する高頻度トレースです。基本は併用し、それぞれに書く情報量を変えます。
  • イベントログへは「起動・停止・致命的エラー」だけを書きます。 すべてのログをイベントログにも流すと、運用担当者が本当に見るべき異常が埋もれます。ファイルログの方は今まで通り詳細に出しつつ、イベントログは絞り込みます。
  • イベントソースの登録には管理者権限が必要です。 EventLog.CreateEventSource はWindows Vista以降、管理者権限がないと呼び出せません。実行時の初回アクセスで登録しようとする設計は避け、インストーラーで事前に登録しておきます。1
  • ETWは「常時収集」ではありません。 EventSource でイベントを定義しておいても、購読者(dotnet-trace、PerfView、ETWベースのツール)がいなければ何も記録されません。パフォーマンス分析や特定の不具合調査のときだけ、対象のプロバイダーを指定して収集するのが基本的な使い方です。2
  • メッセージは文字列連結や文字列補間ではなく、テンプレート形式で書きます。 logger.LogInformation("Order {OrderId} failed", orderId) のように書けば、構造化ログとしてOrderIdをプレースホルダー名付きで検索できます。文字列連結・補間で組み立てると、この構造情報が失われます。3
  • ログの肥大化は既定値の小ささが原因になりがちです。 イベントログの既定の最大サイズは512KBしかなく、業務アプリがイベントログを日常的に使うなら明示的に拡張し、上限に達したときの挙動(上書きするか、破棄するか)も設計時に決めます。4

以下は判断表です。

観点 ファイルログ イベントログ ETW
主な読者 開発者・保守担当 運用担当者、監視ツール、OS標準のイベントビューアー パフォーマンス調査担当、サポートエンジニア
出力量の目安 詳細(Trace/Debugも含めてよい) 少(起動・停止・致命的エラーのみ) 有効化時のみ大量。既定では何も記録されない
寿命・保持 ローテーション設定次第で長期保持しやすい ログサイズ上限に達すると古いものから上書き・破棄される セッション単位。収集し終えたら.etl/.nettraceファイルとして保存
OS標準ツールとの統合 なし(自前のビューアーが必要) イベントビューアー、wevtutilGet-WinEventで標準的に見られる dotnet-trace、PerfView、WPR/WPAで見る
権限 アプリの実行ユーザーの書き込み権限のみ ソース登録に管理者権限が必要(書き込み自体は登録済みソースなら標準ユーザーでも可) 収集開始に管理者権限が必要な場合が多い

2. Windowsイベントログの基礎

Windowsには既定でApplication・System・Securityの3つのログがあり、Securityは読み取り専用です。業務アプリやサービスはApplicationログ、またはアプリ独自のカスタムログ(チャネル)に書き込みます。デバイスドライバーはSystemログに書くのが慣例です。5

書き込みの単位は「イベントソース」です。ソースはアプリケーションを識別する名前で、EventLog.CreateEventSource で事前に登録します。Windows Vista以降、ソースの登録には管理者権限が必要です。 これは、ソース名の一意性を確認するためにSecurityログを含むすべてのイベントログを検索する必要があり、標準ユーザーはSecurityログへのアクセス権を持たないためです。1

using System.Diagnostics;

// インストーラーやセットアップ処理の中で、管理者権限がある間に実行する
const string SourceName = "KomuraSoft.OrderService";
const string LogName = "Application";

if (!EventLog.SourceExists(SourceName))
{
    EventLog.CreateEventSource(SourceName, LogName);
}

ソースを実行時の初回アクセスで作ろうとすると、アプリが標準ユーザーで動く運用ではこの呼び出しが失敗します。管理者権限がいつ必要になるかの一般論は「Windows の管理者特権が必要になるのはいつなのか」に整理していますが、イベントソースの登録もまさにこのパターンで、インストーラーで登録し、アプリ本体は登録済みのソースへ書き込むだけにするのが安全な設計です。

イベントには「レベル」と「イベントID」の2つの識別情報が付きます。レベルは EventLogEntryType(Information、Warning、Error、SuccessAudit、FailureAudit)で、Event Viewerのアイコンや既定のフィルターに使われます。イベントIDはアプリケーション定義の整数で、同じ種類の事象を後から検索・集計するためのキーになります。

3. .NETからイベントログへ書く

.NETからイベントログへ書く経路は大きく2つあります。

3.1 System.Diagnostics.EventLogを直接使う

低レベルなAPIで、細かい制御(カテゴリ、バイナリデータの添付など)が必要な場合に向いています。.NET(.NET Framework以外)では System.Diagnostics.EventLog NuGetパッケージの参照が必要です。

using System.Diagnostics;

public sealed class OrderServiceEventLog
{
    private const string SourceName = "KomuraSoft.OrderService";

    public void WriteServiceStarted()
    {
        EventLog.WriteEntry(SourceName, "OrderServiceを起動しました。",
            EventLogEntryType.Information, eventID: 1000);
    }

    public void WriteFatalError(Exception ex)
    {
        EventLog.WriteEntry(SourceName,
            $"致命的エラーが発生し、処理を継続できません。詳細はファイルログを確認してください。{ex.GetType().Name}: {ex.Message}",
            EventLogEntryType.Error, eventID: 1999);
    }
}

3.2 ILogger + AddEventLogを使う

すでに ILogger で構造化ログを組んでいるなら、Microsoft.Extensions.Logging.EventLog パッケージの AddEventLog を使うほうが既存のロギングパイプラインに素直に乗ります。ASP.NET CoreGeneric HostベースのWorker Serviceでは、Windows上で動く場合に既定でEventLogプロバイダーが有効になっています。6

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.AddEventLog(settings =>
{
    settings.SourceName = "KomuraSoft.OrderService";
    settings.LogName = "Application";
    // 既定のカテゴリフィルターに加えて、イベントログへは
    // Warning以上だけを流す(詳細はファイルログ側に任せる)
    settings.Filter = (_, level) => level >= LogLevel.Warning;
});

using IHost host = builder.Build();
await host.RunAsync();

EventLogSettings を省略した場合、LogName は既定で "Application"SourceName は既定で ".NET Runtime" になります。7 .NET Runtime という汎用的なソース名のままだと、自分のアプリのログか他の.NETアプリのログかがイベントビューアー上で区別しにくくなるため、SourceName は必ず自社アプリ用に明示してください。

Windowsサービスやタスクスケジューラで動くバッチ処理では、「起動」「停止」「致命的エラー」だけをイベントログに書き、それ以外の詳細はファイルログに任せるのが実務上のバランスです。サービスとしての作り方・運用は「Windowsサービスの作り方と運用」、タスクスケジューラ経由の実行トラブルは「タスクスケジューラのタスクが実行されない・0x1で終わる」で扱っています。どちらも、異常時に運用担当者が最初に見るのがイベントログであることが多く、ここに要点が残っているかどうかで初動調査の速度が変わります。

4. ETWとは何か ── OS全体を貫くトレース基盤

Event Tracing for Windows(ETW)は、カーネルからユーザーモードアプリケーションまでを共通の基盤で計装できるトレース機構です。カーネルイベント(ディスクI/O、プロセス生成など)とアプリケーションの独自イベントを、同じタイムライン上でまとめて記録・分析できるのが最大の特徴です。8

.NETでは System.Diagnostics.Tracing.EventSource を継承したクラスを作ることで、独自のETWプロバイダーを定義できます。ETWはPublish-Subscribe方式で動作し、購読者がいなければイベントは記録されません。つまり EventSource を実装しておくだけでは何も起きず、収集ツールで明示的に有効化して初めてイベントが記録されます。2

using System.Diagnostics.Tracing;

[EventSource(Name = "KomuraSoft-OrderService")]
internal sealed class OrderServiceEventSource : EventSource
{
    public static readonly OrderServiceEventSource Log = new();

    [Event(1, Level = EventLevel.Informational, Message = "注文処理を開始しました。OrderId={0}")]
    public void OrderProcessingStart(string orderId)
    {
        if (IsEnabled())
        {
            WriteEvent(1, orderId);
        }
    }

    [Event(2, Level = EventLevel.Informational, Message = "注文処理が完了しました。OrderId={0}")]
    public void OrderProcessingStop(string orderId)
    {
        if (IsEnabled())
        {
            WriteEvent(2, orderId);
        }
    }
}

呼び出し側は次のように使います。イベントを実際に書く前に IsEnabled() で有効かどうかを確認しておくと、収集していないときの余計なコストを避けられます。

OrderServiceEventSource.Log.OrderProcessingStart(order.Id);
try
{
    ProcessOrder(order);
}
finally
{
    OrderServiceEventSource.Log.OrderProcessingStop(order.Id);
}

このイベントを収集するには、クロスプラットフォームな dotnet-trace ツールを使うのが最も手軽です。9

dotnet-trace collect --providers KomuraSoft-OrderService -- OrderService.exe

収集した .nettrace ファイルはVisual StudioやPerfViewで開いて中身を確認します。より本格的なパフォーマンス調査、たとえばカーネルイベントまで含めた分析をしたい場合は、Windows Assessment and Deployment Kit(ADK)に含まれる Windows Performance Recorder(WPR)を使い、Windows Performance Analyzer(WPA)で見る構成になります。10 ただし、この記事の範囲は「ETWで何ができて、いつ使うか」までとし、PerfViewやWPAでの詳細な解析手順には立ち入りません。業務アプリの開発・保守という観点では、まず自分のプロバイダーをEventSourceで定義でき、dotnet-traceで収集できることまでを押さえれば十分です。

5. 構造化ログの実務

ILogger のメッセージテンプレートは、{PlaceHolder} という名前付きプレースホルダーを含む固定の文字列として書きます。これは単なる見た目の作法ではなく、テンプレート自体を変えずに引数だけを渡すことで、ログ基盤側がプレースホルダー名と値の対応関係(構造化データ)を保持できるという意味を持ちます。11

// 推奨: テンプレートは固定、引数は別に渡す
logger.LogWarning("Order {OrderId} の在庫確保に失敗しました。倉庫={WarehouseId}", orderId, warehouseId);

// 非推奨: 文字列連結・補間はテンプレートと値の対応関係を壊す
logger.LogWarning("Order " + orderId + " の在庫確保に失敗しました。倉庫=" + warehouseId);
logger.LogWarning($"Order {orderId} の在庫確保に失敗しました。倉庫={warehouseId}");

コードアナライザーのルール CA2254 は、この非推奨パターン(テンプレートが呼び出しごとに変わってしまう書き方)を検出してくれます。3 呼び出し頻度が高いホットパスでは、さらに LoggerMessageAttribute によるソース生成を使うと、ボクシングやテンプレート解析のコストを避けられます。12

using Microsoft.Extensions.Logging;

internal static partial class Log
{
    [LoggerMessage(
        EventId = 2001,
        Level = LogLevel.Warning,
        Message = "Order {OrderId} の在庫確保に失敗しました。倉庫={WarehouseId}")]
    public static partial void StockReservationFailed(
        this ILogger logger, string orderId, string warehouseId);
}

// 呼び出し側
logger.StockReservationFailed(orderId, warehouseId);

この構成なら、同じログ呼び出しから、ファイルログ(Serilog/NLogなどのシンク)には詳細を、イベントログには絞り込んだ内容を、ETWにはEventSourceLoggerProviderを通じて低コストなトレースを、それぞれ振り分けられます。ILoggerProvider を複数登録し、Filter でプロバイダーごとに出力レベルを変えるのが基本形です。自作ロガーの最小要件は「自作ロガーの最小要件と結合テストチェックリスト」、クラッシュ時にログとダンプをどう結び付けて残すかは「Windowsアプリのクラッシュ時にログとダンプを残す設計」を参照してください。

6. 収集と調査 ── 書いたログを読む側の話

イベントビューアーでは、ログ一覧から「フィルター」または「カスタムビュー」を使って、特定のソース・レベル・イベントIDだけを抽出できます。頻繁に使う条件はカスタムビューとして保存しておくと、次回以降の調査が速くなります。カスタムビューはXPathクエリで定義されており、たとえば特定のイベント名だけを抽出するクエリは次のような形になります。13

<QueryList>
  <Query Id="0" Path="Application">
    <Select Path="Application">
      *[System[Provider[@Name='KomuraSoft.OrderService'] and (Level=2 or Level=3)]]
    </Select>
  </Query>
</QueryList>

コマンドラインからは wevtutil でログのエクスポートや設定変更ができます。障害調査の一環として、発生時点前後のイベントログをエクスポートしてサポートへ送る、という運用にも使えます。14

:: Applicationログを丸ごとevtxファイルにエクスポートする
wevtutil epl Application C:\logs\application_20260707.evtx

:: 特定のソースのイベントだけを表示する(直近10件)
wevtutil qe Application /q:"*[System[Provider[@Name='KomuraSoft.OrderService']]]" /c:10 /rd:true /f:text

クラッシュ調査では、イベントログ・ファイルログ・クラッシュダンプの3つを時刻で突き合わせるのが基本です。イベントログに残った「致命的エラー」のタイムスタンプを起点に、同時刻のファイルログの詳細と、WER/ProcDumpで採取したダンプを照らし合わせます。ダンプの採取方法は「Windowsクラッシュダンプ収集入門 - WER/ProcDump/WinDbg」、採取したダンプの実際の解析手順は「WinDbg + SOSでクラッシュダンプを読む」にまとめています。

7. 落とし穴

  • ソース未登録のまま書こうとして失敗する。 登録されていないソース名で RegisterEventSource 相当の呼び出しを行うと、イベント自体はApplicationログに書き込まれますが、対応するメッセージリソースDLLがないため、イベントビューアーは説明文を表示できずエラー表示になります。15 さらに、実行時に CreateEventSource で自動登録しようとする設計では、非管理者ユーザーで動くプロセスの初回起動時に例外で落ちるという事故も起きがちです。ソース登録は必ずインストーラー側で完了させておきます。
  • 「別ソース名で書けてしまう」混乱。 イベントログは、書き込み権限さえあれば、アプリが登録した覚えのない別のソース名でも書き込めてしまいます。ソース名はコンピューター上で一意である必要がありますが、これを守る仕組みはOS側にはなく、アプリ側の規律に委ねられています。5 汎用的すぎるソース名(会社名や製品名を含まない短い名前)を選ぶと、他のベンダーのアプリと衝突したり、意図せず他アプリのソース名を流用してしまったりする原因になります。
  • ログの肥大化と保持ポリシー。 イベントログの既定の最大サイズはわずか512KBです。4 業務アプリが日常的にイベントログを使うなら、wevtutil sl やインストーラーでサイズを明示的に拡張し、上限に達したときに「古いものから上書きする」か「新しいものを破棄する」かを意識して設定してください。OverwriteOlder は非推奨化されており、指定しても実質的に「上書きしない」動作になり得る点にも注意が必要です。16
  • 多言語環境でのメッセージ表示。 ローカライズされたメッセージリソースファイル(MessageResourceFile)を使う構成にした場合、そのリソースDLLが存在しない・バージョンが違う環境でログを表示すると、「イベントID○○の説明を検出できません」という表示になります。転送されたイベントログを別サーバーで見る、あるいはアプリをアンインストールした後にログだけが残っている、といった場面で起こりやすい問題です。文字列を直接 WriteEntry で書く(リソースファイルを使わない)構成であれば、この種の問題自体を持ち込まずに済みます。
  • 権限不足で書けないケース。 誤解されがちですが、ソースの登録には管理者権限が必要な一方、登録済みのソースへの書き込みは標準ユーザーでも行えます。「管理者権限がないと動かない」と早合点してアプリ全体を管理者実行にする前に、実際にどの操作が権限を必要としているのかを切り分けてください。

関連記事

関連する相談領域

合同会社小村ソフトでは、Windows業務アプリのログ・イベントログ・ETW基盤の設計、障害調査を見据えた観測点設計の技術相談を扱っています。

参考リンク

  1. Microsoft Learn, EventLog.CreateEventSource Method. Windows Vista以降でイベントソースの作成に管理者権限が必要な理由(Securityログを含む全ログの検索が必要なため)について。  2

  2. Microsoft Learn, Getting Started with EventSource. EventSourceがPublish-Subscribeパターンで動作し、購読者(ETWやEventPipe)がいなければイベントが記録されないこと、dotnet-trace collectでの収集方法について。  2

  3. Microsoft Learn, CA2254: Template should be a static expression. ログメッセージのテンプレートに文字列連結や文字列補間を使うと、プレースホルダー名と値の対応関係が失われることについて。  2

  4. Microsoft Learn, EventLog.MaximumKilobytes Property. イベントログの既定の最大サイズが512キロバイトであることについて。  2

  5. Microsoft Learn, EventLog Class. Application/System/Securityの3つの既定ログ、ソースは1台のコンピューター上で一意である必要があること、書き込み権限があれば任意の登録済みソース名で書き込めてしまう仕組みについて。  2

  6. Microsoft Learn, Logging providers in .NET. Generic Hostの既定のロギングプロバイダーにConsole・Debug・EventSource・EventLog(Windowsのみ)が含まれることについて。 

  7. Microsoft Learn, EventLogSettings Class. AddEventLogの既定値(LogNameが”Application”、SourceNameが”.NET Runtime”)について。 

  8. Microsoft Learn, Event Tracing. Event Tracing for Windows(ETW)がアプリケーションのトレースイベントの開始・停止・計装・消費を行う仕組みであることについて。 

  9. Microsoft Learn, dotnet-trace performance analysis utility. EventPipeに基づくクロスプラットフォームなトレース収集ツールdotnet-traceの概要とcollectコマンドについて。 

  10. Microsoft Learn, Introduction to WPR. Windows Performance Recorder(WPR)がETWを拡張し、システムとアプリケーションの詳細な記録を提供するツールであることについて。 

  11. Microsoft Learn, Logging in C# and .NET. ログメッセージテンプレートの{PlaceHolder}構文と、テンプレートに含まれるキー名がログのプロパティ名になる仕組みについて。 

  12. Microsoft Learn, High-performance logging in .NET. LoggerMessageAttributeによるソース生成ログが、ボクシングやテンプレート解析のコストを避けられることについて。 

  13. Microsoft Learn, Comparison of ETW and EventLog logger functionality. イベントビューアーでXPathクエリを使ってイベント名などでカスタムビューを作成する方法について。 

  14. Microsoft Learn, wevtutil. イベントログのエクスポート(epl)、クエリ(qe)、設定変更(sl)などのコマンドライン操作について。 

  15. Microsoft Learn, Event Sources. 未登録のソース名で書き込むとApplicationログにフォールバックされるが、メッセージファイルがないためイベントビューアーが説明文を表示できずエラーになることについて。 

  16. Microsoft Learn, OverflowAction Enum. イベントログが上限サイズに達したときの挙動(上書き・破棄)と、OverwriteOlderが非推奨化されていることについて。 

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

ファイルログがあればイベントログは不要ですか?
詳細な調査用途ならファイルログだけで十分なことも多いです。ただし、運用担当者がOS標準のイベントビューアーや監視ツール(タスクスケジューラの失敗通知、SCOMなど)でアプリの異常に気づけるようにしたいなら、イベントログへの要点だけの出力を併用するのが実務的です。両者は代替関係ではなく、読者が違う別レイヤーの記録だと考えてください。
イベントIDの採番ルールはどう決めればよいですか?
決まった正解はありませんが、実務では『機能領域ごとに帯を分ける(起動系は1000番台、通信系は2000番台など)』『一度払い出したIDは意味を変えない・再利用しない』『欠番を許容する』という3点を守ると事故が減ります。ILoggerのEventIdは構造化ログの検索キーとしても使われるため、後からドキュメント化しやすい採番にしておくと保守時に役立ちます。
SerilogやNLogを使っていますが、この記事の内容とどう関係しますか?
SerilogやNLogは、ILoggerの下でファイル・イベントログ・ETW・外部SaaSなど複数のシンクへ同じログを振り分けるための実装の1つです。この記事で扱った『イベントログには要点だけ』『メッセージテンプレートを崩さない』という設計方針は、ILoggerを直接使う場合でもSerilog/NLog経由でも同じように当てはまります。イベントログ向けのシンク(Serilog.Sinks.EventLogなど)を使うか、標準のAddEventLogを使うかは実装上の選択です。
ETWはどこまで学べば十分ですか?
業務アプリの開発・保守が主目的であれば、EventSourceで独自プロバイダーを作れること、dotnet-traceで自分のプロバイダーのイベントを収集できることまで押さえれば十分です。PerfViewでのコールスタック解析やWPRのカーネルイベント収集は、パフォーマンス調査を専門にする段階で学べば足りる領域で、この記事ではあえて範囲外にしています。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る