Spring Boot ロギング入門 - アクセスログ・業務ログを集める

概要

Java / Spring Boot のロギングの基本を学ぶ勉強会です。ログの重要性、ログレベル、デフォルトのロギングライブラリである Logback について、設定方法、実践パターンを扱います。

対象読者

  • Java / Spring Boot でアプリケーションを開発しているが、ロギングを「なんとなく」使っている方
  • ログレベルやロギングライブラリの選び方を整理したい方
  • 本番運用を見据えたログ設計(構造化ログ、MDC、監査ログ)を学びたい方

この資料の構成

この資料は 2 部構成 になっています。

  1. 触って学ぶロギング: サンプルアプリを実際に作りながら、アクセスログ・業務ログ・MDC を実装していくハンズオン
  2. 座学で学ぶロギング: ロギングの基本概念、ログレベル、SLF4J / Logback、構造化ログなどの理論を整理する座学

「触って学ぶ」で実装した内容を、「座学で学ぶ」で概念として整理し、復習する流れです。対応関係は次の表の通りです。

さわって学ぶロギング

ログの重要性

ハンズオンに入る前に、そもそもなぜログを残すのかを確認しておきましょう。ログは、アプリケーションの「行動記録」です。後から「何が起きたか」を再現し、調査できるようにするために不可欠です。

このハンズオンでは、アクセスログ、業務ログ、MDC を実際に実装しながら、「ログを残す」ことの価値を体感していきます。ログの目的の詳細(デバッグ、障害調査、監視、監査、性能分析)は、後半の「座学で学ぶロギング」で整理します。

ログレベルの軽い導入

ログには重要度を表す「ログレベル」があります。レベルを設定することで、出力するログを絞り込めます。

レベル用途
TRACE最も詳細なデバッグ情報
DEBUG開発・デバッグ用の詳細情報
INFO通常運用で確認すべき情報
WARN注意が必要だが処理は継続
ERROR処理が失敗した場合

レベルは TRACE < DEBUG < INFO < WARN < ERROR の順に重要度が高く、設定したレベル以上のログだけが出力されます。例えば INFO に設定すると、INFO / WARN / ERROR が出力され、DEBUG / TRACE は出力されません。

このハンズオンでは、主に INFO と WARN を使います。詳細なレベルについては、後半の「座学で学ぶロギング」で改めて整理します。

Spring Initializr

次のリンクから Spring Initializr を開き、 Spring Boot プロジェクトを作成してください。

Spring Initializr

アプリの作成

このアプリでは、簡易的なユーザー管理APIを用意します。

  • /addUser: ユーザーを追加する
  • /removeUser: ユーザーを削除する
  • /getUsers: 登録済みユーザー一覧を取得する

サービスの作成

package dev.mikoto2000.springboot.logging.service;

import java.util.HashSet;
import java.util.Set;

import org.springframework.stereotype.Service;

/**
 * UserService
 */
@Service
public class UserService {

  private final Set<String> users = new HashSet<>();

  public void addUser(String name) {
    users.add(name);
  }

  public void removeUser(String name) {
    users.remove(name);
  }

  public Set<String> getUsers() {
    return new HashSet<String>(users);
  }

  public void fireException() {
    throw new RuntimeException("Hello, Exception!!!");
  }
}

コントローラーの作成

src/main/java/dev/mikoto2000/springboot/logging/controller/MiscController.java:

package dev.mikoto2000.springboot.logging.controller;

import java.util.Set;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import dev.mikoto2000.springboot.logging.service.UserService;
import lombok.RequiredArgsConstructor;

/**
 * UserController
 */
@RequiredArgsConstructor
@RestController
public class UserController {

  private final UserService service;

  @GetMapping("addUser")
  public void addUser(
      @RequestParam String name
      ) {
    service.addUser(name);
  }

  @GetMapping("removeUser")
  public void removeUser(
      @RequestParam String name
      ) {
    service.removeUser(name);
  }

  @GetMapping("getUsers")
  public Set<String> getUsers() {
    return service.getUsers();
  }

  @GetMapping("fireException")
  public void fireException() {
    service.fireException();
  }
}

アクセスログの追加

各エンドポイントにアクセスしたことを記録する、アクセスログを追加します。

アクセスログ用フィルタの作成

src/main/java/dev/mikoto2000/springboot/logging/filter/AccessLogFilter.java:

