リターンライダー Javaでイベントログを出力する 【パート1】

プラットフォーム非依存」言語である「Java」において、「Pure Java」ではイベントログを出力することはできない

イベントログ出力には「Windows API」を使用する必要があり
Javaにおいては「JNI (Java Native Interface)」を使うこととなるが、
log4j 1.2」がJNIを使用したイベントログの出力クラスを提供しているので、
これを利用するのが手っ取り早い。


因みに「log4j 1.2」は開発が終了し、log4jチームは「log4j 2.0」への移行を勧めていが、
余程「高性能」「高機能」のログ出力がシステム要件になければ「log4j 1.2」で十分である。
導入コストも断然安い。

- log4j 1.2のダウンロード先 (最終verは 1.2.17)


NTEventLogAppender.dll

log4j-1.2.17.zip」アーカイブ直下に格納されている以下のファイルが、
イベントログを出力するためのNativeライブラリである。

・NTEventLogAppender.dll(32bit用)
・NTEventLogAppender.amd64.dll(64bit用)

尚、最終バージョンである「1.2.17」の「NTEventLogAppender.dll(32bit用)」は、

log4jチームがコンパイルをミスっていて使い物にならない。

dumpbin」でダンプすると分かるが、
イベントソースの制御関数(deregisterEventSource, registerEventSource)」と
イベントログの出力関数(reportEvent)」が漏れている。

[1] : 「1.2.16」のダンプ結果
07|File Type: DLL                                                                             
08|
09| Section contains the following exports for NTEventLogAppender.dll
10|
11| 00000000 characteristics
12| 4BB2CC3E time date stamp Wed Mar 31 13:14:54 2010
13| 0.00 version
14| 1 ordinal base
15| 5 number of functions
16| 5 number of names
17|
18| ordinal hint RVA name
19|
20| 1 0 00001462 DllRegisterServer
21| 2 1 00001394 DllUnregisterServer
22| 3 2 00001628 Java_org_apache_log4j_nt_NTEventLogAppender_deregisterEventSource
23| 4 3 00001884 Java_org_apache_log4j_nt_NTEventLogAppender_registerEventSource
24| 5 4 0000164E Java_org_apache_log4j_nt_NTEventLogAppender_reportEvent



[2] : 「1.2.17」のダンプ結果
07|File Type: DLL                                                     
08|
09| Section contains the following exports for NTEventLogAppender.dll
10|
11| 00000000 characteristics
12| 4FA659D4 time date stamp Sun May 06 20:00:36 2012
13| 0.00 version
14| 1 ordinal base
15| 2 number of functions
16| 2 number of names
17|
18| ordinal hint RVA name
19|
20| 1 0 00001462 DllRegisterServer
21| 2 1 00001394 DllUnregisterServer
|
|
|
22|



C++のソースコードにversion差はないので、「32bit OS」の場合は「1.2.16」のNativeライブラリを使用すればよい。
ただし、Javaアーカイブはバグフィックスされている「log4j-1.2.17.jar」を使用すること


Nativeライブラリの配置

NTEventLogAppender.dll/.amd64.dll」は、どこに配置しても構わないが、
システム環境変数」の「Path」に配置先を定義する必要がある。

よく、最初からPathが通っている「System32」直下に配置する輩がいるが、
アプリ固有のライブラリは、あんなところに入れるもんではない。

2015_0524_01_システム環境変数設定

※画像は「C:\ProgramData\j\sample\lib」に配置した場合の定義例


設定ファイルの記述

log4jの設定ファイルは「XMLファイル(*.xml)」と「プロパティファイル(*.properties)」の2つの形式で定義できるが、特別な事情がなければ「XMLファイル(*.xml)」で記述した方がよい。

何故なら、

・「AsyncAppender(非同期出力)」が「XMLファイル」定義にしか対応していない
・両方のファイルが存在する場合「XMLファイル」が優先される

からである。

イベントログを出力する場合は、以下のように記述する。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="true">

<!-- イベントログ出力のアペンダ sample-commons-logging -->
<appender name="event" class="org.apache.log4j.nt.NTEventLogAppender">
<param name="threshold" value="info" />
<param name="source" value="SampleCommonsLogging" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="[%t]%m" />
</layout>
</appender>

<!-- イベントログ出力(障害検知ログ)のロガー -->
<logger name="event">
<level value="all" />
<appender-ref ref="event" />
</logger>

<!-- ルートのロガー -->
<root>
<level value="off" />
</root>
</log4j:configuration>

この例では「イベントログのソース」に「SampleCommonsLogging」と出力されるアペンダとロガーを「event」という名前で定義している。

余談だが、ソース名に「- (ハイフン)」を入れるとソース名が正常に表示されないことがある。
これは「Microsoft-Windows-XXXX」というソースを「XXXX」と表示したいMicrosoftの仕様である。


