SLF4J+Logbackとは、コードはSLF4J(ログの共通API)だけに書き、出力先・書式・保存期間はLogbackの設定ファイル logback.xml で決める、Javaの定番ログ構成です。コードを変えずに「開発はコンソールにDEBUGまで」「本番はファイルに日付とサイズでローテーション」と切り替えられます。

この記事では、SLF4J 2.x+Logback 1.5系(後継の1.6系もそのまま使えます)向けに、コピペで使える開発用・本番用 logback.xml の全文、パターン書式の一覧、{} と文字列連結の違い、「ログが出ない」原因一覧までをまとめました。コードと設定は Java 21.0.10 で実際に動かし、出力をそのまま掲載しています。

動作確認環境:OpenJDK 21.0.10、slf4j-api 1.7.32、logback-classic 1.2.11(Ubuntu の apt パッケージ)。SLF4J 2.x/Logback 1.5・1.6 で異なる箇所は、公式ドキュメントで確認した内容を注記しています。appender・rollingPolicy・encoder・パターンの書き方は1.2と1.5で共通です。Java 25 でも書き方は変わりません(Logback 1.6 は JDK 11 以上で動作と公式に記載。Java 25 での実行は未確認)。

SLF4J+Logback設定の早見表

やりたいこと 書き方
依存関係を入れる ch.qos.logback:logback-classic だけ追加(slf4j-api は自動で入る)
設定ファイルの置き場所 src/main/resources/logback.xml(テストは src/test/resources/logback-test.xml)
ロガーを作る private static final Logger log = LoggerFactory.getLogger(Foo.class);
値を埋め込む log.info("id={} qty={}", id, qty);(文字列連結ではなく {})
例外をスタックトレースつきで出す log.error("失敗 id={}", id, e);(例外は最後の引数)
ファイルを日付とサイズで分割 RollingFileAppender+SizeAndTimeBasedRollingPolicy(%d と %i 両方必須)
保存期間・合計容量の上限 maxHistory(周期の数)/totalSizeCap(合計サイズ)
書き込みを非同期にする AsyncAppender(既定キュー256件、残り20%でINFO以下を捨てる)
リクエストIDを全ログに付ける MDC.put("requestId", id)+パターンの %X{requestId}
パッケージ単位でレベルを変える <logger name="org.apache" level="WARN"/>
同じログが2回出るのを止める <logger ... additivity="false">
設定の読み込み状況を見る <configuration debug="true">
JSONで出力する 標準 JsonEncoder(1.3.8/1.4.8以降)か logstash-logback-encoder
Spring Bootで環境別に切り替える logback-spring.xml の <springProfile name="prod">

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SLF4J+Logback設定|logback.xmlテンプレートとローテーション」

SLF4JとLogbackの関係

SLF4Jは「ログを書く窓口(API)」、Logbackは「実際に書き出す実装」です。アプリもライブラリもSLF4Jにだけ依存し、実装はクラスパスに置いたものが使われます。

部品 役割
Logger どのクラスのログを、どのレベル以上で通すか(<logger>、<root>)
Appender どこへ書くか(コンソール、ファイル、非同期)
Encoder どんな書式で書くか(%d %-5level %msg%n)

SLF4J 1.7 と 2.x の違い(プロバイダ方式)

SLF4J 2.0から、実装の見つけ方が「StaticLoggerBinder」から「ServiceLoader(プロバイダ)」に変わりました。公式のエラーコード解説(slf4j.org/codes.html)でも、2.0.x 以降は static binder の仕組みを参照しないと説明されています。

項目 SLF4J 1.7.x SLF4J 2.0.x
実装の見つけ方 org.slf4j.impl.StaticLoggerBinder ServiceLoader で SLF4JServiceProvider
対応するLogback 1.2.x 1.3.x 以降(1.5/1.6 が現行)
実装がないときの警告 Failed to load class "org.slf4j.impl.StaticLoggerBinder" No SLF4J providers were found.
実装が複数あるときの警告 Class path contains multiple SLF4J bindings. Class path contains multiple SLF4J providers.
fluent API・Key-Value なし atInfo()、addKeyValue()

slf4j-api 2.x に logback 1.2 を組み合わせると、プロバイダが見つからずログが出ません。Logback 1.5/1.6 を使えば、適切な slf4j-api 2.x が推移的に入ります。

導入:依存関係と設定ファイルの読み込み順

依存関係は logback-classic を1つ書くだけです。2026-10-02時点の公式ニュースでは最新が 1.6.5、1.5系の最終が 1.5.38 です。1.6系は「1.5系のドロップイン置き換え(Janino による <if> 条件式は削除)」とされているので、1.5向けの logback.xml は1.6でもそのまま使えます。

<!-- pom.xml(Gradle なら implementation 'ch.qos.logback:logback-classic:1.5.38') -->
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.5.38</version>
</dependency>

Logback は次の順で設定を探し、最初に見つかったものを使います(Logback 1.5 公式マニュアル)。

順 探すもの
1 ServiceLoader で見つかる独自 Configurator
2 システムプロパティ logback.configurationFile
3 クラスパス上の logback-test.xml
4 クラスパス上の logback.xml
5 properties 形式の設定(1.5.8以降)
6 BasicConfigurator(コンソールに DEBUG 以上)

設定ファイルなしで実行すると6番になり、DEBUG まで出ます。

04:51:42.056 [main] DEBUG OrderApp - 注文を受け付けました: Order[id=A-100, qty=3]
04:51:42.071 [main] INFO OrderApp - 注文を確定しました id=A-100 qty=3

本番で DEBUG が大量に出るときは、logback.xml がクラスパスに入っていない可能性が高いです。

基本:ロガーの作り方とログの書き方

コピペで動くサンプル(OrderApp.java)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class OrderApp {
    private static final Logger log = LoggerFactory.getLogger(OrderApp.class);

    record Order(String id, int qty) {}

    public static void main(String[] args) {
        MDC.put("requestId", "req-0001");
        MDC.put("userId", "u-42");
        try {
            Order order = new Order("A-100", 3);
            log.trace("trace: {}", order);
            log.debug("注文を受け付けました: {}", order);
            log.info("注文を確定しました id={} qty={}", order.id(), order.qty());
            log.warn("在庫が残りわずかです sku={} remaining={}", "SKU-9", 2);
            try {
                Integer.parseInt("abc");
            } catch (NumberFormatException e) {
                // 最後の引数に例外を渡すとスタックトレースが出る
                log.error("数量の変換に失敗しました input={}", "abc", e);
            }
        } finally {
            MDC.clear();
        }
        log.info("MDCクリア後のログ");
    }
}

実行は java -Dlogback.configurationFile=logback-dev.xml -cp slf4j-api.jar:logback-classic.jar:logback-core.jar OrderApp.java です(単一ファイル実行)。

ロガーは private static final でクラスごとに1つ、名前はクラスの完全修飾名にします。ロガー名がパッケージ階層になるので、<logger name="com.example.order"> のようにパッケージ単位でレベルを変えられます(Lombok の @Slf4j も同じフィールドを生成します)。

プレースホルダ {} と文字列連結の違い(実測)

{} を使うと、そのレベルが無効なときはメッセージの組み立て(toString や連結)が行われません。root=INFO(DEBUG無効)で、debug を100万回呼んで比べました。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CostDemo {
    private static final Logger log = LoggerFactory.getLogger(CostDemo.class);
    static int toStringCalls = 0, heavyCalls = 0;

    static class Item {
        @Override public String toString() { toStringCalls++; return "Item#1"; }
    }
    static String heavy() { heavyCalls++; return "heavy-result"; }

    public static void main(String[] args) {
        Item item = new Item();
        long t0 = System.nanoTime();
        for (int i = 0; i < 1_000_000; i++) log.debug("連結: " + item + " i=" + i);
        System.out.printf("文字列連結      : %4d ms, toString=%d回%n", (System.nanoTime() - t0) / 1_000_000, toStringCalls);

        toStringCalls = 0;
        t0 = System.nanoTime();
        for (int i = 0; i < 1_000_000; i++) log.debug("プレースホルダ: {} i={}", item, i);
        System.out.printf("プレースホルダ  : %4d ms, toString=%d回%n", (System.nanoTime() - t0) / 1_000_000, toStringCalls);

        for (int i = 0; i < 3; i++) log.debug("結果: {}", heavy());
        System.out.printf("{} でも heavy() は %d回 呼ばれた%n", heavyCalls);

        heavyCalls = 0;
        for (int i = 0; i < 3; i++) {
            if (log.isDebugEnabled()) log.debug("結果: {}", heavy());
        }
        System.out.printf("isDebugEnabled で囲むと heavy() は %d回%n", heavyCalls);
    }
}