package dev.mikoto2000.springboot.logging.filter;

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class AccessLogFilter extends OncePerRequestFilter {

    private static final Logger accessLogger = LoggerFactory.getLogger("ACCESS_LOG");

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain)
            throws ServletException, IOException {

        long start = System.currentTimeMillis();

        // 接続元 IP 取得
        String ip = request.getRemoteAddr();

        // 成功・失敗フラグ
        boolean success = false;

        try {
            chain.doFilter(request, response);
            success = true;
        } finally {

            long time = System.currentTimeMillis() - start;

            accessLogger.info("ip={}, method={}, request_url={}, status={} success={}, time={}ms",
                ip,
                request.getMethod(),
                request.getRequestURI(),
                response.getStatus(),
                success ? "SUCCESS" : "FAIL",
                time);
        }
    }
}

アクセスログの動作確認

curl コマンドで、それぞれのエンドポイントにアクセスしてみましょう。 ついでに存在しないエンドポイントにもアクセスしてみます。

curl http://localhost:8080/addUser?name=mikoto2000
curl http://localhost:8080/getUsers
curl http://localhost:8080/removeUser?name=mikoto2000
curl http://localhost:8080/fireException
curl http://localhost:8080/invalidEndpoint

次のようなログが出力されます。

2026-02-10T20:59:38.021Z  INFO 51208 --- [logging] [nio-8080-exec-1] ACCESS_LOG : ip=127.0.0.1, method=GET, request_url=/addUser, status=200, success=SUCCESS, time=22ms
2026-02-10T20:59:38.050Z  INFO 51208 --- [logging] [nio-8080-exec-2] ACCESS_LOG : ip=127.0.0.1, method=GET, request_url=/getUsers, status=200, success=SUCCESS, time=15ms
2026-02-10T20:59:38.064Z  INFO 51208 --- [logging] [nio-8080-exec-4] ACCESS_LOG : ip=127.0.0.1, method=GET, request_url=/removeUser, status=200, success=SUCCESS, time=1ms
2026-02-10T20:59:38.082Z  INFO 51208 --- [logging] [nio-8080-exec-5] ACCESS_LOG : ip=127.0.0.1, method=GET, request_url=/fireException, status=200, success=FAIL, time=6ms
2026-02-10T20:59:38.109Z  INFO 51208 --- [logging] [nio-8080-exec-6] ACCESS_LOG : ip=127.0.0.1, method=GET, request_url=/invalidEndpoint, status=404, success=SUCCESS, time=3ms
  • ip: アクセス元の IP アドレス ※実務では、プロキシ配下等の場合に IP の取得方法にひと工夫が必要である(今回は割愛)
  • method: HTTP メソッド(GET / POST など)
  • request_url: アクセスされたパス
  • status: HTTP ステータスコード(200 / 404 など)
  • success: アプリケーションが例外なく処理を完了したかどうか ※ 「HTTP ステータス」の「成功」ではなく、「アプリの内部処理の成否」であることに注意
  • time: 処理にかかった時間(ミリ秒)

また、注目してほしいのは 404 で失敗しているものもちゃんとログに記録されているところです。 Filter でアクセスログを取得しているため、コントローラーが呼ばれない場合でも記録できます。

Controller の開始・終了ログを追加

業務ログの一種として、Controller の開始・終了ログを出力します。これは境界ログとも呼ばれます。

発展トピック: AOP

この章は AOP(Aspect Oriented Programming) を使った実装です。AOP は「共通処理を業務ロジックとは別のクラスに分離する」というやや発展的なテーマです。初めての方や難しく感じた方は、この章を読み飛ばして次の「MDC の追加」に進んでも問題ありません。あとで余裕ができたら戻ってきてください。

pom.xml の修正

開始・終了ログは、 AOP(Aspect Oriented Programming) の機能を使って実装していきます。 まずは Spring Boot で AOP が使えるように依存を追加します。

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>4.0.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>dev.mikoto2000.springboot</groupId>
	<artifactId>logging</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>logging</name>
	<description>Logging demo project for Spring Boot</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webmvc</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webmvc-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- 追加ここから -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aspectj</artifactId>
		</dependency>
		<!-- 追加ここまで -->
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<annotationProcessorPaths>
						<path>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</path>
					</annotationProcessorPaths>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

ログ出力コード実装

次に、ログを出力するコードを実装します。次のコードを追加してください。

src/main/java/dev/mikoto2000/springboot/logging/aop/LoggingAspect.java:

