23
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

slf4j + logback 導入時の java.util.logging 向け対処

Last updated at Posted at 2016-07-10

アプリのログ出力を slf4j + logback の構成にするときに、依存モジュールのログ出力もslf4jに集める必要があります。
Javaには複数のロガーがあっていろいろとわかりにくい状況になっていますが、以下の記事などで状況や対処方法がまとめられています。

大体はこれらの記事の内容でいいのですがjul-to-slf4jについては導入方法などが不足しているようだったので、そのあたりを本記事でまとめました。

なお、slf4jの公式の説明は以下のページになります。

log4j と commons-logging(略称: jcl)

log4j と commons-logging のログ出力をslf4jへバイパスするには、元々のモジュールを依存から除外し、代わりに log4j-over-slf4jjcl-over-slf4j を依存に追加すれば対処できます。

これだけで対処できているのは、各モジュールのLoggerクラスの実体を挿げ替えているためです。
log4jではorg.apache.log4j.Loggerでログを出力していますが、依存モジュールを切り替えたことでlog4jのjarに存在するクラスではなくlog4j-over-slf4jのjarに含まれるクラスを使うようにしています。log4j-over-slf4jに含まれるorg.apache.log4j.Loggerは、クラスのinterfaceがlog4jの形をしていますが出力先がslf4jになっています。

java.util.logging (略称: jul)

一方で、java.util.logging (以降: jul) はこの方法が使えません。
julはJava本体に含まれるクラスであるため依存から除外するということができず、クラスを挿げ替えるという方法をとれません。
そのため、jul-to-slf4jでは事前準備処理を組み込む必要があります。組み込む内容としては以下の2つの対処を追加します。

対処 1. アプリの起動時の初期化相当の部分で以下の2行を実行すること

SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();

対処 2. logback.xmlの定義の最初に以下の記述を追記すること

logback.xml
 <configuration>
+    <!-- jul-to-slf4j の対応 ここから -->
+    <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
+        <resetJUL>true</resetJUL>
+    </contextListener>
+    <!-- ここまで -->
 
     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
             <pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern>
         </encoder>
     </appender>
 
     <logger name="sample" level="ERROR"/>
     <logger name="sample.sub" level="DEBUG"/>

     <root level="ALL">
         <appender-ref ref="STDOUT" />
     </root>
 </configuration>

これらはそれぞれ、どういう意味なのでしょうか?
それを知るためには、 まず jul でのログ出力のステップを知る必要があります。

julのログ出力の流れ

  1. Logging.fine(), finner(), info() などのログ出力メソッドが呼ばれる
  2. そのロガーの出力ログレベルと、実行されたログ出力メソッドのログレベルを比較し、
    ログ出力対象かどうかを判定する
  3. ログ出力内容を LogRecordオブジェクトとしてまとめる
  4. 作成したLogRecordを そのロガーが持つ handlerに渡す
  5. handlerは受け取ったLogRecordの内容を出力先に書き出す

この中のhandlerは、logbackなどでいう Appender に相当するものですが、jul-to-slf4jは「LogRecordを受け取るとslf4jのロガーにログ出力を渡すhandler」を使うことでログのバイパスを行っています。
この「slf4jへバイパスするhandler」を設定する処理が 対処 1 になります。
1行目の removeHandlersForRootLogger() で初期に登録されているhandlerを全て除去し、2行目の install() で slf4jバイパス用のhandlerを登録しています。

handler の挿げ替えでログの出力自体はできますが、前述のログ出力処理の Step 2 のレベル判定部分で問題が残ります。
slf4j のログレベルの設定は、通常では jul には反映されません。そのため、logback.xmlで jul が出力しているロガーに対するレベルをWARNやERRORに変更しても、Step 2の部分でのレベル判定では変更したレベル設定が利用されません。そのため、例えばINFOなどの本来制限されるレベルのログはStep.2〜5の処理をしたうえで、slf4j用handlerにLogRecordが渡ってきた時点で再度ログレベルを判定して出力するかどうかが決まる という流れになってしまいます。
その問題への対処が、対処2の意図になります。このLevelChangePropagatorを登録することで、slf4j 上でのログレベル変更(setLevel()メソッド呼び出し)が jul へ反映されるようになります。