実行結果(3回実行し、連結は171〜329ms、プレースホルダは11〜18msでした):

文字列連結      :  319 ms, toString=1000000回
プレースホルダ  :   11 ms, toString=0回
{} でも heavy() は 3回 呼ばれた
isDebugEnabled で囲むと heavy() は 0回
書き方 無効時の toString/連結 引数の式の評価 使いどころ
log.debug("x=" + x) 毎回実行 実行 避ける
log.debug("x={}", x) 実行されない 実行 基本はこれ
if (log.isDebugEnabled()) {...} 実行されない 実行されない 引数の計算が重いとき
log.atDebug().addArgument(() -> heavy())(2.x) 実行されない 実行されない 2.x での isDebugEnabled の代わり

{} は toString を遅らせるだけで、引数の式(heavy())は評価されます。重い処理を引数に書くときは isDebugEnabled() で囲みます。

例外は最後の引数に渡す

上の CostDemo の main メソッドの末尾に次のコードを追加して実行した結果です。

Exception e = new IllegalStateException("在庫数が負になりました");
log.error("①メッセージだけ: " + e);
log.error("②{} に例外: {}", "SKU-1", e.getMessage());
log.error("③最後の引数に例外 sku={}", "SKU-1", e);
ERROR CostDemo - ①メッセージだけ: java.lang.IllegalStateException: 在庫数が負になりました
ERROR CostDemo - ②SKU-1 に例外: 在庫数が負になりました
ERROR CostDemo - ③最後の引数に例外 sku=SKU-1
java.lang.IllegalStateException: 在庫数が負になりました
    at CostDemo.main(CostDemo.java:41)
    ...

スタックトレースが出るのは③だけです。SLF4J は「{} の数より引数が1つ多く、最後が Throwable なら例外として扱う」ので、catch したら log.error("説明 key={}", value, e) の形にそろえます。例外処理の全体像は「Javaの例外処理」記事も参考にしてください。

SLF4J 2.x の fluent API(公式で確認・未実行)

手元の slf4j-api 1.7 にはないため、SLF4J 公式マニュアルに沿って記載します。

log.atInfo()
   .setMessage("注文を確定しました")
   .addKeyValue("orderId", order.id())
   .addKeyValue("qty", order.qty())
   .log();

log.atDebug().setMessage("集計結果={}").addArgument(() -> heavy()).log(); // 無効時は計算されない
log.atError().setCause(e).log("在庫の更新に失敗しました sku={}", sku);

addKeyValue は JSON 出力と相性がよく、後述の JsonEncoder では kvpList に出力されます。従来の log.info("...{}", x) も 2.x でそのまま使えます。

開発用 logback.xml(全文)

開発用は「コンソールに色つき、自分のパッケージは DEBUG まで」が使いやすい設定です。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{36}) - %msg [%X{requestId}]%n</pattern>
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <!-- 自分のコードだけ DEBUG(普段は com.example などパッケージ名) -->
  <logger name="OrderApp" level="DEBUG"/>

  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
  </root>
</configuration>

実行結果(実際はレベル名とロガー名に色が付きます。色コードは除いて掲載):

