イベントログにメッセージを出力する
1.はじめに
Windowsにはイベントログという、ログを記録・管理する機能がある。これは、システムやアプリケーションで発生したイベントを記録し、ある程度メッセージがたまったら、容量や期間などの設定に合わせて、適宜不要なメッセージを削除してくれるというような機能である。
また当然だが、この機能はWindowsの本体だけが利用できる機能というわけではなく、Windows上で動作する一般のアプリケーションからでも利用できるようにAPIが公開されている。
一般的にいって、アプリケーションからしてみればログ出力は本質的な処理ではない。ましてや、ログファイルのメンテナンスや世代管理などは、工数がかかるくせして目的達成には何の貢献もないという、極めて厄介なお荷物的存在である。というか、荷物以外の何物でもない。であれば、使える物は積極的に活用して、どうでもいい部分については手を抜くのが賢いやり口というものだろう。
だが、イベントログへのメッセージ出力は、単にAPIを叩けばいいというだけのものではなく、いくらかの知識と手数が必要になる。このページでは、イベントログへメッセージを出力する方法について解説したいと思う。
2.メッセージの出力
基本的には、下記の処理でイベントログにメッセージを出力してやることができる。
const char *pMsg = "Error Message"; HANDLE h = RegisterEventSource( NULL, "myapp" ); ReportEvent( h, EVENTLOG_ERROR_TYPE, 0, 0, NULL, 1, 0, &pMsg, NULL ); DeregisterEventSource( h );2.1 RegisterEventSource関数
イベントログにアクセスするためのハンドルを返す。
プロトタイプ
HANDLE RegisterEventSource( LPCTSTR lpUNCServerName, LPCTSTR lpSourceName );lpUNCServerName
イベントログを出力する先のサーバ名を指定する。ローカルのイベントログに書き込みたければNULLを指定すればいい。
lpSourceName
これがくせ者で、いろいろと考えなければいけない。とりあえずここでは、適当なアプリケーションの名前を指定しておくものとする。
戻り値
イベントログに書き込むためのハンドルが返される。失敗した場合はNULLが返される。
2.2 ReportEvent関数
イベントログにメッセージを書き込む。
プロトタイプ
BOOL ReportEvent( HANDLE hEventLog, WORD wType, WORD wCategory, DWORD dwEventID, PSID lpUserSid, WORD wNumStrings, DWORD dwDataSize, LPCTSTR *lpStrings, LPVOID lpRawData );hEventLog
RegisterEventSource関数で取得したハンドルを指定する。
wType
以下の値を指定する。
EVENTLOG_ERROR_TYPE エラーのイベントを出力する。 EVENTLOG_WARNING_TYPE 警告のイベントを出力する。 EVENTLOG_INFORMATION_TYPE 情報のイベントを出力する EVENTLOG_AUDIT_SUCCESS 監査の成功のイベントを出力する EVENTLOG_AUDIT_FAILURE 監査の失敗のイベントを出力する 出力するメッセージにあわせて、好きな奴を適当に選択すればいい。
wCategory
イベントログのシステム的には使われない。アプリケーションの都合で勝手な値を指定すればいいらしい。
dwEventID
イベントのIDを指定する。
基本的にはアプリケーションの都合で任意の番号を出力させることができるのだが、後でいろいろとこの番号について考えなければいけないことがあるので、注意が必要である。
なお、この番号は必ず正の値でなければいけない。
lpUserSid
セキュリティ識別子を指定する。特に意志がないのならNULLを指定しておけばいい。
wNumStrings
lpStringsに指定する文字列の個数(文字数では無い)を指定する。
dwDataSize
lpRawDataに指定するデータ長を指定する。
lpStrings
出力するメッセージの、文字列の配列(const char**)を指定する。各文字列は最大32KBでNULLで終わっていなければならない。この引数の配列長(文字列の個数)は、wNumStringsに指定する。
lpRawData
メッセージに付加して出力するバイナリデータを指定する。データ長はdwDataSizeに指定する。
戻り値
成功した場合は真が返される。
2.3 DeregisterEventSource関数
RegisterEventSourceで取得したイベントログに書き込むためのハンドルを閉じる。
プロトタイプ
BOOL DeregisterEventSource( HANDLE hEventLog );hEventLog
ハンドルを指定する。
戻り値
成功した場合は真が返される。
3 出力内容
上記のプログラムを実行すると、下図のようなメッセージが出力される。
Windows NT 4.0
Windows XP
"Error Message"という自分で出力しようとしたメッセージ以外にも、なんかよくわからない余分なメッセージが一緒に出力されている。
気にしないのならそれでいいのかもしれないが、しかし、こんな変なものが表示されてしまうのでは、アプリケーションソフトとしてはなはだ不都合である。だから、次はこのメッセージの意味と消し方について考えてみる。
4.余分なメッセージの消去
イベントログのシステムは、メッセージの出力元(ソース)とイベントIDから、リソースを検索してそこからメッセージを取り出そうと試みる。そして、リソースを検索した結果該当するリソースを見つけられなかった場合に、上図のようなメッセージを表示する。
だから、検索するリソースとやらを準備してやれば、上図のようなメッセージが表示されることはなくなるはずである。
必要なりソースとかいうものは、下記の手順に従い生成する。
4.1 mcファイルの作成
イベントIDに対応するメッセージを記述した、mcファイルを作成してやる。mcファイルのフォーマットについては、下記のページに記載されているが、何分英語なので何を言っているのか判らない。
http://msdn.microsoft.com/en-us/library/aa385646(VS.85).aspx
とりあえずここでは、下記のように記述する。
LanguageNames=(Japanese=0x0411:MSG00411) MessageId=0 SymbolicName=name0 Language=Japanese メッセージは「%1」です。 .LanguageNamesには、その後のLanguageにJapaneseと指定できるようにするために記述する必要がある。とりあえず、ファイルの先頭に記述しなければならない。
MessageIdにはイベントIDに指定する番号を記述する。
SymbolicNameには、何でもいいから適当に名前を付ける。
Languageには言語を指定する。とりあえず、俺は日本人なのでJapaneseと指定しておく。
その後の%1の部分には、イベントログとして出力するメッセージを記述する。%1は、ReportEvent関数のlpStringsに指定した文字列の0番目のものに置換される。すなわち、%1はlpStrings[0]に、%2はlpStrings[1]に、%3はlpStrings[2]に置き換えられることになる。
最後のピリオド(ピリオドだけの行)は、メッセージの終わりを示している。
とりあえずこういうファイルを作成して、evtest2.mcという名前で保存するものとする。
4.2 コンパイル
下記のコマンドで、作成したmcファイルをコンパイルする。
mc evtest2.mcなお、通常ではmc.exeが入っているところにはパスは通っていない。俺の環境ではC:\Program Files\Microsoft Visual Studio\VC98\Binにあったが、これは人によって異なるはずだ。とりあえず、mc.exeという名前でディスク内を検索して、見つかったディレクトリにパスを通せば何とかなるのではないだろうか。
上記のコマンドでコンパイルに成功すると、下記の3つのファイルがカレントディレクトリ内に生成される。
- MSG00411.bin
- evtest2.h
- evtest2.rc
次に、生成されたrcファイルを、rcコマンドでコンパイルする。
set PATH=%PATH%; rc evtest2.rc
なおrc.exeは、俺の環境では「C:\Program Files\Microsoft Visual Studio\Common\MSDev98\Bin」に格納されていた。だが、これも人によって異なるはずだ。とりあえず、mc.exeと同じようにrc.exeという名前でディスク内を検索して、見つかったディレクトリに適当にパスを通しておけば、多分きっと何とかなるだろう。何とかならなかったら、まぁ諦めるんだな。rcファイルのコンパイルに成功すると、下記のファイルが生成される。
- evtest2.RES
次は、このRESファイルをリンクしてDLLを生成してやる。
link /DLL /NOENTRY /MACHINE:IX86 evtest2.RES当然だが、PATHの設定は・・・・・・って、もういいよな。
上記の処理が全てうまくいくと、メッセージのリソースだけを含んだevtest2.dllというDLLが生成される。
4.3 インストール
4.2で生成したDLLをインストールしてやり、イベントログのシステムから検索できるように設定してやる。
インストールするには、レジストリのHKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\EventLog\Application\{アプリケーション名}に、
EventMessageFileという名前のキーを追加する。値は4.2で生成したDLLのパス名を指定する。{アプリケーション名}には、2.1のRegisterEventSource関数のlpSourceNameに指定する値を設定する。
4.4 表示
これで一応、不要なメッセージは表示されなくなるはずである。再度、先ほどのメッセージをイベントビューアで表示してやると、下記のようになる。
5.複数種類のメッセージを表示する
たとえば、イベントログを出力するプログラムを下記のように変更する。
const char *pMsg0[2] = { "メッセージ0-0", "メッセージ0-1" }; const char *pMsg1[2] = { "メッセージ1-0", "メッセージ1-1" }; HANDLE h = RegisterEventSource( NULL, "myapp" ); ReportEvent( h, EVENTLOG_ERROR_TYPE, 0, 0, NULL, 2, 0, pMsg0, NULL ); ReportEvent( h, EVENTLOG_ERROR_TYPE, 0, 1, NULL, 2, 0, pMsg1, NULL ); DeregisterEventSource( h );でもって、mcファイルを下記の様に記述する。
LanguageNames=(Japanese=0x0411:MSG00411) MessageId=0 SymbolicName=name0 Language=Japanese イベントID=0のメッセージです。「%1」「%2」 . MessageId=1 SymbolicName=name0 Language=Japanese イベントID=1のメッセージです。「%1」「%2」 .つまり、イベントIDとして0番と1番の二つを使用し、かつ、それぞれメッセージ中に二つの文字列を挿入するものとする。
このようにした場合、イベントログには下記のように出力される。
つまり、メッセージ中のロケールに依存する固定文字列の部分はリソースに記述して、可変部分はプログラムから出力するようにしてやればいい。
また、メッセージの種類が多ければ使用するイベントIDの数を増やしてやればよくって、一つのメッセージ中に含まれる可変部分の数が多いのであれば、メッセージ中の%1・%2・・・の個数と、ReportEvent関数のlpStringsに指定する文字列の個数を増やしてやればいいと言うことになる。
6.最後に
上記の4で作成したDLLとレジストリの設定は、イベントログにメッセージを書き込むときではなく、イベントログを参照する際に使用される。
つまり、生成したDLLをアプリケーションとともに配布する必要があり、インストーラでレジストリ設定を行ってやる必要があるということだ。
また、アプリケーションが動作するPCとイベントログを参照するPCが異なる可能性がある場合には、イベントログを参照する側のPCにも、上記のDLLとレジストリ設定が必要になる。アプリケーションを配布する際には、そういったことにも十分に検討しておく必要がある。
(なお最後になるが、このページで公開している画像は全てホスト名を消してある。その辺についてはご了承下さい)