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 補足
今回は、 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!!!");
}
}
参考資料