概要

ログは障害調査の生命線です。運用中に問題が起きたとき、適切なログが残っていなければ原因の特定に何時間もかかることがあります。Java には java.util.logging(JUL)が標準で組み込まれており、外部ライブラリなしでログ出力の基盤を作れます。ただし、JUL はデフォルト設定のまま使うと INFO 未満のログが出力されない、ルートロガーとの二重出力が発生する、例外のスタックトレースが切れるなど、意図しない挙動に悩まされることが少なくありません。この記事では、ロガーの取得とハンドラーの設定、ログレベル(SEVERE〜FINEST)の使い分けの基準、例外をスタックトレースごと記録する正しい方法、そしてデバッグログの遅延評価によるパフォーマンス配慮までを一通り整理します。業務システムで最低限押さえておくべきログ設計の出発点として使ってください。

使いどころ

業務バッチの開始・終了・処理件数を INFO レベルで記録し、夜間バッチの正常完了を運用チームが確認できるようにする

例外発生時に SEVERE レベルでスタックトレースごとログに残し、障害調査で原因箇所を特定する

パフォーマンスチューニング時に FINE レベルでSQL実行時間を記録し、通常運用時はログに出さない切り替えを行う

コード例

LoggingBasicsExample.java
import java.util.List;
import java.util.logging.*;

public class LoggingBasicsExample {

    private static final Logger logger =
            Logger.getLogger(LoggingBasicsExample.class.getName());

    record LogEntry(Level level, String message) {}

    /** コンソールハンドラーを設定 */
    public static void setupLogger() {
        // ルートロガーのデフォルトハンドラーを除去
        var rootLogger = Logger.getLogger("");
        for (var handler : rootLogger.getHandlers()) {
            rootLogger.removeHandler(handler);
        }

        var consoleHandler = new ConsoleHandler();
        consoleHandler.setLevel(Level.ALL);
        consoleHandler.setFormatter(new SimpleFormatter());

        logger.addHandler(consoleHandler);
        logger.setLevel(Level.ALL);
        logger.setUseParentHandlers(false);
    }

    /** 各ログレベルの出力例 */
    public static void demonstrateLevels() {
        var entries = List.of(
                new LogEntry(Level.SEVERE, "システム障害・即時対応が必要"),
                new LogEntry(Level.WARNING, "想定外だが処理は継続可能"),
                new LogEntry(Level.INFO, "正常な業務ログ"),
                new LogEntry(Level.FINE, "デバッグ情報")
        );
        entries.forEach(e -> logger.log(e.level(), e.message()));
    }

    /** 例外をスタックトレースごとログに記録 */
    public static void logWithException() {
        try {
            int result = 10 / 0;
            System.out.println(result);
        } catch (ArithmeticException e) {
            // e.printStackTrace() は使わない
            logger.log(Level.SEVERE, "計算処理でエラー発生", e);
        }
    }

    /** デバッグログの遅延評価 */
    public static void lazyLogging() {
        // isLoggable で事前チェックし、不要な文字列結合を回避
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("タイムスタンプ: " + System.currentTimeMillis());
        }
    }

    public static void main(String[] args) {
        setupLogger();
        logger.info("アプリケーション起動");
        demonstrateLevels();
        logWithException();
        lazyLogging();
        logger.info("アプリケーション終了");
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var でハンドラーやロガーの宣言が簡潔になる。record でログエントリを構造化すると、まとめてログ出力するパターンが書きやすくなる。

Java 17
// Java 17: var + record でログエントリを構造化
var logger = Logger.getLogger(MyClass.class.getName());
record LogEntry(Level level, String message) {}
var entries = List.of(
    new LogEntry(Level.INFO, "処理開始"),
    new LogEntry(Level.INFO, "処理完了")
);
entries.forEach(e -> logger.log(e.level(), e.message()));

Library Comparison

標準 API(java.util.logging)外部依存なしでログ出力を実装したいとき。小規模プロジェクトやプロトタイプで十分に機能する。設定が XML ベースで直感的でなく、SLF4J や Logback に比べて柔軟性が劣る。大規模プロジェクトでは物足りなくなることがある。
SLF4J + Logbackファサードパターンでログ実装を切り替え可能にしたいとき。MDC やマーカーなど高度な機能も使える。依存が増えるが、業務システムではデファクトスタンダード。JUL からの移行パスも用意されている。
Log4j2非同期ログや構造化ログ(JSON 出力)が必要なとき。高スループットのアプリケーションに適している。設定の複雑さと過去の脆弱性(Log4Shell)のイメージがある。最新バージョンでは修正済みだが、セキュリティレビューでの説明コストが発生する場合がある。

注意点

Logger.getLogger("") でルートロガーを取得してハンドラーを追加すると、全ロガーに影響する。クラス専用のロガーに addHandler し、setUseParentHandlers(false) で親への伝播を止めるのが安全

logger.info("値: " + expensiveComputation()) のように書くと、INFO が無効でも文字列結合と関数呼び出しが実行される。isLoggable() で事前チェックするか、ラムダ版の log メソッドを使うこと

e.printStackTrace() はログフレームワークを経由せず標準エラーに直接出力するため、ログレベル制御もファイル出力もできない。必ず logger.log(Level.SEVERE, message, e) を使うこと

ConsoleHandler のデフォルトレベルは INFO のため、ロガーを ALL に設定しても FINE 以下は出力されない。ハンドラー側のレベルも合わせて設定する必要がある

ロガー名にはクラスの完全修飾名を使うのが慣例。パッケージ階層に沿ったロガーツリーが構成され、レベルの一括変更やフィルタリングがしやすくなる

FAQ

java.util.logging と SLF4J のどちらを使うべきですか。

チームやプロジェクトで既に SLF4J + Logback が導入されているならそちらに合わせます。依存なしで始めたい場合や小規模なツールでは JUL で十分です。

ログレベルの FINE と FINER と FINEST はどう使い分けますか。

FINE はデバッグ情報、FINER はメソッドの入口・出口、FINEST は変数値のトレースという使い分けが一般的です。ただし実務では FINE と INFO の2段階で運用するケースが大半です。

ログにスタックトレースを含めるにはどうしますか。

logger.log(Level.SEVERE, "エラー内容", exception) のように、第3引数に Throwable を渡します。これでスタックトレース全体がログに記録されます。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます