概要
ログは障害調査の生命線です。運用中に問題が起きたとき、適切なログが残っていなければ原因の特定に何時間もかかることがあります。Java には java.util.logging(JUL)が標準で組み込まれており、外部ライブラリなしでログ出力の基盤を作れます。ただし、JUL はデフォルト設定のまま使うと INFO 未満のログが出力されない、ルートロガーとの二重出力が発生する、例外のスタックトレースが切れるなど、意図しない挙動に悩まされることが少なくありません。この記事では、ロガーの取得とハンドラーの設定、ログレベル(SEVERE〜FINEST)の使い分けの基準、例外をスタックトレースごと記録する正しい方法、そしてデバッグログの遅延評価によるパフォーマンス配慮までを一通り整理します。業務システムで最低限押さえておくべきログ設計の出発点として使ってください。
使いどころ
業務バッチの開始・終了・処理件数を INFO レベルで記録し、夜間バッチの正常完了を運用チームが確認できるようにする
例外発生時に SEVERE レベルでスタックトレースごとログに残し、障害調査で原因箇所を特定する
パフォーマンスチューニング時に FINE レベルでSQL実行時間を記録し、通常運用時はログに出さない切り替えを行う
コード例
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("アプリケーション終了");
}
}Version Coverage
var でハンドラーやロガーの宣言が簡潔になる。record でログエントリを構造化すると、まとめてログ出力するパターンが書きやすくなる。
// 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
注意点
Logger.getLogger("") でルートロガーを取得してハンドラーを追加すると、全ロガーに影響する。クラス専用のロガーに addHandler し、setUseParentHandlers(false) で親への伝播を止めるのが安全
logger.info("値: " + expensiveComputation()) のように書くと、INFO が無効でも文字列結合と関数呼び出しが実行される。isLoggable() で事前チェックするか、ラムダ版の log メソッドを使うこと
e.printStackTrace() はログフレームワークを経由せず標準エラーに直接出力するため、ログレベル制御もファイル出力もできない。必ず logger.log(Level.SEVERE, message, e) を使うこと
ConsoleHandler のデフォルトレベルは INFO のため、ロガーを ALL に設定しても FINE 以下は出力されない。ハンドラー側のレベルも合わせて設定する必要がある
ロガー名にはクラスの完全修飾名を使うのが慣例。パッケージ階層に沿ったロガーツリーが構成され、レベルの一括変更やフィルタリングがしやすくなる
FAQ
チームやプロジェクトで既に SLF4J + Logback が導入されているならそちらに合わせます。依存なしで始めたい場合や小規模なツールでは JUL で十分です。
FINE はデバッグ情報、FINER はメソッドの入口・出口、FINEST は変数値のトレースという使い分けが一般的です。ただし実務では FINE と INFO の2段階で運用するケースが大半です。
logger.log(Level.SEVERE, "エラー内容", exception) のように、第3引数に Throwable を渡します。これでスタックトレース全体がログに記録されます。