04:49:52.253 DEBUG [main] OrderApp - 注文を受け付けました: Order[id=A-100, qty=3] [req-0001]
04:49:52.275 INFO  [main] OrderApp - 注文を確定しました id=A-100 qty=3 [req-0001]
04:49:52.275 WARN  [main] OrderApp - 在庫が残りわずかです sku=SKU-9 remaining=2 [req-0001]
04:49:52.282 ERROR [main] OrderApp - 数量の変換に失敗しました input=abc [req-0001]
java.lang.NumberFormatException: For input string: "abc"
    at OrderApp.main(OrderApp.java:20)
    ...
04:49:52.283 INFO  [main] OrderApp - MDCクリア後のログ []

TRACE は OrderApp のレベルが DEBUG なので出ません。レベルは TRACE < DEBUG < INFO < WARN < ERROR の順で、設定したレベル以上だけが通ります。どのレベルに何を書くかは、別記事「ログレベルの使い分け」で扱っています。

本番用 logback.xml(全文テンプレート)

本番用は「ファイルに書く・日付とサイズで分割・自動削除・非同期・ERRORは別ファイル」をまとめます。値は ${名前:-既定値} で、システムプロパティや環境変数から上書きできます。

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
  <property name="LOG_DIR" value="${LOG_DIR:-logs}"/>
  <property name="APP_NAME" value="${APP_NAME:-order-app}"/>
  <property name="PATTERN"
            value="%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %-5level [%thread] %logger{40} [req=%X{requestId:-none} user=%X{userId:-none}] - %msg%n"/>

  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_DIR}/${APP_NAME}.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <!-- 日付ごと、maxFileSize を超えたら .0 .1 ... と分割し gzip 圧縮 -->
      <fileNamePattern>${LOG_DIR}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
      <maxFileSize>${LOG_MAX_FILE_SIZE:-100MB}</maxFileSize>
      <maxHistory>${LOG_MAX_HISTORY:-30}</maxHistory>
      <totalSizeCap>${LOG_TOTAL_SIZE_CAP:-3GB}</totalSizeCap>
      <cleanHistoryOnStart>true</cleanHistoryOnStart>
    </rollingPolicy>
    <encoder>
      <pattern>${PATTERN}</pattern>
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <!-- ERROR だけ別ファイルにも出す -->
  <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_DIR}/${APP_NAME}-error.log</file>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>ERROR</level>
    </filter>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <fileNamePattern>${LOG_DIR}/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
      <maxFileSize>50MB</maxFileSize>
      <maxHistory>90</maxHistory>
      <totalSizeCap>1GB</totalSizeCap>
    </rollingPolicy>
    <encoder>
      <pattern>${PATTERN}</pattern>
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <!-- 書き込みを別スレッドに任せる -->
  <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
    <queueSize>8192</queueSize>
    <discardingThreshold>0</discardingThreshold>
    <neverBlock>false</neverBlock>
    <includeCallerData>false</includeCallerData>
    <appender-ref ref="FILE"/>
  </appender>

  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>WARN</level>
    </filter>
    <encoder>
      <pattern>${PATTERN}</pattern>
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <logger name="org.apache" level="WARN"/>
  <logger name="com.zaxxer.hikari" level="INFO"/>

  <root level="${LOG_LEVEL:-INFO}">
    <appender-ref ref="ASYNC_FILE"/>
    <appender-ref ref="ERROR_FILE"/>
    <appender-ref ref="CONSOLE"/>
  </root>

  <!-- 終了時に Logback を止め、非同期キューを書き切る(1.3以降の書き方) -->
  <shutdownHook/>
</configuration>
java -Dlogback.configurationFile=/etc/order-app/logback.xml \
     -DLOG_DIR=/var/log/order-app -DLOG_LEVEL=INFO -jar order-app.jar

OrderApp をこの設定で動かした logs/order-app.log の中身です(1.2.11 では shutdownHook の行を後述のとおり書き換えて実行)。

2026-10-02T04:49:55.957+09:00 INFO  [main] OrderApp [req=req-0001 user=u-42] - 注文を確定しました id=A-100 qty=3
2026-10-02T04:49:55.975+09:00 WARN  [main] OrderApp [req=req-0001 user=u-42] - 在庫が残りわずかです sku=SKU-9 remaining=2
2026-10-02T04:49:55.992+09:00 ERROR [main] OrderApp [req=req-0001 user=u-42] - 数量の変換に失敗しました input=abc
java.lang.NumberFormatException: For input string: "abc"
    ...