package dev.mikoto2000.springboot.logging.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

  private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

  @Around(
    "within(dev.mikoto2000.springboot.logging.controller..*)"
  )
  public Object logMethod(ProceedingJoinPoint pjp) throws Throwable {

    // メソッド情報取得
    String className = pjp.getTarget().getClass().getSimpleName();
    String methodName = pjp.getSignature().getName();

    log.info("START {}#{}", className, methodName);

    // 時間計測開始
    long startTime = System.currentTimeMillis();
    try {

      Object result = pjp.proceed();

      // 時間計測終了
      long endTime = System.currentTimeMillis();

      log.info("END   {}#{}, time={}ms", className, methodName, endTime - startTime);

      return result;

    } catch (Throwable e) {

      // 時間計測終了
      long endTime = System.currentTimeMillis();

      log.error("ERROR {}#{}, time={}", className, methodName, endTime - startTime, e);

      throw e;
    }
  }
}
@Aspect と @Around について
@Aspect

AOP(Aspect Oriented Programming)では、ログ出力やトランザクション管理などの「共通処理」を業務ロジックとは別のクラスとして実装します。

この共通処理を定義するクラスには、@Aspect アノテーションを付与します。

@Aspect を付けることで、このクラスが「AOP の処理を定義するクラス(Aspect)」として Spring に認識されます。

@Around

@Around は、対象となるメソッドの実行を「前後から包み込む」ためのアノテーションです。

今回のサンプルでは、次のように定義することで、Controller のメソッドを実行する前後の処理を記述しています。

  "within(dev.mikoto2000.springboot.logging.controller..*)"

この記述方法は Pointcut と呼ばれます。Pointcut とは「どのメソッドに共通処理を適用するか」を指定する式のことです。今回の例では、within(...) という式を使って「controller パッケージ配下のすべてのメソッド」を対象にしています。

Pointcut の書き方を少し詳しく見てみましょう。

  • within(パッケージ..*): 指定したパッケージ配下のすべてのメソッドが対象
  • execution(戻り値 クラス.メソッド(引数)): 特定のクラス・メソッドを細かく指定
  • @annotation(アノテーション): 特定のアノテーションが付いたメソッドが対象

今回の within(dev.mikoto2000.springboot.logging.controller..*) は、「controller パッケージ配下のすべてのメソッド」にログ出力を適用する、という意味です。..* の部分が「パッケージ配下のすべて」を表しています。

Pointcut は奥が深いテーマですが、まずは「どのメソッドに適用するかを式で指定するもの」という理解で十分です。詳しく学びたい方は、参考資料の「AOP の概念」を参照してください。

動作確認

ここまで来たらもう一度動作確認をしてみましょう。

curl http://localhost:8080/addUser?name=mikoto2000

次のようなログが表示されるようになっています。

2026-02-10T20:59:38.017Z INFO 51208 --- [logging] [nio-8080-exec-1] d.m.s.logging.aop.LoggingAspect : START UserController#addUser
2026-02-10T20:59:38.017Z INFO 51208 --- [logging] [nio-8080-exec-1] d.m.s.logging.aop.LoggingAspect : END   UserController#addUser, time=0ms

MDC(Mapped Diagnostic Context) の追加

アクセスログ、開始・終了ログの追加をしてきましたが、このままではそれぞれのログのつながりがわかりません。 アクセスログ、開始・終了ログのつながりがわかるようにするため、 MDC を導入していきます。

MDC を使用するようにコードを修正

src/main/java/dev/mikoto2000/springboot/logging/filter/AccessLogFilter.java を、次のように修正します。

src/main/java/dev/mikoto2000/springboot/logging/filter/AccessLogFilter.java:

package dev.mikoto2000.springboot.logging.filter;

import java.io.IOException;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class AccessLogFilter extends OncePerRequestFilter {

    private static final Logger accessLogger = LoggerFactory.getLogger("ACCESS_LOG");

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain)
            throws ServletException, IOException {

        /* 追加ここから */
        // MDC に記録する値を取得
        String user = "dummy"; // Spring Security と連携すると取得できる
        String traceId = UUID.randomUUID().toString();

        // MDC に値をセット
        MDC.put("user", user);
        MDC.put("traceId", traceId);
        /* 追加ここまで */

        long start = System.currentTimeMillis();

        // 接続元 IP 取得
        String ip = request.getRemoteAddr();

        // 成功・失敗フラグ
        boolean success = false;

        try {
            chain.doFilter(request, response);
            success = true;
        } finally {

            long time = System.currentTimeMillis() - start;

            accessLogger.info("ip={}, method={}, request_url={}, status={}, success={}, time={}ms",
                ip,
                request.getMethod(),
                request.getRequestURI(),
                response.getStatus(),
                success ? "SUCCESS" : "FAIL",
                time);

            /* 追加ここから */
            // MDC クリア
            MDC.clear();
            /* 追加ここまで */
        }
    }
}

