なぜこの記事を書くに至ったかの経緯をまず書いているので
どう対応したかは最初に***ここ***から読んで見てください。
ことの始まり
log4jの設定をlogbackに移行していたら以下のような設定に出会った。
-
2つファイルアペンダーがあるが、layout以外は全て同じ設定
-
それぞれのアペンダーは異なるロガーがrefで参照している
つまり目的は -
1つのログファイル
に異なるロガーから異なるフォーマットのメッセージ
を出力したい -
バックアップファイル名は
ログファイル名.n
の連番でつけたい(log4j.RollingFileAppenderの仕様)
でもね、後で書きますがこの設定logbackだと起動時エラーが出るんですよ。
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" >
<appender name="error" class="org.apache.log4j.RollingFileAppender">
<param name="File" value="/var/usr/log/app.log" />
<param name="MaxFileSize" value="10MB" />
<param name="MaxBackupIndex" value="5" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="Error %d[yyyy/MM/dd HH:mm:ss:SSS]%m%n" /> <!-- エラー用メッセージフォーマット -->
</layout>
</appender>
<appender name="info" class="org.apache.log4j.RollingFileAppender">
<param name="File" value="/var/usr/log/app.log" />
<param name="MaxFileSize" value="10MB" />
<param name="MaxBackupIndex" value="5" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="Info %d[yyyy/MM/dd HH:mm:ss:SSS]%m%n" /> <!-- インフォ用メッセージフォーマット -->
</layout>
</appender>
<logger name="co.jp.xxxxxx.app">
<appender-ref ref="error"/>
</root>
<logger name="co.jp.xxxxxx.dao.repository">
<appender-ref ref="info"/>
</root>
</log4j:configuration>
1つのログファイルに異なるフォーマットのメッセージを出力する方法
log4j
上記のようにアペンダーを複数設定する事で可能だが、log4jとして機能を提供しているわけではなくたまたま動く程度のようなもののようで、ログファイルの消失の可能性があるそう。
設定で問題を回避する方法もあるようだが本題では無いので今回は触れない。
logback
先にちょっと触れたが、logbackは複数のファイルアペンダーに同一ファイルを指定すると起動時エラーが出るため通常は不可能である。
だがRollingFileAppenderにprudentモードというものが用意されており、これをtureにすることで各アペンダーが排他ロックをかけてログを書き込むため、log4j同様に同一ファイルにアペンダーを複数設定することができる。このprudentモードを使用することで可能となる。
ただし、ファイルをローテートする場合、サポート対象はTimeBasedRollingPolicyのみ
logbackで連番をつけてファイルをバックアップする設定とprudentモードは共存できない
logbackで連番をつけてファイルをバックアップするには**FixedWindowRollingPolicy**を使う必要がある。
つまり、1つのログファイルに異なるフォーマットのメッセージを出力することは可能だが、
ログファイル名.n
の連番でローテートしながら異なるフォーマットのメッセージを出力することはできないわけです。
本題 TimeBasedRollingPolicy以外とprudentモードを共存させる
タイトルにprudentモードを共存させる
と書きましたが、これは厳密には誤りで
基本方針は
appenderの定義がひとつならprudentモードを使う必要無い
です。
この方針を踏まえ、実際に行った対応は次の二本立てです。
- appenderの定義を一つにまとめる
- RollingFileAppenderの拡張クラスを作り
異なるフォーマットのメッセージ
を出力する機能を追加する
今回私が対応をしたのはFixedWindowRollingPolicyなのでFixedWindowRollingPolicyで説明します。
appenderの定義を一つにまとめる
log4j.xmlで共通で設定されていた項目を一つにまとめたアペンダーを新しく作り、
loggerの参照先は全てこの新しく作ったアペンダーにする。
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" >
<appender name="arrange_appender" class="xx.xx.xx.ArrangeRollingFileAppender"> <!-- 新しく作るRollingFileAppenderの拡張クラスを指定 -->
<file>/var/usr/log/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<maxIndex>5<maxIndex/>
<FileNamePattern>/var/usr/log/app.log.%i<FileNamePattern/>
<rollingPolicy/>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern></pattern> <!-- logback.xmlで設定したencoderは使用しないので特に設定しない -->
</encoder>
</appender>
<logger name="co.jp.xxxxxx.app">
<appender-ref ref="arrange_appender"/> <!-- 新しく一つにまとめたアペンダーを参照設定する -->
</root>
<logger name="co.jp.xxxxxx.dao.repository">
<appender-ref ref="arrange_appender"/> <!-- 新しく一つにまとめたアペンダーを参照設定する -->
</root>
</log4j:configuration>
RollingFileAppenderの拡張クラスを作る
logbackのログ出力処理に割り込んで、log4j.xmlで設定されていたロガーごとに異なるフォーマットのメッセージを切り替える処理
をここに持ってくる。
logbackに特化した危ないキャストとか冗長な実装とかあるのでご指摘大歓迎です。
public class ArrangeRollingFileAppender<E> extends RollingFileAppender<E> {
// log4j.xml errorアペンダーの設定をここに移行
private static final String LOGGER_NAME_ERROR = "co.jp.xxxxxx.app";
private static final String MESSAGE_PATTERN_ERROR = "Error %d[yyyy/MM/dd HH:mm:ss:SSS]%m%n";
// log4j.xml infoアペンダーの設定をここに移行
private static final String LOGGER_NAME_INFO = "co.jp.xxxxxx.dao.repository";
private static final String MESSAGE_PATTERN_INFO = "Info %d[yyyy/MM/dd HH:mm:ss:SSS]%m%n";
// ログメッセージマップ
private Map<String, String> mpm = new HashMap<String, String>();
// レイアウトエンコーダマップ
private Map<String, PatternLayoutEncoder> plem = new HashMap<String, PatternLayoutEncoder>();
/**
* コンストラクタでロガー名とログフォーマットのマップを作る
*/
public ArrangeRollingFileAppender() {
this.mpm.put(LOGGER_NAME_ERROR, MESSAGE_PATTERN_ERROR);
this.mpm.put(LOGGER_NAME_INFO, MESSAGE_PATTERN_INFO);
}
/**
* RollingFileAppenderのsubAppendに割り込んでPatternLayoutEncoder切り替えてログフォーマット変える
*/
@Override
public void subAppend(E event) {
// ロガーネームと前方一致で見つかったログエンコーダーを設定する
String loggerName = ((LoggingEvent) event).getLoggerName();
// XXX if分岐はダサいからいつかなんとかしたい、keyListのstreamとかで綺麗になるのでは
if (loggerName.startsWith(ArrangeRollingFileAppender.LOGGER_NAME_ERROR)) {
this.setEncoder((Encoder<E>)this.getPatternLayoutEncoder(ArrangeRollingFileAppender.LOGGER_NAME_ERROR));
} else if (loggerName.startsWith(ArrangeRollingFileAppender.LOGGER_NAME_INFO)) {
this.setEncoder((Encoder<E>)this.getPatternLayoutEncoder(ArrangeRollingFileAppender.LOGGER_NAME_INFO));
} else {
return; // ログ出力しない
}
// スーパークラスに移譲
super.subAppend(event);
}
private PatternLayoutEncoder getPatternLayoutEncoder(String key) {
if (! this.plem.containsKey(key)) {
// エンコーダが登録されていない場合、現在設定されているエンコーダの設定を元にしたエンコーダを作る
PatternLayoutEncoder baseEncoder = (PatternLayoutEncoder)this.getEncoder();
PatternLayoutEncoder newEncoder = new PatternLayoutEncoder();
newEncoder.setContext(baseEncoder.getContext());
newEncoder.setOutputPatternAsHeader(baseEncoder.isOutputPatternAsHeader());
newEncoder.setPattern(this.mpm.get(key));
newEncoder.start();
this.plem.put(key, newEncoder);
}
return this.plem.get(key);
}
}
おわりに
とりあえずこれらを組み合わせたら
-
1つのログファイル
に異なるフォーマットのメッセージ
を出力したい - バックアップファイル名は
ログファイル名.n
の連番でつけたい
この要件を満たしたのですが、LogAppenderの仕様はほとんどわかっていないので危ない場所とかあったらご指摘頂けると嬉しいです。
多分FixedWindowRollingPolicy以外のポリシーでも動くと思います(未検証)
今後の展開で一番嬉しいのはこんな面倒なことしなくてもこんなことできるよって言うツッコミが来ることですね。