0
0

More than 1 year has passed since last update.

【logback】TimeBasedRollingPolicy以外とprudentモードを共存させ、単一ログファイルに複数フォーマットのメッセージを出力する(タイトルに偽りあり)

Last updated at Posted at 2021-10-21

なぜこの記事を書くに至ったかの経緯をまず書いているので
どう対応したかは最初にここから読んで見てください。

ことの始まり

log4jの設定をlogbackに移行していたら以下のような設定に出会った。

  • 2つファイルアペンダーがあるが、layout以外は全て同じ設定
  • それぞれのアペンダーは異なるロガーがrefで参照している
     
    つまり目的は

  • 1つのログファイル異なるロガーから異なるフォーマットのメッセージを出力したい

  • バックアップファイル名はログファイル名.nの連番でつけたい(log4j.RollingFileAppenderの仕様

 でもね、後で書きますがこの設定logbackだと起動時エラーが出るんですよ

log4j.xml
<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.xml
<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に特化した危ないキャストとか冗長な実装とかあるのでご指摘大歓迎です。

xx.xx.xx.ArrangeRollingFileAppender.java
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以外のポリシーでも動くと思います(未検証)

今後の展開で一番嬉しいのはこんな面倒なことしなくてもこんなことできるよって言うツッコミが来ることですね。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0