ククログ

株式会社クリアコード > ククログ > Visual Studioなしでmingw-w64だけでWindowsのイベントログに出力する機能を実現する方法

Visual Studioなしでmingw-w64だけでWindowsのイベントログに出力する機能を実現する方法

GNU/Linux、*BSD、OS X、Windowsなど複数のプラットフォームに対応するプログラムをC/C++で開発する場合、GCCを利用すると便利です。例に挙げたすべてのプラットフォームに対応している上に、クロスコンパイルもできるからです。特にWindows向けバイナリーのクロスコンパイルは便利です。開発環境(たとえばGNU/Linux)上でビルドできるので、環境を切り替える手間が減って開発効率があがります。

クロスコンパイル関連のことについてはここでは説明しません。必要であれば、Debian GNU/LinuxでWindows用バイナリをビルドする方法や近日中にまとめる予定のCygwinのSSHサーバーに公開鍵認証でログインする方法を参照してください。

これに該当するソフトウェアに全文検索エンジンのGroongaがあります。Groongaは全文検索サーバーとしても動作します。サーバーは長期間動作するプロセスで、状況を確認したり問題を調査するためにログを出力する機能は必須です。ログは確認しやすくないと活用されなくなってしまうため、運用しているシステムとうまく連携するのがベターです。たとえば、UNIX系のシステムなら/var/log/以下のファイルに出力したりSyslogに出力したり、Windowsならイベントログに出力したり、という具合です。

システムと連携する場合、どうしてもその環境特有のコードになってしまいます。ここでは、Windowsでイベントログにログを出力する機能を実現する方法を説明します。ただし、Visual Studioは使わずにmingw-w64だけで実現するという制限をつけることにします。理由は、そうしないとクロスコンパイルできないからです。

なお、以下Windowsのイベントログについて調べた結果を情報源(MSDN)付きで説明していますが、説明している人はWindowsに詳しくない人なので誤った理解を説明しているかもしれません。疑問に思った箇所は情報源を参照してください。

Windows Event Log APIとWindows Logging API

具体的な実現方法を説明する前にイベントログ関連のAPIを説明します。

Windowsイベントを出力するには次のどちらかのAPIを使います。

Event Logging APIはWindows Server 2003、Windows XP、Windows 2000用に開発されたAPIです。

Windows Event Log APIはWindows Server 2008、Windows Vista以降用に開発されたAPIで、Event Logging APIの上位互換になっています。

Event Logging APIはログを出力するだけならコードを書くだけで別途ツールは必要ありません。ただし、ログをキレイに表示するならMC.exe(Message Compilerの略)というツールが必要です。キレイに表示しなくてもよいならツールは必要ありません。

Windows Event Log APIは別途ツールが必要です。

Windows Event Log APIではログを出力するアプリケーションのことをプロバイダーと呼んでいます。プロバイダーには2種類あります。

  • クラシックプロバイダー(classic provider)

  • マニフェストベースのプロバイダー(manifest-based provider)

それぞれログを出力するために使うAPIも違いますし、使うツールも違います。なお、ログを出力することを「イベントを書く」と呼んでいるので、以降ではそのように書いている箇所があります。

クラシックプロバイダーでイベントを書く場合はRegisterTraceGuids()TraceEvent()というAPIとmofcomp(Managed Object Format compilerの略。Managed Object FormatはMOFと略される。)というツールが必要です。

マニフェストベースのプロバイダーでイベントを書く場合はEventRegister()EventWrite()というAPIとMC.exe(Event Logging APIでキレイにログを表示するために使うツールと同じ)というツールを使います。

今はVisual Studioを使わずにmingw-w64だけでログを出力したいので選択肢はEvent Logging APIしかありません。ただし、そのままではログはキレイに表示されません。この回避策については後で触れます。

(なお、Visual Studioと書いていますが、MC.exeはWindows Kits(Windows SDKのこと?)に含まれるもので、mofcompはWMI Administrative Toolsに含まれていそうなものなので、本当はVisual Studioではありません。)

Event Logging APIを使ったログ出力の実装方法

それでは、Event Logging APIを使ったログ出力の実装方法を説明します。ログをキレイに表示するための回避策については次のセクションで説明します。

Event Logging APIでログを出力するために使うAPIは次の3つです。