MDC を表示するように Logback を設定

MDC を表示したい場合には、 %X{xxx} 形式で、 PATTERN に記述します。

src/main/resources/logback-spring.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- Spring Bootのデフォルト設定を読み込む(defaults.xmlで変数が定義される) -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <!-- コンソールの出力パターンのみを上書き定義する -->
    <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-}] [%X{user:-}] %-5level %logger{36} - %msg%n"/>

    <!-- デフォルトのコンソールアペンダーを読み込む(上のpropertyが適用される) -->
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

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

MDC の動作確認

もう一度 curl コマンドで、エンドポイントにアクセスしてみましょう。

curl http://localhost:8080/addUser?name=mikoto2000
curl http://localhost:8080/removeUser?name=mikoto2000

次のようなログが出力されます。

2026-02-11 00:22:56.046 [http-nio-8080-exec-3] [92a4a2a9-0db6-4072-bc81-2547f7d48da5] [dummy] INFO  d.m.s.logging.aop.LoggingAspect - START UserController#addUser
2026-02-11 00:22:56.047 [http-nio-8080-exec-3] [92a4a2a9-0db6-4072-bc81-2547f7d48da5] [dummy] INFO  d.m.s.logging.aop.LoggingAspect - END   UserController#addUser, time=0ms
2026-02-11 00:22:56.047 [http-nio-8080-exec-3] [92a4a2a9-0db6-4072-bc81-2547f7d48da5] [dummy] INFO  ACCESS_LOG - ip=127.0.0.1, method=GET, request_url=/addUser, status=200, success=SUCCESS, time=1ms
2026-02-11 00:24:08.395 [http-nio-8080-exec-5] [7154ca6d-5764-43ae-a045-956f6b0617ad] [dummy] INFO  d.m.s.logging.aop.LoggingAspect - START UserController#removeUser
2026-02-11 00:24:08.395 [http-nio-8080-exec-5] [7154ca6d-5764-43ae-a045-956f6b0617ad] [dummy] INFO  d.m.s.logging.aop.LoggingAspect - END   UserController#removeUser, time=0ms
2026-02-11 00:24:08.396 [http-nio-8080-exec-5] [7154ca6d-5764-43ae-a045-956f6b0617ad] [dummy] INFO  ACCESS_LOG - ip=127.0.0.1, method=GET, request_url=/removeUser, status=200, success=SUCCESS, time=2ms

紐づいている開始・終了ログとアクセスログに、同じ traceId が付与されていることがわかります。

このようにすると、「traceId で grep をかけると見たいリクエストのみが時系列で追える」などのメリットが出てきます。

MDC 補足

MDC は「スレッドごとに記録できる Map」というイメージです。 こう捉えると、理解しやすいでしょう。 MDC に値をセットすると、そのスレッドで出力するログに、セットした値を含められます。

Tomcat は「1 リクエスト 1 スレッド」ですので、ちょうど良く「リクエストごとに一意な値」を設定できるというわけです。

user 補足

今回は、 user に dummy という値をリテラルで設定していましたが、 本来であれば次のコードのように Spring Security と連携し、ユーザー情報を取得します。

Authentication auth =
  SecurityContextHolder.getContext().getAuthentication();

if (auth != null && auth.isAuthenticated()) {
  MDC.put("user", auth.getName());
}

業務ログを完成させる

Controller の開始・終了ログを出力したことで、クラス名とメソッド名から「大体何をやっているか」は分かるようになりましたが、業務ログでは 5W1H が重要です。 Service にログを入れることで、業務ログを完成させましょう。

今回は例として Service 層に「誰を追加、削除したか」というログを追加します。 業務ではログ設計、ログ方針に応じてログを出力するようにしましょう。

Controller では処理の境界を、Service では業務上の意味を持つイベントをログに記録するというイメージです。

src/main/java/dev/mikoto2000/springboot/logging/service/UserService.java:

package dev.mikoto2000.springboot.logging.service;

import java.util.HashSet;
import java.util.Set;

import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

/**
 * UserService
 */
@Service
@Slf4j
public class UserService {

  private final Set<String> users = new HashSet<>();

  public void addUser(String name) {
    /* 修正ここから */
    if (users.add(name)) {
      log.info("Add user: name={}", name);
    } else {
      log.warn("Add user failed: name={}", name);
    }
    /* 修正ここまで */
  }