2026-10-02T04:49:55.992+09:00 INFO  [main] OrderApp [req=none user=none] - MDCクリア後のログ

コンソールには WARN と ERROR だけが出て、ERROR は order-app-error.log(813バイト)にも書かれました。MDC クリア後は %X{requestId:-none} の既定値 none になっています。

設定 意味
scan="true" 設定ファイルの変更を検知して再読み込み(既定は1分ごと)。レベル変更に再起動が不要
%d{yyyy-MM-dd} 日付が変わったら切り替え(パターンの最小単位が周期)
%i 同じ周期内で maxFileSize を超えたら 0, 1, 2…と分割
.gz/.zip 末尾に付けると切り替え時に圧縮
maxHistory 周期(この例では日)の数でアーカイブを削除
totalSizeCap アーカイブ全体の合計上限。超えたら古い順に削除(maxHistory と併用)
ThresholdFilter 指定レベル以上だけそのアペンダーに通す

shutdownHook はバージョンで書き方が違う

Logback 1.3 以降は <shutdownHook/> だけで DefaultShutdownHook が使われます(公式マニュアル)。手元の 1.2.11 で同じ書き方をすると次のエラーになりました。

|-ERROR in ch.qos.logback.core.joran.action.ShutdownHookAction - Missing class name for shutdown hook. Near [shutdownHook] line 74

DefaultShutdownHook を明示しても 1.2.11 には存在せず ClassNotFoundException になります。1.2 系では <shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/> と書きます。設定にエラーがあると Logback は内部ステータスを自動で表示するので、まずそこを読むのが近道です。

ローテーション:日付×サイズの動きを実行して確かめる

SizeAndTimeBasedRollingPolicy は「日付で切り替え、同じ日に大きくなったら連番で分割」します。%d と %i は両方必須です。maxFileSize を 50KB、totalSizeCap を 150KB にして、約120バイトの行を2,000行書きました。

設定は本番テンプレートの FILE アペンダーと同じ形で、<file>roll/app.log</file>、<fileNamePattern>roll/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>、maxFileSize 50KB、maxHistory 7、totalSizeCap 150KB にしています。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RollDemo {
    private static final Logger log = LoggerFactory.getLogger(RollDemo.class);
    public static void main(String[] args) throws Exception {
        String pad = "x".repeat(100);
        for (int i = 1; i <= 2000; i++) {
            log.info("item={} {}", i, pad);
            Thread.sleep(1);
        }
    }
}

実行後のファイルと先頭行(Logback 1.2.11):

$ ls -l roll
-rw-r--r-- 1 root root 58368 Oct  2 04:50 app.2026-10-02.3.log
-rw-r--r-- 1 root root 58368 Oct  2 04:50 app.2026-10-02.4.log
-rw-r--r-- 1 root root 12312 Oct  2 04:50 app.log
roll/app.2026-10-02.3.log: INFO  RollDemo - item=1152
roll/app.2026-10-02.4.log: INFO  RollDemo - item=1536
roll/app.log: INFO  RollDemo - item=1920
  1. 連番 .0〜.2 は totalSizeCap(150KB)を超えたため古い順に自動削除され、.3 と .4 が残りました。
  2. 1ファイルが 58KB と、maxFileSize(50KB)を少し超えました。1.2系はサイズ確認を毎回は行わないためで、スリープなしで一気に書くと1ファイル148KBまで膨らみました。
  3. Logback 1.5.8 以降は書き込んだバイト数を数える方式になり、checkIncrement は不要(非推奨)と公式マニュアルにあります。ただし「切り替えはログが書かれたときに行われ、一時的に超えることはある」とも書かれているので、ディスク見積もりには余裕を持たせると安心です。
周期・方式 fileNamePattern/ポリシー
日ごと+サイズ(迷ったらこれ) app.%d{yyyy-MM-dd}.%i.log.gz+SizeAndTimeBasedRollingPolicy
日付のみ app.%d{yyyy-MM-dd}.log+TimeBasedRollingPolicy
日付フォルダに分ける %d{yyyy/MM, aux}/app.%d{yyyy-MM-dd}.%i.log.gz(aux は周期に使わない補助日付)