RegisterEventSource()ReportEvent()を実行するときに使うハンドルを取得して、ReportEvent()でログを出力します。ログは何回出力してもかまいません。ログを出力し終わったらDeregisterEventSource()RegisterEventSource()で取得したハンドルを解放するという流れです。

コードでいうと次のような流れです。

HANDLE event_source;

event_source = RegisterEventSource(/* ... */);
ReportEvent(event_source/*, ... */);
ReportEvent(event_source/*, ... */);
ReportEvent(event_source/*, ... */);
/* ... */
ReportEvent(event_source/*, ... */);
DeregisterEventSource(event_source);

このコードを動くようにするために最低限決めなければいけないことは次のことです。

  • イベントソース名

  • ログの種類

  • メッセージ

イベントソース名はアプリケーション名にするとよいでしょう。Groongaの場合は"Groonga"です。

ログの種類は次から選びます。

  • 成功

  • 認証失敗

  • 認証成功

  • エラー

  • 情報(知っておくといいよ、という情報。たとえば、どのポート番号で起動した、とか。)

  • 警告

多くの場合は「エラー」、「情報」、「警告」から選ぶことになるでしょう。Groongaではこの3つのどれかだけを使っています。

メッセージはログに出力するメッセージです。

次のように決めたとします。

  • イベントソース名:「"MyApp"

  • ログの種類:「エラー」

  • メッセージ:「"File not found"

このときのコードは次のようになります。ReportEvent()の引数が多いですが、ほとんどの引数は0NULLといった値を使うことができます。

#include <windows.h>

int
main(int argc, char **argv)
{
  const char *event_source_name = "MyApp";
  HANDLE event_source;
  WORD type = EVENTLOG_ERROR_TYPE;
  WORD category = 0;
  DWORD event_id = 0;
  SID *user_sid = NULL;
  WORD n_strings = 1;
  DWORD data_size = 0;
  const char *strings[] = {"File not found"};
  void *data = NULL;

  event_source = RegisterEventSource(NULL, event_source_name);
  ReportEvent(event_source, type, category, event_id, user_sid,
              n_strings, data_size,
              strings, data);
  DeregisterEventSource(event_source);

  return 0;
}

次のようにmingw-w64でクロスコンパイルします。

% x86_64-w64-mingw32-gcc -Wall -o log.exe log.c

このlog.exeをWindowsにコピーして動かすとWindowsのイベントログに出力できます。イベント ビューアーで確認すると次のようになります。

イベントビューアーで表示するとキレイに表示されない

「File not found」というメッセージの前に「ソース"MyApp"からの…」という説明が書いています。これが「キレイにログが表示されない」ということです。これの回避方法は次のセクションで説明します。

なお、WevtUtil.exeというツールを使うとコマンドラインからでも確認できます。(XMLは見やすいように整形していますが実際は1行です。)

> wevtutil.exe query-events Application /c:1 /rd:true /f:xml
<Event xmlns='http://schemas.microsoft.com/win/2004/08/events/event'>
  <System>
    <Provider Name='MyApp'/>
    <EventID Qualifiers='0'>0</EventID>
    <Level>2</Level>
    <Task>0</Task>
    <Keywords>0x80000000000000</Keywords>
    <TimeCreated SystemTime='2015-09-09T20:36:16.000000000Z'/>
    <EventRecordID>30162</EventRecordID>
    <Channel>Application</Channel>
    <Computer>gatows</Computer>
    <Security/>
  </System>
  <EventData>
    <Data>File not found</Data>
  </EventData>
</Event>

オプションの説明は省略します。「wevtutil.exe /?」で確認できるのでそちらを参照してください。

ポイントは「/f:xml」です。これはXMLで出力するという意味です。デフォルトではテキストで出力するのですが、そうするとメッセージ(今は「File not found」)を確認できません。これは「キレイにログが表示されない」と関係しているのですが、それの説明も次のセクションに回します。

ここで示したコードと引数で「イベントソース名」、「ログの種類」、「メッセージ」を出力できるコードをkou/windows-event-log-loggerに置いてあるので参考にしてください。

キレイにログを表示する方法

Windowsのイベントログはログを出力する側(たとえばアプリケーション)とログを読む側(たとえばイベントビューアー)で別になっています。ログを出力する側は必要な情報を出力します。ログを読む側は出力されたログをいい感じに表示します。いい感じとは、たとえば国際化して表示するということです。英語ユーザー向けには「File "logger.exe" doesn't exist.」と表示して、日本語ユーザー向けには「『logger.exe』というファイルがありません。」と表示するというようなことです。