  public void removeUser(String name) {
    /* 修正ここから */
    if (users.remove(name)) {
      log.info("Remove user: name={}", name);
    } else {
      log.warn("Remove user failed: name={}", name);
    }
    /* 修正ここまで */
  }

  public Set<String> getUsers() {
    return new HashSet<String>(users);
  }

  public void fireException() {
    throw new RuntimeException("Hello, Exception!!!");
  }
}

座学で学ぶロギング

ロギングの基本概念と重要性

ロギングとは何をすることか

ロギングとは、アプリケーションの動作中に発生するイベントや状態変化を記録する仕組みです。プログラムが「何を」「いつ」「どのように」実行したかを、時系列のテキストとして残します。

具体的には、以下のようなことを行います。

  • イベントの記録: 起動、リクエスト受信、処理完了、例外発生などを記録する
  • 状態変化の記録: 変数の値、処理の分岐、リソースの利用状況などを記録する
  • 文脈情報の付与: ユーザー ID、リクエスト ID、処理時間などをログに含める
  • 原因調査の支援: 不具合発生時に、処理の流れや変数の値を再現して原因を特定するための記録(デバッグの手がかり)

つまりロギングは、アプリケーションの「行動記録」を残すことで、後から何が起きたかを再現し、調査できるようにするための仕組みです。

なぜログが必要か

ログは以下の目的で不可欠です。それぞれの「なぜ」を押さえると、ログ設計の判断基準が見えてきます。

  • デバッグ: 開発中に処理の流れや変数の値を確認し、不具合の原因を特定する。開発中は DEBUG レベルを活用し、詳細な情報を残す
  • 障害調査: 問題発生時に原因を特定するための手がかり。本番では INFO 以上を残し、トレース ID で相関を取れるようにする
  • 監視・アラート: システムの異常を早期に検知する。WARN / ERROR を監視対象にし、アラートの基準を決める
  • 監査・コンプライアンス: 誰がいつ何をしたかを記録する。改ざん防止や保存期間の要件を満たすため、通常のログと分離して保存する
  • 性能分析: 処理時間やリソース使用量の傾向を把握する。パフォーマンスログを残し、閾値を超えた処理を WARN で記録する

ログ・トレース・メトリクスの違い

ログ・トレース・メトリクスは「3 つの柱」として並べて語られることが多いですが、観測対象が異なるため、同列に扱うのではなく「何を見たいか」で使い分けるのが正しい理解です。

観点ログトレースメトリクス
目的イベント記録・状態把握リクエストの流れを追跡数値の集計・傾向把握
粒度個別イベント単位リクエスト全体単位集計値(時系列)
質問「何が起きたか」「どこで遅い・失敗したか」「どれくらい起きたか」
相関ログ単体では文脈が分かりにくいトレース ID で全体を俯瞰数値の増減で異常を検知
ツールLogback / Log4j2 などOpenTelemetry / Jaeger などPrometheus / Micrometer など
  • ログ: 個々のイベントの詳細(エラー内容、アクセス元など)。原因調査に強い
  • トレース: リクエストの流れを俯瞰し、遅延や失敗の箇所を特定する
  • メトリクス: CPU 使用率やリクエスト数などの数値を集計し、傾向把握・監視アラートに強い

ログは「点」、トレースは「線」、メトリクスは「面(数値の時系列)」で捉えると理解しやすいです。「ログで原因を探る、メトリクスで異常を検知する、トレースで全体像を掴む」と使い分けるのが定番です。三者を組み合わせることで、システム全体の可視性が向上します。

今回の資料の対象

今回の資料はログについてを説明するものです。トレースやメトリクスの詳細は扱いませんが、ログを正しく理解するために、この違いを冒頭で押さえておきます。

良いログとは何か

ログは「誰が読むのか」を意識して設計すると、その価値が大きく変わります。ログの読み手は主に以下の 3 種類です。

  • 開発者: デバッグや障害調査で、処理の流れと原因を追う
  • 運用担当者: 監視・アラートで、異常の早期検知と対応判断に使う
  • 監査・セキュリティ担当: 誰が何をしたかの記録として、コンプライアンス要件を満たす

読み手が違えば必要な情報も変わります。開発者は詳細な変数の値やスタックトレースを、運用担当者は処理の開始、完了、失敗の要約を、監査担当者はユーザー ID と操作内容を求めます。ログは「誰の」「何の」ために残すかを決めてから設計するのが基本です。