OS の logrotate と Logback のローテーションはどちらか一方にします。logrotate 側で管理する場合は「logrotateの設定方法」記事を参照してください。

AsyncAppender:非同期で書くときの設定

AsyncAppender はログをキューに積み、別スレッドで書き出します。公式マニュアルで確認した既定値です。

プロパティ 既定値 意味
queueSize 256 キューの最大件数
discardingThreshold キューの20% 残りがこれを下回ると TRACE・DEBUG・INFO を捨て、WARN・ERROR だけ残す
neverBlock false 満杯時に false は待つ、true は捨てて進む
includeCallerData false 呼び出し元(クラス・メソッド・行番号)を取るか
maxFlushTime 未指定 停止時にキューを書き切るまで待つ最大ミリ秒

既定のままだと混雑時に INFO が黙って捨てられます。INFO も残したいなら discardingThreshold を 0 に、アプリの応答を優先するなら neverBlock を true にします。

includeCallerData=false だと行番号が「?」になる

パターンを %-5level [%thread] %class{0}.%method:%line - %msg%n にして AsyncAppender 経由で出した結果です。

### includeCallerData=false(既定)
INFO  [main] ?.?:? - 注文を確定しました id=A-100 qty=3
WARN  [main] ?.?:? - 在庫が残りわずかです sku=SKU-9 remaining=2

### includeCallerData=true
INFO  [main] OrderApp.main:17 - 注文を確定しました id=A-100 qty=3
WARN  [main] OrderApp.main:18 - 在庫が残りわずかです sku=SKU-9 remaining=2

非同期では呼び出し元情報が既定で取得されません。取得コストがかかるため、本番は %logger とメッセージで場所がわかる書き方にし、%line は使わないのが一般的です。スレッド名と MDC は非同期でも正しく引き継がれます。

MDC:リクエストIDを全ログに付ける

MDC(Mapped Diagnostic Context)は、スレッドごとに持てる「ログ用の付箋」です。処理の入口で MDC.put すると、以降のログに %X{キー} で出せます。

MDC.put("requestId", UUID.randomUUID().toString());
try {
    chain.doFilter(request, response);
} finally {
    MDC.remove("requestId"); // スレッドの再利用で値が残らないよう必ず消す
}

finally で消すことが大切です。スレッドプールのスレッドは再利用されるため、消し忘れると別リクエストのログに前の ID が付きます。ExecutorService などに処理を渡すと MDC は別スレッドに自動では引き継がれないので、MDC.getCopyOfContextMap() で取り出し、タスク内で MDC.setContextMap() します。なお Logback 1.5.22 以降は、名前に password・secret・confidential を含む MDC キーを出力しない変更が入っています(公式ニュース)。

パターン書式の一覧(実行結果つき)

com.example.order.service.PatternDemo で log.info("在庫を引き当てました count={}", 5)(MDC に requestId=req-7)を、書式を1つずつ変えて出力した結果です。

書式 出力(実行結果) 説明
%d 2026-10-02 04:52:21,318 日時(ミリ秒の区切りはカンマ)
%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} 2026-10-02T04:52:22.814+09:00 タイムゾーン付き。本番向き
%-5level INFO 5文字左寄せで列がそろう
%thread main スレッド名
%logger com.example.order.service.PatternDemo ロガー名
%logger{20} c.e.o.s.PatternDemo 20文字に収まるようパッケージを省略
%logger{0} PatternDemo クラス名だけ
%msg 在庫を引き当てました count=5 メッセージ
%X{requestId} req-7 MDC の値
%X{userId:-none} none MDC の値(なければ既定値)
%mdc requestId=req-7 MDC 全部
%class{0}.%method:%line PatternDemo.main:9 呼び出し元(重い)
%replace(%msg){"\d+","#"} (数字を置換) 正規表現で置換。マスキングに使える
%ex{short} 例外の1行目+先頭フレーム スタックトレースを短く

