はじめに

マイクロサービスやクラウドネイティブ環境でアプリケーションを運用する際、ログの収集・分析・監視は非常に重要です。従来のテキスト形式のログは人間には読みやすいものの、Elasticsearch、Splunk、CloudWatch Logsなどのログ分析基盤で効率的に処理するには、構造化されたJSON形式のログが適しています。

本記事では、Spring Boot REST APIにおけるJSON形式の構造化ログ出力について、Logstash Logback Encoderの導入からMDCによるコンテキスト情報の付与まで、実践的な手法を解説します。

前提条件

  • Java 17以上
  • Spring Boot 3.2以上
  • Maven または Gradle によるプロジェクト管理
  • Spring Webの依存関係(spring-boot-starter-web

構造化ログとは

構造化ログとは、ログデータを一貫したフォーマット(JSON、XML、Key-Valueペアなど)で出力する手法です。各ログエントリがフィールドとして明確に分離されているため、機械的な解析が容易になります。

従来のテキストログとの比較

従来のテキスト形式のログは以下のような形式です。

2026-01-04 17:00:00.123 INFO [main] c.e.myapp.UserService - User created: userId=12345, email=user@example.com

一方、JSON形式の構造化ログは以下のようになります。

1
2
3
4
5
6
7
8
9
{
  "@timestamp": "2026-01-04T17:00:00.123+09:00",
  "level": "INFO",
  "thread_name": "main",
  "logger_name": "com.example.myapp.UserService",
  "message": "User created",
  "userId": "12345",
  "email": "user@example.com"
}

JSON形式ログのメリット

構造化ログを採用することで、以下のメリットが得られます。

メリット 説明
検索性の向上 フィールド単位での検索・フィルタリングが可能
分析の効率化 ログ分析ツールでの集計・可視化が容易
一貫性の確保 統一されたスキーマでチーム間の認識を統一
自動化への対応 アラート設定やダッシュボード構築が容易
コンテキスト情報の保持 リクエストIDやユーザーIDなどを構造化して保持

Logstash Logback Encoderの導入

Spring BootでJSON形式のログを出力するには、Logstash Logback Encoderを使用します。このライブラリはLogbackと連携し、ログイベントをJSON形式に変換して出力します。

依存関係の追加

Maven

pom.xmlに以下の依存関係を追加します。

1
2
3
4
5
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>9.0</version>
</dependency>

Gradle

build.gradleに以下を追加します。

1
implementation 'net.logstash.logback:logstash-logback-encoder:9.0'

基本的なlogback-spring.xml設定

src/main/resourcesディレクトリにlogback-spring.xmlを作成し、以下の設定を追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    
    <!-- Spring Bootのデフォルト設定を読み込み -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
    
    <!-- JSON形式でコンソールに出力 -->
    <appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <includeContext>false</includeContext>
            <includeMdc>true</includeMdc>
            <timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSSXXX</timestampPattern>
        </encoder>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="JSON_CONSOLE" />
    </root>
    
</configuration>

出力されるJSONログの構造

上記の設定でログを出力すると、以下のようなJSON形式で出力されます。

1
2
3
4
5
6
7
8
9
{
  "@timestamp": "2026-01-04T17:00:00.123+09:00",
  "@version": "1",
  "message": "Application started",
  "logger_name": "com.example.myapp.Application",
  "thread_name": "main",
  "level": "INFO",
  "level_value": 20000
}

LogstashEncoderの主要な設定オプション

LogstashEncoderには多くの設定オプションがあり、出力内容を細かくカスタマイズできます。

フィールド名のカスタマイズ

デフォルトのフィールド名を変更できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
    <fieldNames>
        <timestamp>time</timestamp>
        <message>msg</message>
        <logger>log</logger>
        <thread>thread</thread>
        <level>severity</level>
        <levelValue>[ignore]</levelValue>
    </fieldNames>
</encoder>

[ignore]を指定すると、そのフィールドは出力されません。

タイムスタンプのカスタマイズ

タイムスタンプの形式やタイムゾーンを変更できます。

1
2
3
4
5
6
7
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
    <!-- ISO 8601形式 -->
    <timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSSXXX</timestampPattern>
    
    <!-- UTCタイムゾーン -->
    <timeZone>UTC</timeZone>
</encoder>

特殊な形式として、以下も使用可能です。

パターン 説明
[UNIX_TIMESTAMP_AS_NUMBER] Unixタイムスタンプ(数値)
[UNIX_TIMESTAMP_AS_STRING] Unixタイムスタンプ(文字列)
[ISO_OFFSET_DATE_TIME] ISO 8601形式(デフォルト)

MDCによるコンテキスト情報の付与

MDC(Mapped Diagnostic Context)を使用すると、リクエストIDやユーザーIDなどのコンテキスト情報を、ログ出力時に自動的に付与できます。

MDCの基本的な使い方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

@Service
public class UserService {
    
    private static final Logger log = LoggerFactory.getLogger(UserService.class);
    
    public User createUser(String email) {
        // MDCにコンテキスト情報を設定
        MDC.put("userId", "12345");
        MDC.put("email", email);
        
        try {
            log.info("ユーザー作成処理を開始");
            // ユーザー作成処理
            User user = new User(email);
            log.info("ユーザー作成処理が完了");
            return user;
        } finally {
            // MDCをクリア(メモリリーク防止)
            MDC.clear();
        }
    }
}

出力結果

上記のコードでログを出力すると、MDCの値がJSONフィールドとして自動的に追加されます。

1
2
3
4
5
6
7
8
9
{
  "@timestamp": "2026-01-04T17:00:00.123+09:00",
  "level": "INFO",
  "message": "ユーザー作成処理を開始",
  "logger_name": "com.example.myapp.UserService",
  "thread_name": "http-nio-8080-exec-1",
  "userId": "12345",
  "email": "user@example.com"
}

リクエストIDを自動付与するフィルター

REST APIでは、リクエストごとにユニークなIDを付与することで、リクエストのトレーサビリティを確保できます。以下はSpring MVCのフィルターを使用した実装例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.example.myapp.filter;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

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

@Component
@Order(1)
public class RequestIdFilter implements Filter {
    
    private static final String REQUEST_ID_HEADER = "X-Request-ID";
    private static final String REQUEST_ID_MDC_KEY = "requestId";
    private static final String CLIENT_IP_MDC_KEY = "clientIp";
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        try {
            // リクエストヘッダーからリクエストIDを取得、なければ生成
            String requestId = httpRequest.getHeader(REQUEST_ID_HEADER);
            if (requestId == null || requestId.isEmpty()) {
                requestId = UUID.randomUUID().toString();
            }
            
            // MDCにリクエストIDとクライアントIPを設定
            MDC.put(REQUEST_ID_MDC_KEY, requestId);
            MDC.put(CLIENT_IP_MDC_KEY, getClientIp(httpRequest));
            
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
    
    private String getClientIp(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }
}

MDCフィールドのフィルタリング

特定のMDCフィールドのみを出力する、または除外する設定も可能です。

1
2
3
4
5
6
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
    <!-- 特定のMDCキーのみ出力 -->
    <includeMdcKeyName>requestId</includeMdcKeyName>
    <includeMdcKeyName>userId</includeMdcKeyName>
    <includeMdcKeyName>clientIp</includeMdcKeyName>
</encoder>

または、特定のキーを除外することもできます。

1
2
3
4
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
    <!-- 特定のMDCキーを除外 -->
    <excludeMdcKeyName>sensitiveData</excludeMdcKeyName>
</encoder>

カスタムフィールドの追加

アプリケーション全体で共通のフィールドを追加したい場合や、ログ出力時に動的にフィールドを追加したい場合の方法を解説します。

グローバルカスタムフィールド

すべてのログに共通のフィールドを追加するには、customFieldsを使用します。

1
2
3
4
5
6
7
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
    <customFields>{
        "application": "my-rest-api",
        "environment": "${SPRING_PROFILES_ACTIVE:-local}",
        "version": "1.0.0"
    }</customFields>
</encoder>

StructuredArgumentsによるイベント固有フィールド

ログ出力時に動的にフィールドを追加するには、StructuredArgumentsを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import static net.logstash.logback.argument.StructuredArguments.*;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private static final Logger log = LoggerFactory.getLogger(UserController.class);
    
    @PostMapping
    public ResponseEntity<User> createUser(@RequestBody UserRequest request) {
        log.info("ユーザー作成リクエストを受信 {}",
            keyValue("email", request.getEmail()));
        
        User user = userService.create(request);
        
        log.info("ユーザー作成が完了 {} {}",
            keyValue("userId", user.getId()),
            keyValue("createdAt", user.getCreatedAt()));
        
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}

StructuredArgumentsの主要なメソッドは以下の通りです。

メソッド 説明 出力例
value(key, val) キーと値のペア(メッセージには値のみ) "key": "value"
keyValue(key, val) キーと値のペア(メッセージにもkey=val形式) "key": "value"
kv(key, val) keyValueの省略形 "key": "value"
entries(map) Mapの全エントリをフィールドとして追加 複数フィールド
array(key, vals...) 配列フィールド "key": [1, 2, 3]

Markersによるフィールド追加

Markersを使用すると、ログメッセージとは独立してフィールドを追加できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import static net.logstash.logback.marker.Markers.*;

@Service
public class OrderService {
    
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);
    
    public Order processOrder(OrderRequest request) {
        log.info(
            append("orderId", request.getOrderId())
                .and(append("totalAmount", request.getTotalAmount()))
                .and(append("items", request.getItems().size())),
            "注文処理を開始"
        );
        
        // 注文処理
        Order order = createOrder(request);
        
        return order;
    }
}

環境別設定の実践例

開発環境と本番環境で異なるログ設定を適用する完全な設定例を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
    
    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
    
    <springProperty scope="context" name="appName" source="spring.application.name" defaultValue="my-app" />
    
    <property name="LOG_PATH" value="${LOG_PATH:-./logs}" />
    
    <!-- 開発環境:読みやすいテキスト形式 -->
    <springProfile name="dev,local">
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%clr(%d{HH:mm:ss.SSS}){faint} %clr(%-5level) %clr([%15.15thread]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %msg%n</pattern>
            </encoder>
        </appender>
        
        <logger name="com.example.myapp" level="DEBUG" />
        
        <root level="INFO">
            <appender-ref ref="CONSOLE" />
        </root>
    </springProfile>
    
    <!-- 本番環境:JSON形式 -->
    <springProfile name="prod,staging">
        <!-- JSON形式でコンソール出力(コンテナ環境向け) -->
        <appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="net.logstash.logback.encoder.LogstashEncoder">
                <includeContext>false</includeContext>
                <timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSSXXX</timestampPattern>
                <timeZone>UTC</timeZone>
                <customFields>{
                    "application": "${appName}",
                    "environment": "${SPRING_PROFILES_ACTIVE:-unknown}"
                }</customFields>
                <fieldNames>
                    <levelValue>[ignore]</levelValue>
                </fieldNames>
            </encoder>
        </appender>
        
        <!-- JSON形式でファイル出力 -->
        <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>${LOG_PATH}/${appName}.json.log</file>
            <encoder class="net.logstash.logback.encoder.LogstashEncoder">
                <includeContext>false</includeContext>
                <timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSSXXX</timestampPattern>
                <timeZone>UTC</timeZone>
                <customFields>{
                    "application": "${appName}",
                    "environment": "${SPRING_PROFILES_ACTIVE:-unknown}"
                }</customFields>
            </encoder>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>${LOG_PATH}/${appName}.%d{yyyy-MM-dd}.%i.json.log.gz</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <totalSizeCap>3GB</totalSizeCap>
            </rollingPolicy>
        </appender>
        
        <logger name="com.example.myapp" level="INFO" />
        
        <root level="INFO">
            <appender-ref ref="JSON_CONSOLE" />
            <appender-ref ref="JSON_FILE" />
        </root>
    </springProfile>
    
</configuration>

実行環境での動作確認

テスト用コントローラ

以下のコントローラを作成して、構造化ログの出力を確認できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.myapp.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static net.logstash.logback.argument.StructuredArguments.*;

@RestController
@RequestMapping("/api/log-test")
public class StructuredLogTestController {
    
    private static final Logger log = LoggerFactory.getLogger(StructuredLogTestController.class);
    
    @GetMapping
    public String testStructuredLogging() {
        // MDCでコンテキスト情報を設定
        MDC.put("userId", "user-001");
        MDC.put("sessionId", "session-abc123");
        
        try {
            // 基本的なログ出力
            log.info("構造化ログのテスト開始");
            
            // StructuredArgumentsを使用したログ出力
            log.info("処理が完了 {} {} {}",
                keyValue("processedItems", 42),
                keyValue("duration", "150ms"),
                keyValue("status", "success"));
            
            return "構造化ログのテスト完了";
        } finally {
            MDC.clear();
        }
    }
}

期待される出力結果

本番プロファイルで実行した場合、以下のようなJSON形式のログが出力されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "@timestamp": "2026-01-04T08:00:00.123Z",
  "@version": "1",
  "message": "処理が完了 processedItems=42 duration=150ms status=success",
  "logger_name": "com.example.myapp.controller.StructuredLogTestController",
  "thread_name": "http-nio-8080-exec-1",
  "level": "INFO",
  "application": "my-rest-api",
  "environment": "prod",
  "userId": "user-001",
  "sessionId": "session-abc123",
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "clientIp": "192.168.1.100",
  "processedItems": 42,
  "duration": "150ms",
  "status": "success"
}

ログ分析基盤との連携

構造化されたJSONログは、さまざまなログ分析基盤と連携できます。

Elasticsearchへの送信

Filebeat等を使用して、JSONログをElasticsearchに送信する構成が一般的です。

flowchart LR
    A[Spring Boot App] -->|JSON Log| B[ログファイル]
    B --> C[Filebeat]
    C --> D[Elasticsearch]
    D --> E[Kibana]

CloudWatch Logsへの送信

AWS環境では、CloudWatch Logs Agentやfluentdを使用して、JSONログをCloudWatch Logsに送信できます。CloudWatch Logs Insightsでは、JSONフィールドを使用した高度なクエリが可能です。

fields @timestamp, level, message, userId, requestId
| filter level = "ERROR"
| sort @timestamp desc
| limit 100

まとめ

本記事では、Spring Boot REST APIにおけるJSON形式の構造化ログ出力について解説しました。主なポイントは以下の通りです。

  • 構造化ログはログ分析基盤での検索・集計を効率化し、運用効率を大幅に向上させる
  • Logstash Logback Encoder 9.0を使用することで、LogbackでJSON形式のログ出力が可能
  • MDCを活用することで、リクエストIDやユーザーIDなどのコンテキスト情報を自動的にログに付与できる
  • StructuredArgumentsやMarkersを使用して、イベント固有のカスタムフィールドを追加できる
  • 環境別設定により、開発環境では読みやすいテキスト形式、本番環境ではJSON形式と使い分けられる

構造化ログを導入することで、マイクロサービス環境における障害調査やパフォーマンス分析が格段に容易になります。本記事の設定例を参考に、プロジェクトの要件に合わせた構造化ログを構築してください。

参考リンク