また、ログには以下のような種類があります。

  • アクセスログ: 誰がいつどのリクエストを送ったか(HTTP アクセス、認証イベント)
  • アプリケーションログ: 業務処理の開始・完了・失敗、例外発生
  • 監査ログ: 誰が何をしたか(操作履歴、権限変更)
  • システムログ: 起動・停止、リソース状態、設定変更

このようにログは「記録の目的」ごとに分けて考えると、何を、どのレベルで、どういう形式で残すべきかが明確になります。次の「ログ出力の基本原則」は、こうしたログ設計の考え方を実践に落とし込むための指針です。

ログ出力の基本原則

  1. 適切なレベルで出力する — 重要度に応じてレベルを使い分ける
  2. 文脈情報を含める — ユーザー ID、リクエスト ID などをログに含める
  3. 機密情報を出さない — パスワード、個人情報、認証情報はログに含めない
  4. ログは構造化する — 機械的に解析できる形式で出力する
以降の章について

以降(2章〜7章)は Spring Boot 特有の話です。 1章で学んだロギングの基本概念を、Spring Boot の実装・設定にどう落とし込むかを扱います。Java 以外の技術スタックの方は、概念(1章)を参考にしつつ、各フレームワークの流儀に合わせて読み替えてください。

ログレベル(TRACE / DEBUG / INFO / WARN / ERROR)とフィルタリング

ログレベルの一覧

レベル用途例
TRACE最も詳細なデバッグ情報SQL のバインドパラメータ、内部状態の遷移
DEBUG開発・デバッグ用の詳細情報メソッド呼び出し、変数の値
INFO通常運用で確認すべき情報起動、リクエスト受信、処理完了
WARN注意が必要だが処理は継続リトライ発生、リソース枯渇の兆候
ERROR処理が失敗した場合例外発生、DB 接続失敗
OFFログ出力を完全に停止特定パッケージのログを止めたい場合

レベルの優先順位

TRACE < DEBUG < INFO < WARN < ERROR < OFF

設定したレベル以上のログだけが出力されます。例えば INFO に設定すると、INFO / WARN / ERROR が出力され、DEBUG / TRACE は出力されません。

フィルタリング

ログレベルによるフィルタリングは、以下の観点で行います。

  • パッケージ・クラス単位: 特定のパッケージだけ DEBUG にする
  • ロガー名単位: 特定のクラスだけ詳細ログを出す
  • 環境ごとの設定: 開発環境は DEBUG、本番環境は INFO など

レベル設定の実践例

application.yml
logging:
  level:
    root: INFO                    # ルートロガーは INFO
    com.example.service: DEBUG    # 特定パッケージは DEBUG
    com.example.mapper.UserMapper: TRACE  # 特定クラスは TRACE

SLF4J と Logback(Spring Boot のデフォルト)

SLF4J(Simple Logging Facade for Java)

SLF4J はロギングライブラリのファサード(窓口)です。アプリケーションコードは SLF4J の API だけを使い、実際のロギング実装をプラグ可能にします。

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

private static final Logger log = LoggerFactory.getLogger(MyClass.class);
log.info("処理を開始します: {}", userId);

メリット:

  • 実装ライブラリを後から差し替えられる
  • コード変更なしで Logback → Log4j2 に移行可能
  • {} プレースホルダによる遅延評価(文字列連結を避けられる)

Logback

SLF4J の標準実装として最も広く使われています。Spring Boot のデフォルトです。

特徴:

  • 設定ファイル: logback.xml / logback-spring.xml
  • 高性能(Log4j より高速)
  • 自動リロード、条件付き設定に対応
  • 豊富なアペンダー(コンソール、ファイル、DB、メールなど)

発展トピック: Log4j2

Apache が開発するロギングライブラリで、非同期ロギングの高性能さや豊富なフィルタリング機能が特徴です。ただし Spring Boot では別途依存追加が必要で、設定も複雑になります。

入門者へのアドバイス

入門者はまず Logback を使い、高度な非同期や複雑なフィルタが必要になってから Log4j2 を検討するのがおすすめです。

Spring Boot でのロギング設定

application.yml での設定

application.yml
logging:
  level:
    root: INFO          # ルートロガーのレベル
    com.example: DEBUG  # パッケージ単位のレベル
  file:
    name: logs/app.log  # ログファイル出力
  logback:
    rollingpolicy:
      max-file-size: 10MB
      max-history: 30
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

環境ごとの設定