「ログが出ない」「二重に出る」原因一覧と再現

症状 出るメッセージ 原因 直し方
何も出ない(1.7) Failed to load class "org.slf4j.impl.StaticLoggerBinder" 実装がない logback-classic を追加
何も出ない(2.x) No SLF4J providers were found. 実装がない/slf4j-api 2.x+logback 1.2 Logback 1.5/1.6 にそろえる
書式が違う multiple SLF4J bindings(2.x は providers) slf4j-simple などが混在 mvn dependency:tree -Dincludes=org.slf4j で探して exclusion
アプリのログだけ消えた Could not find an appender named [...] appender-ref の名前違い 名前を合わせる
DEBUG が出ない なし ロガーまたは root が INFO <logger name="com.example" level="DEBUG"/>
設定変更が効かない Found resource [...] の場所が違う 別の logback-test.xml が優先 debug="true" で確認
同じ行が2回出る なし ロガーと root の両方に同じアペンダー additivity="false"
終了直前のログが欠ける なし 非同期キューを書き切っていない <shutdownHook/>

実装がない/複数ある(SLF4J 1.7 の実行結果)

slf4j-api だけで実行すると、ログは1行も出ず警告だけが出ます。

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

slf4j-simple と logback-classic を両方置いた結果です。

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/usr/share/java/slf4j-simple.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/usr/share/java/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Actual binding is of type [org.slf4j.impl.SimpleLoggerFactory]
[main] INFO OrderApp - 注文を確定しました id=A-100 qty=3

slf4j-simple が選ばれ、logback.xml は使われていません。どちらになるかはクラスパス順で決まるため、実装は1つだけにします。SLF4J 2.x では同じ状況で No SLF4J providers were found./Class path contains multiple SLF4J providers. が出ます(公式 codes.html。どちらも例外ではなく警告なので、起動直後の SLF4J: の行は必ず確認しましょう)。

appender-ref の名前違い

<appender-ref ref="CONSOL"/> と1文字違えた結果です。

|-ERROR in ch.qos.logback.core.joran.action.AppenderRefAction - Could not find an appender named [CONSOL]. Did you define it below instead of above in the configuration file?
...
|-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.

この後アプリのログは1行も出ませんでした。ログが出ないときは、起動直後の |-ERROR in/|-WARN in の行を探します。

同じログが2回出る(additivity)

<logger name="OrderApp" level="INFO">
  <appender-ref ref="CONSOLE"/>
</logger>
<root level="WARN"><appender-ref ref="CONSOLE"/></root>
INFO  OrderApp - 注文を確定しました id=A-100 qty=3
INFO  OrderApp - 注文を確定しました id=A-100 qty=3
WARN  OrderApp - 在庫が残りわずかです sku=SKU-9 remaining=2
WARN  OrderApp - 在庫が残りわずかです sku=SKU-9 remaining=2

ロガーのログは自分のアペンダーに書いた後、親(root)のアペンダーにも渡されます。しかも root を WARN にしても INFO が root 経由で出ています。レベル判定はログを出したロガー(OrderApp=INFO)で1回だけ行われ、親では再判定されないためです。additivity="false" を付けると各行1回になりました。レベルだけ変えたいロガーにはアペンダーを付けず <logger name="..." level="DEBUG"/> とだけ書くのが簡単な防ぎ方です。

JSONで出力する(公式で確認・未実行)

ログ収集基盤に送るなら 1行1JSON が扱いやすくなります。持たせる項目の設計は別記事「構造化ログ(JSON)設計」で扱っているので、ここでは設定方法だけ示します。手元の 1.2.11 には JsonEncoder がないため、公式ドキュメントに沿った未実行の設定です。

<!-- Logback 標準(1.3.8/1.4.8 以降)。FILE アペンダーの encoder を差し替える -->
<encoder class="ch.qos.logback.classic.encoder.JsonEncoder">
  <withFormattedMessage>true</withFormattedMessage>
  <withMessage>false</withMessage>
  <withArguments>false</withArguments>
</encoder>

