Logbackのログファイルのエンコーディング

Slf4jの代表的な実装ライブラリ、
「Logback」には、FileAppender があります。
(実際に使うのは RollingFileAppender でしょう)

ログファイルを出力するからには、
エンコーディングの設定があるはずです。
(I/Oしてるところにエンコーディングあり!)

ググればすぐに出てきそうなものですが、
意外にエンコーディングの設定なしのサンプルも多く、
そのまま参考して抜けた状態で設定しちゃう、
ってなこともあり得ます。

ということもあり、
せっかくのjfluteの日記ということもあり、
コードリーディングで探してみましょう。

Appenderの継承関係

継承関係はこのようになっています。

OutputStreamAppender
  △
  |
 FileAppender
   △
   |
  RollingFileAppender

Fileの上に OutputStreamAppender という
概念がいるんですね。
ファイルとは限らないじゃない!ってことですね。

どちらの Appender のコードを見ても、
encoding とか charset とか出てきません。

encoderさんの登場

一方で、encoder というちょっと近い名前の
属性を持っていることがわかります。
(in OutputStreamAppender)

encoderがどのように使われているのが辿ると...
// in OutputStreamAppender of logback
protected void writeOut(E event) throws IOException {
    byte[] byteArray = this.encoder.encode(event);
    writeBytes(byteArray);
}
private void writeBytes(byte[] byteArray) throws IOException {
    if(byteArray == null || byteArray.length == 0)
        return;
    
    lock.lock();
    try {
        this.outputStream.write(byteArray);
        if (immediateFlush) {
            this.outputStream.flush();
        }
    } finally {
        lock.unlock();
    }
}
encoderさんがバイト配列に変換してます。
そのバイト配列をoutputStreamにwriteしていますから、
まさしくencoderさんが、出力されるログの文字列を、
エンコーディングしてそうです。名前もしっくり来ますし。

encoderの継承関係

Encoderを追ってみましょう。
ただのインターフェースです。

(もし、Eclipseであれば)
command + T で実装クラスを探しましょう。

Encoder
  △
  |
 EncoderBase
   △
   |
  LayoutWrappingEncoder
    △
    |
   PatternLayoutEncoderBase
     △
     |
    PatternLayoutEncoder

他にも枝分かれの実装クラスが出てきますが、
怪しいところだけ切り出しています。

charsetの発見

この中のどこかに、encodingとかcharsetとか、
それっぽいものがないでしょうか?
LayoutWrappingEncoderにいました。
// in LayoutWrappingEncoder of logback
public void setCharset(Charset charset) {
    this.charset = charset;
}
...
private byte[] convertToBytes(String s) {
    if (charset == null) {
        return s.getBytes();
    } else {
        return s.getBytes(charset);
    }
}
String@getBytes()の引数で指定しています。
もう、これっぽいですね。

logback.xmlの設定

さあ、logback.xml だとどこでしょう?
(LastaFlute の Example の logback.xml を元に)
<!-- logback example -->
<appender name="appfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <File>${log.file.basedir}/app_${domain.name}.log</File>
  <Append>true</Append>
  <encoder><pattern>${log.pattern}</pattern></encoder>
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>${log.file.basedir}/backup/app_${domain.name}${backup.date.suffix}.log</fileNamePattern>
    <maxHistory>${backup.max.history}</maxHistory>
  </rollingPolicy>
</appender>
おお、encoder っているじゃないですか。

ネスト要素に pattern がいます。
Encoderの階層のクラスの中の pattern を探すと、
PatternLayoutEncoderBase にいます。
普通に setter が用意されています。

ということは、charsetも同じような要領で、
設定できるんじゃないかと考えます。
試しに、UTF-8と入れてみましょう。
<!-- logback example -->
<appender name="appfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <File>${log.file.basedir}/app_${domain.name}.log</File>
  <Append>true</Append>
  <encoder><charset>UTF-8</charset><pattern>${log.pattern}</pattern></encoder>
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>${log.file.basedir}/backup/app_${domain.name}${backup.date.suffix}.log</fileNamePattern>
    <maxHistory>${backup.max.history}</maxHistory>
  </rollingPolicy>
</appender>
UnitTestで動かすなどして、
実際にログファイルに出力してみましょう。
UTF-8になっていますか?