application-{profile}.yml を使うと、環境ごとにログレベルを変えられます。

application-dev.yml
logging:
  level:
    root: DEBUG
application-prod.yml
logging:
  level:
    root: INFO

ログファイルのローテーション

ファイルサイズによるローテーション設定:

application.yml
logging:
  logback:
    rollingpolicy:
      max-file-size: 10MB
      max-history: 30

日次ローテーションなど細かい制御が必要な場合は logback-spring.xml を直接編集します。

実践的なロギングパターン

例外ログ

例外をログに残す際のポイントです。

例外ログの良い例
// 良い例: 例外情報を詳細に残す
try {
    processOrder(orderId);
} catch (Exception e) {
    log.error("注文処理に失敗しました。orderId={}", orderId, e);
}

注意点:

  • スタックトレースを必ず含める(e を渡す)
  • 例外メッセージだけでなく、関連するパラメータもログに含める
  • e.printStackTrace() は使わない(標準出力に出てしまう)

パフォーマンスログ

処理時間を計測してログに残すパターンです。

パフォーマンスログ
long start = System.currentTimeMillis();
try {
    // 処理
    result = heavyOperation();
} finally {
    long duration = System.currentTimeMillis() - start;
    log.info("処理完了: operation={}, duration={}ms", operationName, duration);
}

注意点:

パフォーマンスログの目的は「遅い処理を素早く見つける」ことです。ここを忘れると、ログがただのノイズになります。

  • 遅い処理だけを INFO で記録する: 全処理の時間を DEBUG で記録すると、ログが膨大になり「遅い処理」が埋もれてしまいます。そこで、通常の処理は DEBUG(本番では出力しない)にし、遅い処理だけを INFO で記録して目立たせます。DEBUG と INFO の違いは「開発時に見るか、本番運用で見るか」です。遅い処理は本番で見たいので INFO にします。
  • 閾値を設けて、閾値を超えた場合のみ WARN で記録する: 例えば「100ms を超えたら WARN、それ未満は INFO」のように、基準を決めておきます。WARN は「注意して見るべき」レベルなので、運用監視でアラートを出す基準にも使えます。閾値を超えた処理だけが WARN になることで、「今日の遅い処理はどれか」が一目で分かります。

つまり、DEBUG(全件)→ INFO(遅いもの)→ WARN(特に遅いもの) と段階を絞ることで、ログの量を抑えながら問題を見つけやすくするのがポイントです。

監査ログ

誰が何をしたかを記録するパターンです。セキュリティ要件やコンプライアンス要件で必要になります。

監査ログ
log.info("監査: user={}, action={}, target={}, result={}", userId, "DELETE", targetId, "SUCCESS");

注意点:

  • 監査ログは通常のログと分離して保存する
  • 改ざん防止のため、追記専用ファイルや署名を検討する
  • 個人情報を含めない

ログの肥大化対策

  • ローテーション設定: ファイルサイズ・日次でローテーション
  • レベル調整: 本番では INFO 以上に設定
  • 不要なログの削減: デバッグ用ログを本番で出力しない
  • サンプリング: 大量ログの場合は間引いて出力

MDC(Mapped Diagnostic Context)

MDC(Mapped Diagnostic Context)とは

MDC は、スレッドごとにキー・バリュー形式のコンテキスト情報を保持する SLF4J の機能です。リクエスト ID やユーザー ID をログに自動的に含めることができます。

MDC
import org.slf4j.MDC;

// リクエスト開始時に設定
MDC.put("requestId", requestId);
MDC.put("userId", userId);

// ログ出力時に自動で含まれる
log.info("処理を開始");

// リクエスト終了時にクリア
MDC.remove("requestId");
MDC.remove("userId");

なぜ MDC が必要か

requestId や userId を JSON に含めるには、すべてのログ出力箇所でそれらを引数に渡す必要があります。しかし、処理が深くネストしていると、各メソッドにコンテキストを手で引き回すことになり、コードが冗長になりがちです。

MDC を使うと、フィルターなどで一度設定するだけで、そのスレッド内のすべてのログに自動的にコンテキストが含まれます。ログ出力箇所では MDC を意識する必要がないため、以下の利点があります。

  • コードの簡潔さ: 各メソッドに requestId を渡す必要がなくなる
  • 漏れの防止: 設定を忘れてログにコンテキストが欠落する事故を防げる
  • 一貫性: 例外やサードパーティのライブラリが出力するログにも自動で含まれる
  • スレッド安全: スレッドごとに独立したコンテキストを持つため、並列処理でも混ざらない

