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

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 の開始・終了ログを出力します。これは境界ログとも呼ばれます。

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 は、対象となるメソッドの実行を「前後から包み込む」ためのアノテーションです。

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

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

この記述方法は Pointcut と呼ばれるものですが、今回は詳細には立ち入りません。

動作確認

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

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 補足

今回は、 userdummy という値をリテラルで設定していましたが、 本来であれば次のコードのように 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!!!");
  }
}

参考資料