C# - Windows イベントログ

## はじめに

仕事でトラブル解析を行う際、イベントログを確認することがありました。
本記事では、まず、Windows イベントログのしくみ等について説明して、C# での操作について記載します。
最後に、固有のメッセージ DLL を利用する形態について記載します。

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
記載したソースコードは .NET 8 ベースとしています。
.NET Framework 4.8 の場合は、コメントで記載している null 許容参照型の明示 ? を削除してください。
:::

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::

## Windows イベントログ

Windows イベントログは、Windows としてのログ管理機構で、システムやアプリケーションの動作、セキュリティイベントなどを記録する役割を持っています。
この情報を確認することで、システム障害や不正アクセスなどの事象を、時系列で確認できます。

### ログファイル

ログファイルは、EVTX 形式（Windows XML Event Log Format）のファイルとして保存されます。
EVTX 形式は、バイナリ形式で圧縮された XML を格納しており、ファイル拡張子は .evtx で、以下のフォルダに格納されています。

```
C:\Windows\System32\winevt\Logs
```

::: note
EVTX 形式の詳細については、[GitHub - libyal/libevtx/documentation/Windows XML Event Log](https://github.com/libyal/libevtx/blob/main/documentation/Windows%20XML%20Event%20Log%20%28EVTX%29.asciidoc) をご確認ください。
:::


### 主なログの種類

| ログ名        | 説明                                       |
| ------------- | ----------------------------------------- |
| Application   | アプリケーションの動作ログ                   |
| Security      | ログオン、アクセス制御などのセキュリティ関連のログ |
| System        | OS やドライバのログ                         |
| Setup         | インストールやアップグレードのログ            |
| ForwardedEvents | 他の PC から転送されたログ                |

上記以外にも、イベントログ格納フォルダには多数の EVTX形式ファイルが存在し、それぞれ異なるイベントログを記録しています。

### 格納情報

イベントログには、下記情報が格納されます。

| 項目          | 内容                               |
| ------------- | ----------------------------------|
| レベル        | イベントの重要度（エラー/警告/情報など）   |
| 日付と時刻     | イベントの発生日時                  |
| ソース        | イベントソース                    |
| イベントID    | イベント ID                       |
| ユーザー      | イベントを発生させたユーザー          |
| コンピュータ   | イベントを発生させたコンピュータ      |
| イベントデータ | イベント詳細情報                        |


### 確認方法

イベントログは以下のツールで確認できます。

- イベントビューア（eventvwr.exe）
  - GUI を持ったツールで、閲覧、検索、各種ファイル形式（ .evtx / .xml / .txt / .csv ）へのエクスポートなどが可能です
- [wevtutil.exe](https://learn.microsoft.com/ja-jp/windows-server/administration/windows-commands/wevtutil)
  - コマンドラインでイベントログに対する各種操作ができます
  - Application ログの最新 10件をテキスト出力
`wevtutil qe Application /c:10 /rd:true /f:text`
  - Application ログのクリア（管理者権限が必要）
`wevtutil cl Application` 


## C# 基本操作

ここでは、固有のメッセージ DLL を利用しないケースについて記載します。
サンプルとして、Application イベントログの foobarbaz イベントソースを用います。

### イベントソース登録

イベントソース登録は管理者権限が必要です。
インストーラで登録する場合は、レジストリに直接登録することが一般的でしょうが、C# での手法を記載します。

https://learn.microsoft.com/ja-jp/dotnet/api/system.diagnostics.eventlog.createeventsource

```csharp
string source = "foobarbaz";    // イベントソース名
string logName = "Application"; // ログの種類（Application ログ）

// イベントソース存在確認 - 管理者権限が必要
if (!EventLog.SourceExists(source))
{
  // イベントソース登録 - 管理者権限が必要
  EventLog.CreateEventSource(source, logName);
  // 下記レジストリに登録
  // HKLM\SYSTEM\CurrentControlSet\Services\EventLog\Application\foobarbaz
  // メッセージDLL 格納変数(EventMessageFile)を作成して既定値がセットされる
  // 削除せず、そのままとする
}
```

### イベントログ出力

https://learn.microsoft.com/ja-jp/dotnet/api/system.diagnostics.eventlog.writeentry

```csharp
private void EventLoggingDirect(int eventId, EventLogEntryType type, string message)
{
  string source = "foobarbaz";    // イベントソース名
//string logName = "Application"; // ログの種類（Application ログ）

  try
  {
    // イベントログにメッセージ出力 - Application ログは管理者権限不要
    EventLog.WriteEntry(source, message, type, eventId);
  }
  catch (SecurityException)
  {
    // TODO - 対象イベントソースが存在しないため、メッセージ出力エラー
    Console.WriteLine("対象イベントソースが存在しません。");
  }
}
```

以下のように利用します。

```csharp
EventLoggingDirect(10, EventLogEntryType.Information, "サンプルです");
```

### イベントログ取得

前述、イベントソース foobarbaz について、日時が本日で最新 10件を取得するコードを記載します。

```csharp
string source = "foobarbaz";     // イベントソース名
string logName = "Application";  // ログの種類（Application ログ）
DateTime today = DateTime.Today; // 本日の日付（時刻は 00:00:00）

// イベントログ取得 - Application ログは管理者権限不要
// 当日で、イベントソースが foobarbaz の最新 10件にフィルタリング
EventLog eventLog = new EventLog(logName);
var filteredEntries = eventLog.Entries.Cast<EventLogEntry>()
        .Where(entry => entry.TimeGenerated.Date == today)  // 先に日付フィルタを適用
        .Where(entry => entry.Source == source)             // 次にソースフィルタを適用
        .OrderByDescending(entry => entry.TimeGenerated)
        .Take(10);

foreach (var entry in filteredEntries)
{
  Console.WriteLine($"イベント ID: {entry.InstanceId}");
  Console.WriteLine($"レベル: {entry.EntryType}");
  Console.WriteLine($"ソース: {entry.Source}");
  Console.WriteLine($"日時: {entry.TimeGenerated}");
  Console.WriteLine($"メッセージ: {entry.Message}");
  Console.WriteLine("----------------------------");
}

// リソース解放
eventLog.Close();
```

## メッセージ DLL

イベントソース固有のメッセージ DLL を登録することで、より柔軟なメッセージ管理が可能となります。
メッセージ DLL が登録されている場合、対象イベントソースのイベント ID に関するメッセージはメッセージ DLL から取得します。
このメカニズムを採用することで、以下のようなメリットがあります。

- 多言語対応
  - メッセージ DLL に複数の言語向けのメッセージを格納することで、異なる言語環境でも適切な情報表示が可能
- メッセージの更新が容易
  - アプリケーションのアップデート時にメッセージ DLL を更新することで、表示メッセージも更新可能

メッセージ DLL は、イベントログファイルとイベントソースに関係づけて、レジストリで管理されています。
例えば、Application イベントログのイベントソース Outlook の場合、以下のレジストリ項目となります。

```
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\Application\Outlook
→ EventMessageFile がメッセージ DLL です
```

::: note
レジストリ登録情報は、[Event Sources - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/eventlog/event-sources) で確認できます。
:::


### メッセージファイル

多言語対応やメッセージ更新時の柔軟性を確保するため、メッセージ DLL では、イベント ID に関するメッセージに対してプレースフォルダを利用できます。
以下に、英語と日本語のメッセージを定義し、プレースフォルダも利用したメッセージファイル（メッセージ DLL の元となるデータ）のサンプルを提示します。


```text:HogeHoge.mc
MessageIdTypedef=DWORD

SeverityNames=(Success=0x0:STATUS_SEVERITY_SUCCESS
Informational=0x1:STATUS_SEVERITY_INFORMATIONAL
Warning=0x2:STATUS_SEVERITY_WARNING
Error=0x3:STATUS_SEVERITY_ERROR
)

FacilityNames=(System=0x0:FACILITY_SYSTEM
Runtime=0x2:FACILITY_RUNTIME
Stubs=0x3:FACILITY_STUBS
Io=0x4:FACILITY_IO_ERROR_CODE
)

LanguageNames=(English=0x409:MSG00409)
LanguageNames=(Japanese=0x411:MSG00411)

MessageId=501
Severity=Error
Facility=Io
SymbolicName=ERROR_WORKFILECREATE
Language=English
Failed to create work file.
.
Language=Japanese
ワークファイルの作成に失敗しました。
.

MessageId=502
Severity=Informational
Facility=Runtime
SymbolicName=INFO_PLACEHOLDERS
Language=English
This is a sample event log message with placeholders: %1 and %2
.
Language=Japanese
プレースフォルダ利用イベントログのサンプル: %1 と %2
.
```

Severity 値一覧：

| 値             | 意味                     | イベントログのレベル       |
|----------------|--------------------------|---------------------------|
| Success       | 正常終了                 | 情報（Information）         |
| Informational | 通知・情報               | 情報（Information）         |
| Warning       | 警告                     | 警告（Warning）             |
| Error         | エラー                   | エラー（Error）             |
| Severe        | 深刻なエラー（非推奨）   | 通常は Error として扱われる |

::: note
イベントログのレベルは、イベント出力時に EventLogEntryType を指定すると、そちらが優先されます。
例えば、Severity=Warning となっていても、イベントログ出力時に EventLogEntryType.Error として出力すると、レベルはエラーになります。
:::


他項目についての詳細は [メッセージ テキスト ファイル - Win32 apps | Microsoft Learn](https://learn.microsoft.com/ja-jp/windows/win32/eventlog/message-text-files) を確認してください。


### ビルド

ビルド用のソリューションを作成することもできますが、[メッセージファイル DLL を作成する | iPentec](https://www.ipentec.com/document/windows/windows-create-message-file-dll) に記載されている手順で、メッセージ DLL をビルドします。


スタートメニューから `Visual Studio 2022` - `x64 Native Tools Command Prompt for VS 2022` を開きます。
HogeHoge.mc を BOM付き UTF-8 ファイルとして保存し、以下のコマンドを実行してメッセージ DLL を作成します。

```
mc.exe -cp utf-8 HogeHoge.mc
rc.exe HogeHoge.rc
link.exe /NOENTRY /DLL /MACHINE:X64 HogeHoge.res /OUT:HogeHoge.dll
```

上記、DLL 作成過程で生成された HogeHoge.h にメッセージ ID が格納されています。
HogeHoge.mc で指定した SymbolicName がメッセージ ID のキーワードです。

```c:HogeHoge.h
//
// MessageId: ERROR_WORKFILECREATE
//
// MessageText:
//
// Failed to create work file.
//
#define ERROR_WORKFILECREATE             ((DWORD)0xC00401F5L)

//
// MessageId: INFO_PLACEHOLDERS
//
// MessageText:
//
// This is a sample event log message with placeholders: %1 and %2
//
#define INFO_PLACEHOLDERS                ((DWORD)0x400201F6L)
```

イベントログ出力用に C# ソースへ転記します。

```csharp
private const long ERROR_WORKFILECREATE = 0xC00401F5L;
private const long INFO_PLACEHOLDERS = 0x400201F6L;
```


### イベントソース登録

イベントソース登録は管理者権限が必要です。
インストーラで登録する場合は、レジストリに直接登録することが一般的でしょうが、C# での手法を記載します。

```csharp
string source = "HogeHoge";     // イベントソース名
string logName = "Application"; // ログの種類（Application ログ）
string dllPath = @"C:\Path\To\HogeHoge.dll"; // TODO - メッセージ DLL パス

// イベントソース存在確認 - 管理者権限が必要
if (!EventLog.SourceExists(source))
{
  // イベントソース登録 - 管理者権限が必要
  var data = new EventSourceCreationData(source, logName)
  {
    MessageResourceFile = dllPath
  };
  EventLog.CreateEventSource(data);
  // 下記レジストリに登録
  // HKLM\SYSTEM\CurrentControlSet\Services\EventLog\Application\HogeHoge
}
```

### イベントログ出力

メッセージ DLL のメッセージを利用するので、プレースフォルダ値のみを出力します。

https://learn.microsoft.com/ja-jp/dotnet/api/system.diagnostics.eventlog.writeevent

```csharp
// .NETFramework の場合、引数は「object [] values」で良い
private void EventLoggingUseMessageDll(long messageId, 
                                       EventLogEntryType type, object[]? values)
{
  string source = "HogeHoge";     // イベントソース名
  string logName = "Application"; // ログの種類（Application ログ）

  EventLog eventLog = new EventLog(logName)
  {
    Source = source
  };

  try
  {
    // イベントログにメッセージ出力 - Application ログは管理者権限不要
    EventInstance eventInstance = new EventInstance(messageId, 0, type);
    eventLog.WriteEvent(eventInstance, values);
  }
  catch (SecurityException)
  {
    // TODO - 対象イベントソースが存在しないため、メッセージ出力エラー
    Console.WriteLine("対象イベントソースが存在しません。");
  }
  finally
  {
    // リソース解放
    eventLog.Close();
  }
}
```

以下のように利用します。

```CSharp
// messageId は HogeHoge.h から転記した値を利用
// private const long ERROR_WORKFILECREATE = 0xC00401F5L;
// private const long INFO_PLACEHOLDERS = 0x400201F6L;
EventLoggingUseMessageDll(ERROR_WORKFILECREATE, EventLogEntryType.Error, null);

object[] values = { "fuga", "piyo" };
EventLoggingUseMessageDll(INFO_PLACEHOLDERS, EventLogEntryType.Information, values);
```

### イベントログ取得

Application イベントログについて、日時が本日で最新 10件を取得するコードとします。
イベントログを確認して、メッセージ DLL 利用であれば、メッセージ DLL からメッセージを取得します。


```CSharp
string logName = "Application";  // 取得するログの種類
DateTime today = DateTime.Today; // 本日の日付（時刻は 00:00:00）

// イベントログ取得 - Application ログは管理者権限不要
// 当日の最新 10件にフィルタリング
EventLog eventLog = new EventLog(logName);
var filteredEntries = eventLog.Entries.Cast<EventLogEntry>()
        .Where(entry => entry.TimeGenerated.Date == today)  // 日付フィルタを適用
        .OrderByDescending(entry => entry.TimeGenerated)
        .Take(10);

foreach (var entry in filteredEntries)
{
  Console.WriteLine($"イベント ID: {entry.InstanceId}");
  Console.WriteLine($"レベル: {entry.EntryType}");
  Console.WriteLine($"ソース: {entry.Source}");
  Console.WriteLine($"日時: {entry.TimeGenerated}");
  Console.WriteLine($"メッセージ: {entry.Message}");
  Console.WriteLine("----------------------------");
}

// リソース解放
eventLog.Close();
```