イベントビューアーで見たときに「ソース"MyApp"からの…」という説明がでるのは、いい感じに表示できていないということを表しています。いい感じに表示するためには追加の情報が必要です。それがメッセージファイルです。

メッセージファイルには「このイベントのメッセージはこんな風に表示する」という情報が入っています。それが見つかるとイベントビューアーは「ソース"MyApp"からの…」という説明が消えます。

そのメッセージファイルを用意するにはMC.exeというツールを使いながらその情報が入ったDLLを作成する必要があります。

ただ、それだとクロスコンパイルできないので別の方法を2つ紹介します。

  • 他のマルチプラットフォーム対応のフリーソフトウェアが提供しているDLLを利用する。

  • Windows組み込みのDLLを利用する。

他のマルチプラットフォーム対応のフリーソフトウェアが提供しているDLLを利用

最初の方法は他のマルチプラットフォーム対応のフリーソフトウェアが提供しているメッセージファイルの情報が入ったDLLを利用する方法です。

Windowsのイベントログの作法に従うなら、アプリケーションはメッセージを組み立てるために必要な情報だけを出力し、メッセージファイルの方でそれを組み立ててメッセージにします。たとえば、アプリケーションからはファイル名だけを出力し、メッセージファイルには「『ここにファイル名を入れる』というファイルが見つかりませんでした。」というテンプレートを用意しておくということです。

しかし、その方法に従うと他のプラットフォームでうまくログを出力できません。他のプラットフォームではアプリケーションがメッセージを組み立てることが多いからです。そのため、マルチプラットフォーム対応のフリーソフトウェアはメッセージの内容をアプリケーション側で組み立てて、メッセージファイルには「『ここにそのままメッセージを入れる』」というテンプレートを用意し、すべてのメッセージをアプリケーション側で組み立てています。

たとえば、PostgreSQLもそのような実装になっていて、そのようなメッセージファイルの情報が入ったDLLを提供しています。

PostgreSQLが提供しているDLL(PostgreSQLのバイナリーのzipをダウンロードして展開した中にあるpgsql\lib\pgevent.dll)を利用する場合は次のようにします。管理者権限で実行しないと失敗するので注意してください。

> regsvr32 /n /i:MyApp pgsql\lib\pgevent.dll

イベントビューアーを見てください。「ソース"MyApp"からの…」という説明が消えています。

イベントビューアーで見るとキレイに表示される

注意点は一度登録したらDLL(この場合はpgevent.dll)の場所を変えてはいけないということです。レジストリーにこのDLLのパスが書き込まれているからです。

Windows組み込みのDLLを利用

他のマルチプラットフォーム対応のフリーソフトウェアのDLLを使えば自分のアプリケーションではDLLを作成する必要はありませんでした。しかし、いちいちDLLを登録するのは面倒です。ということで別の方法です。

別の方法も方針は前述の方法と一緒で、「『ここにそのままメッセージを入れる』」というテンプレートを利用するのですが、そのテンプレートを持ってくる方法が違います。Windowsに標準で入っているたくさんのテンプレートの中の、たまたまそのようなテンプレートになっているテンプレートを使います。

この方法を使うとメッセージ以外の情報(たとえばカテゴリーとか)がおかしなことになります。ただ、メッセージだけに興味があるのでその他は気にしないと割り切るならアリでしょう。

この方法を使っているのはApacheです。Apacheはnetmsg.dllを使っています。このDLLの中のイベントID3299のメッセージのテンプレートが「『ここにそのままメッセージを入れる1』『ここにそのままメッセージを入れる2』...『ここにそのままメッセージを入れる9』」というものなのです。

興味がある人はApacheのソースのserver/mpm/winnt/nt_eventlog.cをみてください。

まとめ

Visual Studioを使わずにmingw-w64だけでクロスコンパイルできるWindowsのイベントログ出力機能の実装方法について説明しました。

同じようなことをしようとして調べているフリーソフトウェア開発者の役に立つ情報になることを期待します。

なお、Groongaはこの間リリースされた5.0.7からWindowsのイベントログ出力機能が実装されています。WindowsでもGroongaを使ってみてください。