<!-- logstash-logback-encoder(対応バージョンは公式 README で確認) -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <customFields>{"app":"order-app"}</customFields>
</encoder>
方法 追加依存 特徴
JsonEncoder(標準) なし timestamp、level、loggerName、mdc、kvpList、message、throwable などを出力。1.5.0以降は <withXxx> で項目を切り替え
logstash-logback-encoder あり フィールド名の変更・追加など柔軟
Spring Boot の構造化ログ なし Boot 3.4以降、logging.structured.format.console などで形式を指定

Spring Boot の logback-spring.xml との違い

Spring Boot では logback-spring.xml という名前にすると、プロファイル切り替えや application.yml の値の参照が使えます。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
  <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
  <springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="app"/>

  <springProfile name="local | dev">
    <logger name="com.example" level="DEBUG"/>
    <root level="INFO"><appender-ref ref="CONSOLE"/></root>
  </springProfile>

  <springProfile name="prod">
    <!-- 本番用テンプレートの FILE / ASYNC_FILE をここに置く -->
    <root level="INFO"><appender-ref ref="CONSOLE"/></root>
  </springProfile>
</configuration>
項目 logback.xml logback-spring.xml
読み込み Logback が起動直後に読む Spring Boot が初期化の途中で読む
<springProfile> 使えない 使える(name="dev | local"、!prod)
<springProperty> 使えない application.yml の値を変数にできる
Boot の既定書式 defaults.xml 自分で書く <include> で使える
Spring Boot での推奨 動くが Spring 機能なし 公式で推奨

Spring Boot アプリで logback.xml という名前にすると、Spring より先に読まれるため <springProfile> が効きません。レベル変更だけなら application.yml の logging.level.com.example: DEBUG で十分です。プロファイルの切り替え方は「Spring Bootプロファイル切り替え」記事を参照してください。Spring Boot の jar が手元にないため、この節は Spring Boot 公式リファレンスに沿った未実行の内容です。

よくある質問

Q. SLF4J だけ入れれば Logback の設定は不要ですか? A. いいえ。SLF4J は API だけなので、実装がないと警告が出てログは出ません。logback-classic を依存に追加すれば slf4j-api も一緒に入ります。

Q. maxHistory と totalSizeCap はどちらを設定すればよいですか? A. 両方設定するのがおすすめです。maxHistory は「何周期分残すか」、totalSizeCap は「アーカイブ合計の上限」で、公式マニュアルでは totalSizeCap は maxHistory とあわせて使うと説明されています。ログが急に増えた日もディスクを使い切らずに済みます。

Q. 非同期にすると何が変わりますか? A. アプリのスレッドがファイル書き込みを待たなくなり、応答時間が安定しやすくなります。一方で既定では混雑時に INFO 以下が捨てられ、行番号は ? になります。discardingThreshold=0 と <shutdownHook/> を組み合わせてください。

Q. logback.xml と logback-spring.xml を両方置いたらどうなりますか? A. 標準名の logback.xml が先に使われます。Spring Boot アプリでは logback-spring.xml だけを置くと混乱がありません。

まとめ

  • SLF4J は API、Logback は実装。依存は logback-classic だけで、Logback 1.5/1.6 には slf4j-api 2.x を組み合わせる
  • ログは {} で書く。100万回の DEBUG 呼び出しで、連結は toString が100万回、{} は0回だった
  • 例外は最後の引数に渡すとスタックトレースが出る。重い引数は isDebugEnabled() か 2.x の Supplier で
  • 本番は SizeAndTimeBasedRollingPolicy。%d と %i が必須、maxHistory と totalSizeCap を両方設定
  • AsyncAppender は既定で INFO 以下を捨てることがある。discardingThreshold=0 と <shutdownHook/> をセットで
  • ログが出ないときは起動直後の SLF4J: と |-ERROR in を読む。実装なし・複数実装・名前違い・additivity が代表例
  • Spring Boot では logback-spring.xml にすると <springProfile> で環境別に切り替えられる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SLF4J+Logback設定|logback.xmlテンプレートとローテーション」

株式会社RJCでは、SLF4JとLogbackによるログ設定を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?