Java Tips | 基本ユーティリティ:ログフォーマット

Java Java
スポンサーリンク

ログフォーマットは「あとから読めるログ」を作るための設計

ログは「その瞬間に何が起きていたか」を後から再現するための記録です。
でも、ただ System.out.println("エラーしました") と出しているだけでは、
「いつ」「どの処理で」「どのユーザーで」「どのリクエストで」起きたのかが分からず、運用でほぼ役に立ちません。

そこで大事になるのが「ログフォーマット」です。
ログフォーマットとは、「ログ1行に、どんな情報を、どんな形で載せるか」を決めるルールのことです。
このルールをユーティリティとしてまとめておくと、アプリ全体のログが揃い、トラブルシュートが一気に楽になります。


まず押さえるべき「ログ1行に欲しい情報」

最低限ほしい情報のイメージ

業務システムで「後から読めるログ」にするために、1行に載せたい典型的な情報はだいたいこうです。

日時(いつ)
レベル(INFO / WARN / ERROR など)
スレッド名(どのスレッド)
クラス・メソッド(どの処理)
メッセージ(何が起きたか)
トレース ID やリクエスト ID(どのリクエスト)
ユーザー ID やテナント ID(誰の/どの顧客の)

これらを毎回手書きするのは現実的ではないので、
「ログフレームワークのパターン設定」と「アプリ側のフォーマットユーティリティ」を組み合わせて整えていきます。


ログフレームワーク側のフォーマット(パターンレイアウト)

典型的なパターン例(Logback / Log4j2)

Logback や Log4j2 では、設定ファイルでログのフォーマットを指定できます。
例えば、こんなパターンです。

%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger - %msg%n

これで、ログは次のような形になります。

2026-01-23 15:30:12.345 INFO [http-nio-8080-exec-1] com.example.UserService - ユーザ登録開始

ここでのポイントは、
日時・レベル・スレッド・ロガー名(クラス名)・メッセージが、毎行同じ形式で出ることです。
この「土台のフォーマット」は、基本的にログフレームワーク側の設定で統一します。

アプリ側のユーティリティは、「メッセージ部分をどう整えるか」「ID やコンテキストをどう埋め込むか」を担当させるときれいに分担できます。


アプリ側でやるべき「メッセージのフォーマット統一」

プレーン文字列だと情報がバラバラになる

例えば、こんなログが散らばっているとします。

log.info("ユーザ登録開始: id=" + userId);
log.info("ユーザ登録完了 user=" + userId);
log.error("エラー userId=" + id + " reason=" + e.getMessage());

一見それっぽく見えますが、
キー名が id だったり user だったり userId だったりバラバラで、
あとから検索するときに「どれがどれだっけ?」となりがちです。

ここをユーティリティで揃えてしまうと、ログが一気に“機械的に扱いやすく”なります。

シンプルな「キー=値」形式をユーティリティで揃える

例えば、こんなユーティリティを用意します。

public final class LogFormat {

    private LogFormat() {}

    public static String kv(String key, Object value) {
        return key + "=" + String.valueOf(value);
    }

    public static String join(String... parts) {
        return String.join(" ", parts);
    }
}
Java

使う側はこう書けます。

log.info(LogFormat.join(
        "ユーザ登録開始",
        LogFormat.kv("userId", userId),
        LogFormat.kv("requestId", requestId)
));
Java

出力はこうなります。

ユーザ登録開始 userId=123 requestId=abc-xyz

ここで深掘りしたいのは、「キー名と書式をユーティリティで揃えることで、ログが“検索しやすいデータ”になる」という点です。
userId= で検索すれば、ユーザー関連のログが一気に拾えますし、
requestId= で検索すれば、1 リクエストに紐づくログをまとめて追えます。


コンテキスト情報(リクエスト ID など)を自動で埋め込む

MDC(Mapped Diagnostic Context)を使う

SLF4J/Logback/Log4j2 には、MDC という「スレッドローカルなコンテキスト」をログに埋め込む仕組みがあります。
例えば、リクエストごとに requestId を MDC に入れておくと、ログフォーマット側で %X{requestId} と書くだけで、全ログに自動で付与できます。