DLLのレジストリ登録

出力したイベントログのメッセージに『ソースXXXからのイベントIDXXXの説明が見つかりません・・・』と出力される場合は、レジストリに「メッセージファイル」が登録されていないことが原因だ。
log4j」においては「NTEventLogAppender.dll」が「メッセージファイル」となる。

NTEventLogAppender.dll」には、このレジストリの登録処理が存在しているのだが、実行権限の問題からか機能していないことが多い。

そのため、レジストリキー「HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\Application\(ソース名)」を追加し、以下の値を登録する必要がある。

EventMessageFile    : メッセージファイルのファイルパス。
CategoryMessageFile : メッセージファイル(タスクのカテゴリ用)のファイルパス。
TypesSupported : イベントの種類数。基本「7」。
CategoryCount : タスクのカテゴリの件数。log4jは「6」。

2015_0524_02_レジストリ登録



ソースコード


[1] ログを出力する「EventLogger」クラス。
package jp.j.sample.commons.logging.logger;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.nt.NTEventLogAppender;

/**
* Windows イベントログのアプリケーションにログを出力するためのロガークラスです。
* @author J.in.ny
*/
public class EventLogger {

/** ロガー */
private Logger logger = Logger.getLogger("event");

/**
* コンストラクタ
*/
public EventLogger() {
}

/**
* アペンダを終了させます。
* @see java.lang.Object#finalize()
*/
public void deregister() {
synchronized (logger) {
NTEventLogAppender ap = (NTEventLogAppender) logger.getAppender("event");
ap.finalize();
}
}


/**
* メッセージをイベントログに出力します。
* @param l ログレベル
* @param message イベントログに出力するメッセージ
*/
public void write(Level l, String message) {

switch (l.toInt()) {
case Level.FATAL_INT:
logger.fatal(message);
break;
case Level.ERROR_INT:
logger.error(message);
break;
case Level.WARN_INT:
logger.warn(message);
break;
case Level.INFO_INT:
logger.info(message);
break;
default:
throw new IllegalArgumentException("ログレベルが不正です。level=" + l.toString());
}
}
}

NTEventLogAppender」クラスの「finalize」メソッドを、外から実行するためのメソッド(deregister)を必ず実装すること。
「アプリ終了前」および「ソース名の変更前」に finalize を実行しないと、ハンドルがリークしてしまう。

また、NTEventLogAppender」クラスはスレッドセーフではないので、必ず同期(synchronized)をかけてからアクセスすること。


[2] ロガーにアクセスするための「Log」クラス。
package jp.j.sample.commons.logging;

import jp.j.sample.commons.logging.logger.EventLogger;

/**
* ログ出力のためのオブジェクトを提供するクラスです。
* @author J.in.ny
*/
public class Log {

/** イベントログ出力用のロガー */
public static final EventLogger event = new EventLogger();

/**
* コンストラクタ
*/
private Log() {
}
}

ロガー(EventLogger)」のインスタンスは、処理コスト面からも動的に生成するものではない
シングルトンにしても良いが、ここではSystemクラスのような扱いができるように、クラス変数として公開している。


[3] イベントログ出力の試験用クラス「LogTest」。
package jp.j.sample.commons.logging;

import org.apache.log4j.Level;
import org.apache.log4j.LogManager;

/**
* テスト用のクラスです。
* @author J.in.ny
*/
public class LogTest {

/**
* コンストラクタ
*/
public LogTest() { }

/**
* イベントログの出力を試験します。
* @param args 未使用
*/
public static void main(String[] args) {
try {
Log.event.write(Level.INFO, "情報のイベントログを出力します。");
Log.event.write(Level.WARN, "警告のイベントログを出力します。");
Log.event.write(Level.ERROR, "エラーのイベントログを出力します。");
Log.event.write(Level.FATAL, "致命的エラーのイベントログを出力します。");
} finally {
Log.event.deregister();
LogManager.shutdown();
}
}
}

プログラムの終了時には、必ず「deregister」を実行して、ハンドルを解放すること。
また、「LogManager#shutdown」も必ず実行すること。


実行結果

サンプルのソースコードを実行すると、以下のようにイベントログが出力される。

2015_0524_04_実行結果

尚、「eclipse」で実行するときには、別途、環境変数の「Path」を設定する必要がある。

2015_0524_03_eclipse.png


log4j 1.2 によるイベントログ出力の問題点

このように log4j 1.2 を使って、簡単にイベントログが出力できるのだが、下記の問題点もある。

・「イベントID」が「4096」固定である
・「タスクのカテゴリ」にログレベルが英語で出力される


これらの問題に対応するためには、log4j 1.2の拡張が必要となるが、
拡張方法については、別の機会に紹介する。

- 本記事で使用したeclipseプロジェクト
関連記事

コメント

非公開コメント