公式ドキュメントにおいては、上記の 対処2 の内容を「性能問題への対処」として説明されていますが、それはこの点への言及かと思います。 本来 Step 2の時点で捨てられるレベルのログが、handlerに渡されるまで処理してしまう部分が性能低下になるとしているようです。

なお、実際には性能の問題だけでなく 対処 2 をしておかないと jul でのINFO未満(FINE、FINERなど)のレベルのログが出力されないという問題もあります。jul ではデフォルトのログレベルが INFO になっているため、Step 2の時点でログ出力が抑制されてしまうからです。

動作確認

実際に、LevelChangePropagatorの効果を確認するために、以下の様なサンプルを作成しました。

sample/Main.java
package sample;

import org.slf4j.bridge.SLF4JBridgeHandler;
import java.util.logging.*;

public class Main {
    // slf4jのロガーを1つでも作らないとちゃんと初期化が行われない様子
    private static org.slf4j.Logger notuse = org.slf4j.LoggerFactory.getLogger("");

    private static Logger log = Logger.getLogger("sample.Main");
    private static Logger logSub = Logger.getLogger("sample.sub.Sub");

    public static void main(String[] args) {
        // jul-to-slf4jのhandlerを設定
        SLF4JBridgeHandler.removeHandlersForRootLogger();
        SLF4JBridgeHandler.install();

        // jul で handler に渡された内容を確認するための処理
        LogManager.getLogManager().getLogger("").addHandler(new Handler() {
            @Override
            public void publish(LogRecord record) {
                System.out.printf("----- [%s]  %s : %s\n",
                        record.getLoggerName(), record.getLevel(), record.getMessage()
                );
            }

            @Override
            public void flush() {}

            @Override
            public void close() throws SecurityException {}
        });

        System.out.println("\n# MAIN");
        exec(log);
        System.out.println("\n# SUB");
        exec(logSub);
    }

    private static void exec(Logger log) {
        log.fine("fine");
        log.finer("finer");
        log.finest("finest");
        log.config("config");
        log.info("info");
        log.warning("warning");
        log.severe("severe");
    }
}

この処理を、前述のlogback.xmlを使って実行した結果が以下になります。

LevelChangePropagatorを使った場合

# MAIN
23:29:27.460 ERROR sample.Main - severe
----- [sample.Main]  SEVERE : severe

# SUB
23:29:27.467 DEBUG sample.sub.Sub - fine
----- [sample.sub.Sub]  FINE : fine
23:29:27.468 INFO  sample.sub.Sub - config
----- [sample.sub.Sub]  CONFIG : config
23:29:27.469 INFO  sample.sub.Sub - info
----- [sample.sub.Sub]  INFO : info
23:29:27.469 WARN  sample.sub.Sub - warning
----- [sample.sub.Sub]  WARNING : warning
23:29:27.470 ERROR sample.sub.Sub - severe
----- [sample.sub.Sub]  SEVERE : severe

LevelChangePropagatorを使わなかった場合

# MAIN
----- [sample.Main]  INFO : info
----- [sample.Main]  WARNING : warning
23:29:44.067 ERROR sample.Main - severe
----- [sample.Main]  SEVERE : severe

# SUB
23:29:44.071 INFO  sample.sub.Sub - info
----- [sample.sub.Sub]  INFO : info
23:29:44.073 WARN  sample.sub.Sub - warning
----- [sample.sub.Sub]  WARNING : warning
23:29:44.073 ERROR sample.sub.Sub - severe
----- [sample.sub.Sub]  SEVERE : severe

上記のとおり、LevelChangePropagatorを使った場合は期待通りの出力になっています。handlerに渡されるログも、実際に出力されるものだけです。

一方で、LevelChangePropagatorを使わなかった場合は、そもそもinfo未満のレベルのログがでていません。また、handlerに出力対象ではないレベルのログも渡されてきています。

性能の問題について

ちなみに、上記のサンプルプログラムを使って処理時間の差異を簡単にみてみたのですが、雑な計測だったからなのかイマイチ優位な違いは見て取れませんでした。

まとめ

Javaのロガーの世界はいろいろとわかりにくところがあります。
その上で、組み込み方も jul についてはちょっと対処が必要ですので、上記を参考にしていただければと思います。

23
21
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
23
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?