これにより、ログの「相関」の利点(requestId で一連の処理を追跡)を、コードを汚さずに実現できます。

MDC のパターン設定

logback.xml のパターンに MDC キーを組み込むと、すべてのログに自動で含まれます。

logback.xml
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{requestId}] [%X{userId}] %-5level %logger{36} - %msg%n</pattern>

フィルターでの MDC 利用

RequestIdFilter.java
// フィルターでリクエスト ID を MDC に設定
@Component
public class RequestIdFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
        String requestId = UUID.randomUUID().toString();
        MDC.put("requestId", requestId);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove("requestId");
        }
    }
}

構造化ロギング

「触って学ぶ」の方では、ログを JSON 形式で出力する方法は扱いませんでしたが、実務では構造化ロギングを扱う事も多いため言及します。。 ここでは構造化ロギングの概要と、Logback での JSON 出力方法を紹介します。

構造化ロギングとは

ログを JSON などの機械可読形式で出力する手法です。CloudWatch Logs、Datadog、Splunk などのログ収集基盤での検索・分析が容易になります。

JSON ログの例:

JSON
{
  "timestamp": "2025-01-15T10:30:00.123+09:00",
  "level": "INFO",
  "logger": "com.example.OrderService",
  "message": "注文処理を開始",
  "requestId": "abc-123",
  "userId": "user-456",
  "duration": 123
}

なぜ構造化ロギングが必要か

従来のテキスト形式のログは、人間が読むには優れていますが、機械による検索や集計には向きません。たとえば「特定のユーザーの全操作」を追いたいとき、テキストログでは正規表現や文字列パースに頼ることになり、フォーマットの揺れや改行の混入で壊れやすくなります。

構造化ロギングでは、各項目がキー・バリューで分離されているため、以下のようなことが容易になります。

  • 検索: userId = "user-456" のような条件で正確に絞り込める
  • 集計・分析: duration の平均・最大値をクエリで計算できる
  • 相関: requestId をキーに、複数サービスにまたがる一連の処理を追跡できる
  • スキーマの安定: 項目の追加・変更がログ収集基盤側の設定変更だけで済む

特にマイクロサービスやサーバーレス構成では、ログを横断して追跡する機会が増えるため、機械可読な形式がほぼ必須になります。

Logback での JSON 出力

Logback 標準には JSON エンコーダーが含まれていないため、logstash-logback-encoder ライブラリを使うのが一般的です。pom.xml に依存を追加します。

pom.xml
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.4</version>
</dependency>

logback-spring.xml で JSON エンコーダーを設定します。

logback-spring.xml
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>

用語集

用語説明
ログアプリケーションの動作中に発生するイベントや状態変化を記録したもの
ログレベルログの重要度を表す指標。TRACE / DEBUG / INFO / WARN / ERROR / OFF の 6 段階
フィルタリング設定したログレベル以上のログだけを出力する仕組み
SLF4JJava のロギングファサード。実装ライブラリをプラグ可能にする API
LogbackSLF4J の標準実装。Spring Boot のデフォルトロギングライブラリ
Log4j2Apache が開発するロギングライブラリ。非同期ロギングに強みがある
アペンダーログの出力先(コンソール、ファイル、DB、メールなど)を定義するコンポーネント
エンコーダーログメッセージを出力形式(テキスト、JSON など)に変換するコンポーネント
ローテーションログファイルが肥大化しないよう、サイズや日次でファイルを分割・入れ替える仕組み
構造化ロギングログを JSON などの機械可読形式で出力する手法。ログ収集基盤での検索・分析が容易になる
MDC(Mapped Diagnostic Context)スレッドごとにキー・バリュー形式のコンテキスト情報を保持する SLF4J の機能。リクエスト ID やユーザー ID をログに自動的に含められる
トレースリクエストの流れを追跡する仕組み。ログ(点)と対になる「線」の概念
ファサード複数の実装を共通 API で扱うための窓口。SLF4J はロギング実装のファサード
プレースホルダSLF4J の {} 記法。文字列連結を避け、遅延評価でパフォーマンスを改善する
監査ログ誰がいつ何をしたかを記録するログ。セキュリティ・コンプライアンス要件で必要
パフォーマンスログ処理時間やリソース使用量を計測して記録するログ
スタックトレース例外発生時の呼び出し履歴。例外ログに含めることで原因特定が容易になる
プロファイルSpring Boot の環境設定(dev / prod など)。環境ごとにログレベルを切り替えられる

参考資料