- 技術 💻
SLF4J+Logback設定|logback.xmlテンプレートとローテーション
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
- 連番 .0〜.2 は totalSizeCap(150KB)を超えたため古い順に自動削除され、.3 と .4 が残りました。
- 1ファイルが 58KB と、maxFileSize(50KB)を少し超えました。1.2系はサイズ確認を毎回は行わないためで、スリープなしで一気に書くと1ファイル148KBまで膨らみました。
- 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期連続の黒字決算。