アプリ側では、こんなユーティリティを用意します。

import org.slf4j.MDC;

public final class LogContext {

    private static final String KEY_REQUEST_ID = "requestId";
    private static final String KEY_USER_ID = "userId";

    private LogContext() {}

    public static void setRequestId(String requestId) {
        MDC.put(KEY_REQUEST_ID, requestId);
    }

    public static void setUserId(String userId) {
        MDC.put(KEY_USER_ID, userId);
    }

    public static void clear() {
        MDC.clear();
    }
}
Java

Web フィルタやインターセプタでこう使います。

try {
    LogContext.setRequestId(generateRequestId());
    LogContext.setUserId(currentUserId());
    // コントローラ処理へ
} finally {
    LogContext.clear();
}
Java

ログフォーマットをこうしておけば、

%d %-5level [%thread] %X{requestId} %X{userId} %logger - %msg%n

全てのログに requestIduserId が自動で付きます。

ここでの重要ポイントは、「ID の埋め込みを“メッセージ文字列”ではなく“コンテキスト”でやる」という設計です。
これにより、アプリ側のログメッセージはシンプルなまま、ログ全体としてはリクエスト単位・ユーザー単位で追いやすくなります。


JSON ログフォーマットとユーティリティ

構造化ログ(JSON)を前提にする場合

最近は、ログを JSON 形式で出力し、Elasticsearch や Loki などに食わせて検索・集計する構成も多いです。
この場合、「メッセージを人間向けに整形する」よりも、「フィールドを機械向けに揃える」ことが重要になります。

Logback などには JSON ログ用のアペンダやエンコーダがあり、
level, timestamp, logger, thread, message, mdc などを自動で JSON にしてくれます。

アプリ側のユーティリティは、「message の中身をどうするか」よりも、
「必要な情報を MDC やフィールドとしてきちんとセットする」ことに重心が移ります。

それでも「メッセージの一貫性」は効いてくる

JSON ログでも、message フィールドは人間が読むことが多いです。
ここで、先ほどの LogFormat.kv のような「キー=値」スタイルを使っておくと、
ダッシュボード上でメッセージを見たときに、状況が直感的に分かりやすくなります。


例外ログのフォーマットとユーティリティ

例外は「メッセージ+スタックトレース」で出す

例外をログに出すときは、基本的にこう書きます。

log.error("ユーザ登録に失敗しました userId={}", userId, e);
Java

ログフレームワークが、メッセージとスタックトレースをよしなに出してくれます。

ここでユーティリティを挟むとしたら、例えば「共通のエラーメッセージフォーマット」を作る形です。

public final class ErrorLogFormat {

    private ErrorLogFormat() {}

    public static String userActionFailed(String action, Object userId) {
        return "ユーザ処理失敗 action=" + action + " userId=" + userId;
    }
}
Java

使う側はこうです。

log.error(ErrorLogFormat.userActionFailed("register", userId), e);
Java

ここでのポイントは、「エラー時のメッセージ構造をユーティリティで揃える」ことです。
「何のアクションが」「どのユーザーで」「失敗したのか」が、どのログでも同じ形で出てくるようになります。


まとめ:ログフォーマットユーティリティで身につけるべき感覚

ログフォーマットは、「その場しのぎの println」ではなく、「後から検索・分析・追跡できる“データ”としてログを設計する」ことです。

押さえておきたい感覚はこうです。

ログフレームワーク側で「行全体のフォーマット」(日時・レベル・スレッド・ロガー)を決める。
アプリ側では、「メッセージ部分の書き方」をユーティリティで揃え、キー=値形式などで検索しやすくする。
MDC を使って、requestId や userId などのコンテキストを自動で全ログに埋め込む。
必要に応じて JSON ログを使い、「構造化されたログ」として扱う前提で設計する。
例外ログやエラーログのメッセージも、ユーティリティでパターン化しておくと、運用時に読みやすくなる。

タイトルとURLをコピーしました