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

LogbackのFileAppender

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) に、なっちゃうかも!?
 => ファイル操作/デフォルトのファイル文字コードを確認する