...

いま、ほとんどの環境でデフォルトで
UTF-8 で動くことが多いので、
変わらないかもしれませんね。

であれば、一時的に SJIS とかに設定して、
実際にログファイルを出力して、
UTF-8として開いてみましょう。
日本語が文字化けすればOK。
さらに、SJISで開いて正常表示されればOK。
エンコーディング指定が効いた証拠です。

実際には、ベタ打ちするのではなく、
他の項目のように ${log.file.encoding} という感じで、
変数に切り出すほうが良いでしょう。
FileAppenderは何個も設定するでしょうから。
(アプリ通知用ファイル、エラー用ファイルなど)

こちらの logback.xml を参考に:
 => logback.xml の Example in LastaFlute

ただ、環境によって切り替えるとか、
時期によって切り替えるとか考えにくいので、
値自体は properties 化する必要もなく、
固定で UTF-8 で良いとは思います。

Encoderはそれでいいの?

さっき、他の枝分かれの Encoder がいて、
厳密には、この路線の実装クラスが、
利用しているクラスとは確定していないのですが、
元々指定していた pattern もあるし、
ってことが考えると、
ほぼこの路線で考えてよいだろうと考えられます。
実際に、枝分かれの EchoEncoder には、
pattern も charset もいませんし、
何よりも実際に設定して動きましたから。

ただ、"動いたから" だけじゃ不安ではあります。
たまたま動いただけかもしれないし。
なので、改めてここで追ってみてると...

OutputStreamAppender にて、
encoder変数の代入箇所を探したら、
new LayoutWrappingEncoder() が見つかります。

逆に LayoutWrappingEncoder の方から、
(Eclipseなら) ctrl+shift+G して探せば、
OutputStreamAppender が簡単に見つかります。

もちろん、さっきの時点でしっかり追って、
それを確定させてから追っても良いでしょう。
ただ、焦点を絞って読んでいるときは、
ある程度は推測で進んで早く到達するやり方も大切で、
「pattern も charset もある」
「そして実際に設定して試したら動く」
というのを先に検証してゴールを見定めてから、
詳細を探すほうが当たりが付けやすくなって、
結果的に早いということもあります。

というのは、encoderの実体が、
簡単に見つからない可能性も十分にあり、
そこでハマって時間ロスの可能性もあるからです。
もし最終的にもencoderの実体がわからなくても、
実際に試して動いたことで、
焦点を絞った検索で世の文献を探すことができて、
このやり方で問題ないと業務的に判断できれば、
別にそれでいいわけです。
今の論点はencoderの実体を知ることじゃないので。

「推測・検証の方が気軽にできるのであれば、
とっととやってしまったほうが良い」

というのも一つのコツです。

# 一方で、
# encoderの実体探しでサクッと見つからないな...
# じゃあすぐに切り替えて推測・検証だ、
# ってのを、安定してできるのが一番ではありますが。

ConsoleAppenderでも

ConsoleAppenderでも、
同じように設定ができます。

実際に、設定して SJIS に設定すると、
UTF-8 のつもりで表示してるコンソールで文字化けします。

なので、Example の方では、
ConsoleAppender にも同じように設定しています。

コード的には、
ConsoleAppenderのコードを開けば、
すぐにわかります。
開いた瞬間に目の前に入ってきた情報で、
「あー、はいはい」
となりますから。

LastaFluteユーザーの方へ

というわけで...
見事に、LastaFlute の Example で抜けておりました。
いまここで懺悔します。。。

なので...
Example からスタートアップされた方、
Example を参考に logback.xml を構築された方、
ぜひ、エンコーディング指定の移行をオススメします。

こちらの変更履歴を参考に:
 => add log.file.encoding for FileAppender
 => also add log.file.encoding to ConsoleAppender

jfluteの知ってる限りのほとんどの現場では、
サーバーが Linux ばかりなので!?
全く問題なく UTF-8 で出力されていますが、
それもあくまで環境依存なので、
明示的に指定する方が将来的に安心でしょう。

# 例えば、Windows Server だったら、
# SJIS (MS932) に、なっちゃうかも!?
#  => ファイル操作/デフォルトのファイル文字コードを確認する