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

なぜこの記事を書くに至ったかの経緯をまず書いているので
どう対応したかは最初に***[ここ](https://qiita.com/o________o/items/c17a63f8d82d8dcb6557#%E6%9C%AC%E9%A1%8Ctimebasedrollingpolicy%E4%BB%A5%E5%A4%96%E3%81%A8prudent%E3%83%A2%E3%83%BC%E3%83%89%E3%82%92%E5%85%B1%E5%AD%98%E3%81%95%E3%81%9B%E3%82%8B)***から読んで見てください。

# ことの始まり

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

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

- `1つのログファイル`に`異なるロガーから異なるフォーマットのメッセージ`を出力したい
- バックアップファイル名は`ログファイル名.n`の連番でつけたい（[log4j.RollingFileAppenderの仕様](https://www.techscore.com/tech/Java/ApacheJakarta/Log4J/3-3/)）

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


```xml: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として機能を提供しているわけではなくたまたま動く程度のようなもののようで、[ログファイルの消失の可能性](https://ja.stackoverflow.com/questions/55088/log4j-xml%E3%81%A7%E8%A4%87%E6%95%B0%E3%81%AEappender%E8%A6%81%E7%B4%A0%E3%81%AEfile%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AB%E5%90%8C%E4%B8%80%E3%83%91%E3%82%B9%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%81%AE%E6%98%AF%E9%9D%9E%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)があるそう。
設定で問題を回避する方法もあるようだが本題では無いので今回は触れない。

## logback
先にちょっと触れたが、logbackは複数のファイルアペンダーに同一ファイルを指定すると起動時エラーが出るため通常は不可能である。
だがRollingFileAppenderに[prudentモード](http://logback.qos.ch/manual/appenders_ja.html#prudent)というものが用意されており、これをtureにすることで各アペンダーが排他ロックをかけてログを書き込むため、log4j同様に同一ファイルにアペンダーを複数設定することができる。このprudentモードを使用することで可能となる。

ただし、ファイルをローテートする場合、**[サポート対象はTimeBasedRollingPolicyのみ](http://logback.qos.ch/manual/appenders_ja.html#prudentWithRolling)**

## logbackで連番をつけてファイルをバックアップする設定とprudentモードは共存できない
logbackで連番をつけてファイルをバックアップするには**[FixedWindowRollingPolicy](http://logback.qos.ch/manual/appenders_ja.html#FixedWindowRollingPolicy)**を使う必要がある。


つまり、1つのログファイルに異なるフォーマットのメッセージを出力することは可能だが、
**`ログファイル名.n`の連番でローテートしながら異なるフォーマットのメッセージを出力することはできない**わけです。


# 本題　TimeBasedRollingPolicy以外とprudentモードを共存させる
タイトルに`prudentモードを共存させる`と書きましたが、これは厳密には誤りで
基本方針は

**appenderの定義がひとつならprudentモードを使う必要無い** 

です。

この方針を踏まえ、実際に行った対応は次の二本立てです。

- appenderの定義を一つにまとめる
- RollingFileAppenderの拡張クラスを作り`異なるフォーマットのメッセージ`を出力する機能を追加する

今回私が対応をしたのはFixedWindowRollingPolicyなのでFixedWindowRollingPolicyで説明します。

## appenderの定義を一つにまとめる
log4j.xmlで共通で設定されていた項目を一つにまとめたアペンダーを新しく作り、
loggerの参照先は全てこの新しく作ったアペンダーにする。

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

```java: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以外のポリシーでも動くと思います（未検証）


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