CLOVER🍀

That was when it all began.

HTTPリクエスト/レスポンスをログ出力するLogbookをSpring Boot 3で試す

これは、なにをしたくて書いたもの?

HTTPリクエスト/レスポンス(クライアント、サーバー)のログ出力をしてくれる、Logbookというライブラリーがあります。

1度試しておきたかったので、今回はSpring Bootで試してみることにしました。

Logbook

LogbookのGitHubリポジトリーはこちら。

GitHub - zalando/logbook: An extensible Java library for HTTP request and response logging

機能としては以下になっています。

  • ログ出力
    • HTTPリクエストおよびレスポンスの記録(HTTPボディ含む)
    • 認証されていないリクエストなどは、部分的な記録なこともある
  • カスタマイズ
    • ログ出力、フォーマット、ログ出力の条件
  • 多数のフレームワーク、HTTPクライアントライブラリーのサポート
  • センシティブなデータの難読化
  • Spring Boot Auto Configuration
  • Scalyr互換
  • 理にかなったデフォルト値

Logbook / Features

サポートしているフレームワークやライブラリーはこのあたりです。

Logbook / Dependencies

JAX-RS 3.x、Spring Boot 3.xおよびSpring Framework 6.xを使う場合はJava 17以降が必要で、それ以外はJava 8以降で使用できます。

基本的な使い方はこちらを参照。

Logbook / Usage

設定はLogbookのインスタンスに対して行います。

ログの記録戦略、リクエストやレスポンスからの属性の抽出、出力する際の条件指定・フィルタリング・書式設定なども行えます。

ログ出力自体は、デフォルトではSLF4Jを使って行われますが、こちらもカスタマイズできます。

https://github.com/zalando/logbook/blob/3.9.0/logbook-core/src/main/java/org/zalando/logbook/core/DefaultHttpLogWriter.java

ログレベルはTRACEで指定されているので、少なくともLogbookのログに関してはTRACEレベル以上を出力するように設定する必要が
あります。

今回はSpring Boot 3でこのLogbookを試していきます。

Spring BootでLogbookを使う

Spring FrameworkやSpring Bootで使う場合のドキュメントはこちら。Spring Bootで使う場合は、Starterを利用することになります。

ちなみに、Spring Framework 5.xやSpring Boot 2.xで使う時はclassifierがjavaxのlogbook-servletを追加する必要があるようです。

<dependency>
    <groupId>org.zalando</groupId>
    <artifactId>logbook-servlet</artifactId>
    <version>${logbook.version}</version>
    <classifier>javax</classifier>
</dependency>

Logbook Auto ConfigurationではLogbookのインスタンスが自動構成されます。また、必要に応じてフィルターなどBean定義することで
カスタマイズも容易そうです。

logbook/logbook-spring-boot-autoconfigure/src/main/java/org/zalando/logbook/autoconfigure/LogbookAutoConfiguration.java at 3.9.0 · zalando/logbook · GitHub

設定できるプロパティーも一覧化されています。

Logbook / Usage / Spring Boot Starter / Configuration

https://github.com/zalando/logbook/blob/3.9.0/logbook-spring-boot-autoconfigure/src/main/java/org/zalando/logbook/autoconfigure/LogbookProperties.java

今回はあまりカスタマイズなど考えずに単純に組み込んでみます。

Webアプリケーションを2つ作成し、以下のような構成にしてみます。

flowchart LR
    クライアント --> |curl/HTTP| A
    A[API-A/Spring Boot] --> |RestClient/HTTP| B[API-B/Spring Boot]

この時、@RestControllerから見たリクエスト/レスポンスの記録、RestClientから見たリクエスト/レスポンスの記録を行います。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.3 2024-04-16
OpenJDK Runtime Environment (build 21.0.3+9-Ubuntu-1ubuntu122.04.1)
OpenJDK 64-Bit Server VM (build 21.0.3+9-Ubuntu-1ubuntu122.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.7 (8b094c9513efc1b9ce2d952b3b9c8eaedaf8cbf0)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.3, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-112-generic", arch: "amd64", family: "unix"

API-Bを作成する

まずは奥にあるAPI-Bから作成します。

Spring Initializrで、依存関係にwebを指定してプロジェクトを作成。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=3.3.0 \
  -d javaVersion=21 \
  -d type=maven-project \
  -d name=api-b \
  -d groupId=org.littlewings \
  -d artifactId=api-b \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.logbook.b \
  -d dependencies=web \
  -d baseDir=api-b | tar zxvf -

ディレクトリ内に移動。

$ cd api-b

生成されたソースコードは削除しておきます。

$ rm src/main/java/org/littlewings/spring/logbook/b/ApiBApplication.java src/test/java/org/littlewings/spring/logbook/b/ApiBApplicationTests.java

Maven依存関係はこのような状態になっていました。

        <properties>
                <java.version>21</java.version>
        </properties>
        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>

        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>

また、application.propertiesはこういう状態です。

src/main/resources/application.properties

spring.application.name=api-b

まずは依存関係にlogbook-spring-boot-starterを追加します。

     <dependency>
            <groupId>org.zalando</groupId>
            <artifactId>logbook-spring-boot-starter</artifactId>
            <version>3.9.0</version>
        </dependency>

単純な@RestControllerを作成。

src/main/java/org/littlewings/spring/logbook/b/HelloController.java

package org.littlewings.spring.logbook.b;

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

@RequestMapping("/b/hello")
@RestController
public class HelloController {
    @GetMapping
    public String message(@RequestParam(required = false) String word) {
        if (word != null) {
            return String.format("Hello %s!!", word);
        } else {
            return "Hello World!!";
        }
    }
}

mainメソッドを持ったクラスを作成。

src/main/java/org/littlewings/spring/logbook/b/App.java

package org.littlewings.spring.logbook.b;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

ここで、application.propertiesを変更しLogbookに関するログレベルをTRACEに変更します。

src/main/resources/application.properties

spring.application.name=api-b

server.port=9080

logging.level.org.zalando.logbook=TRACE

また、今後のためにポートも変更しておきました。

これでいったん起動。

$ mvn spring-boot:run

アクセスしてみます。

$ curl localhost:9080/b/hello
Hello World!!

これだけでリクエストとレスポンスのログが記録されました。

2024-06-09T00:03:17.198+09:00 TRACE 42419 --- [api-b] [nio-9080-exec-1] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"a85396154ef9a9c3","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"GET","uri":"http://localhost:9080/b/hello","host":"localhost","path":"/b/hello","scheme":"http","port":"9080","headers":{"accept":["*/*"],"host":["localhost:9080"],"user-agent":["curl/7.81.0"]}}
2024-06-09T00:03:17.243+09:00 TRACE 42419 --- [api-b] [nio-9080-exec-1] org.zalando.logbook.Logbook              : {"origin":"local","type":"response","correlation":"a85396154ef9a9c3","duration":100,"protocol":"HTTP/1.1","status":200,"headers":{"Content-Length":["13"],"Content-Type":["text/plain;charset=UTF-8"],"Date":["Sat, 08 Jun 2024 15:03:17 GMT"]},"body":"Hello World!!"}

フォーマットはJSONですね。

このフォーマットはHTTP、JSON、Common Log Format(CLF)、Extended Log Format(ELF)、cURL、Splunkから選べるようです。

Logbook / Usage / Phases / Formatting

QueryStringも入れてみましょう。

$ curl localhost:9080/b/hello?word=LogBook
Hello LogBook!!

ログ。

2024-06-09T00:05:56.753+09:00 TRACE 42419 --- [api-b] [nio-9080-exec-2] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"db09731279cf521b","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"GET","uri":"http://localhost:9080/b/hello?word=LogBook","host":"localhost","path":"/b/hello","scheme":"http","port":"9080","headers":{"accept":["*/*"],"host":["localhost:9080"],"user-agent":["curl/7.81.0"]}}
2024-06-09T00:05:56.756+09:00 TRACE 42419 --- [api-b] [nio-9080-exec-2] org.zalando.logbook.Logbook              : {"origin":"local","type":"response","correlation":"db09731279cf521b","duration":4,"protocol":"HTTP/1.1","status":200,"headers":{"Content-Length":["15"],"Content-Type":["text/plain;charset=UTF-8"],"Date":["Sat, 08 Jun 2024 15:05:56 GMT"]},"body":"Hello LogBook!!"}

よさそうですね。

もうひとつ@RestControllerを追加してみましょう。

src/main/java/org/littlewings/spring/logbook/b/CalcController.java

package org.littlewings.spring.logbook.b;

import org.springframework.web.bind.annotation.*;

@RequestMapping("/b/calc")
@RestController
public class CalcController {
    @GetMapping
    public CalcResponse get(CalcRequest request) {
        return new CalcResponse(request.a() + request.b());
    }

    @PostMapping
    public CalcResponse post(@RequestBody CalcRequest request) {
        return new CalcResponse(request.a() + request.b());
    }

    public record CalcRequest(int a, int b) {
    }

    public record CalcResponse(int result) {
    }
}

1度停止して起動。

$ mvn spring-boot:run

GET。

$ curl 'localhost:9080/b/calc?a=3&b=5'
{"result":8}

ログ。

2024-06-09T00:07:36.025+09:00 TRACE 42814 --- [api-b] [nio-9080-exec-1] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"e07c573c5790e411","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"GET","uri":"http://localhost:9080/b/calc?a=3&b=5","host":"localhost","path":"/b/calc","scheme":"http","port":"9080","headers":{"accept":["*/*"],"host":["localhost:9080"],"user-agent":["curl/7.81.0"]}}
2024-06-09T00:07:36.098+09:00 TRACE 42814 --- [api-b] [nio-9080-exec-1] org.zalando.logbook.Logbook              : {"origin":"local","type":"response","correlation":"e07c573c5790e411","duration":110,"protocol":"HTTP/1.1","status":200,"headers":{"Content-Type":["application/json"],"Date":["Sat, 08 Jun 2024 15:07:36 GMT"],"Transfer-Encoding":["chunked"]},"body":{"result":8}}

POST。

$ curl -XPOST -H 'Content-Type: application/json' localhost:9080/b/calc -d '{"a": 5, "b": 3}'
{"result":8}

ログ。

2024-06-09T00:08:27.818+09:00 TRACE 42814 --- [api-b] [nio-9080-exec-2] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"e2c540ee7c0b6ed0","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"POST","uri":"http://localhost:9080/b/calc","host":"localhost","path":"/b/calc","scheme":"http","port":"9080","headers":{"accept":["*/*"],"content-length":["16"],"content-type":["application/json"],"host":["localhost:9080"],"user-agent":["curl/7.81.0"]},"body":{"a":5,"b":3}}
2024-06-09T00:08:27.857+09:00 TRACE 42814 --- [api-b] [nio-9080-exec-2] org.zalando.logbook.Logbook              : {"origin":"local","type":"response","correlation":"e2c540ee7c0b6ed0","duration":39,"protocol":"HTTP/1.1","status":200,"headers":{"Content-Type":["application/json"],"Date":["Sat, 08 Jun 2024 15:08:27 GMT"],"Transfer-Encoding":["chunked"]},"body":{"result":8}}

これでAPI-Bの準備は完了しました。

API-Aを作成する

次は手前にあるAPI-Aを作成します。

このREST APIは、API-Bへリクエストを転送するプロキシの役割になります。HTTPリクエストはRestClientを使って行います。

Spring Bootプロジェクトの作成。依存関係はこちらもwebのみです。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=3.3.0 \
  -d javaVersion=21 \
  -d type=maven-project \
  -d name=api-a \
  -d groupId=org.littlewings \
  -d artifactId=api-a \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.logbook.a \
  -d dependencies=web \
  -d baseDir=api-a | tar zxvf -

ディレクトリ内へ移動。

$ cd api-a

生成されたソースコードは削除しておきます。

$ rm src/main/java/org/littlewings/spring/logbook/a/ApiAApplication.java src/test/java/org/littlewings/spring/logbook/a/ApiAApplicationTests.java

Maven依存関係など。

        <properties>
                <java.version>21</java.version>
        </properties>
        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>

        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>

application.propertiesの内容。

src/main/resources/application.properties

spring.application.name=api-a

API-Bと同じようにlogbook-spring-boot-starterを依存関係に追加しておきます。

     <dependency>
            <groupId>org.zalando</groupId>
            <artifactId>logbook-spring-boot-starter</artifactId>
            <version>3.9.0</version>
        </dependency>

まずはAPI-BのHelloControllerにリクエストを転送する@RestControllerを作成。

src/main/java/org/littlewings/spring/logbook/a/HelloController.java

package org.littlewings.spring.logbook.a;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriComponentsBuilder;
import org.zalando.logbook.spring.LogbookClientHttpRequestInterceptor;

@RequestMapping("/a/hello")
@RestController
public class HelloController {
    private RestClient restClient;

    public HelloController(RestClient.Builder restClientBuilder, LogbookClientHttpRequestInterceptor logbookClientHttpRequestInterceptor) {
        this.restClient =
                restClientBuilder
                        .baseUrl("http://localhost:9080")
                        .requestInterceptor(logbookClientHttpRequestInterceptor)
                        .build();
    }

    @GetMapping
    public String message(@RequestParam(required = false) String word) {
        if (word != null) {
            return restClient
                    .get()
                    .uri(UriComponentsBuilder.fromPath("/b/hello").queryParam("word", word).build().toUriString())
                    .retrieve()
                    .body(String.class);
        } else {
            return restClient.get().uri("/b/hello").retrieve().body(String.class);
        }
    }
}

ポイントは、RestClient.Builder#requestInterceptorにLogbookのLogbookClientHttpRequestInterceptorを設定していることです。

    public HelloController(RestClient.Builder restClientBuilder, LogbookClientHttpRequestInterceptor logbookClientHttpRequestInterceptor) {
        this.restClient =
                restClientBuilder
                        .baseUrl("http://localhost:9080")
                        .requestInterceptor(logbookClientHttpRequestInterceptor)
                        .build();
    }

LogbookのドキュメントではRestTemplateの例のみが載っていますが、こちらではRestTemplateBuilder#additionalInterceptorsで
設定することになっています。

private final RestTemplate restTemplate;
MyClient(RestTemplateBuilder builder, LogbookClientHttpRequestInterceptor interceptor){
  this.restTemplate = builder
    .additionalInterceptors(interceptor)
    .build();
}

Logbook / Usage / Spring Boot Starter / Autoconfigured beans from logbook-spring

mainメソッドを持ったクラス。

src/main/java/org/littlewings/spring/logbook/a/App.java

package org.littlewings.spring.logbook.a;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

application.propertiesでLogbookのログレベルをTRACEにします。

src/main/resources/application.properties

spring.application.name=api-a

logging.level.org.zalando.logbook=TRACE

この状態で、1度起動。

$ mvn spring-boot:run

リクエストを送ってみます。

$ curl localhost:8080/a/hello
Hello World!!

記録されたログ。

2024-06-09T00:17:28.832+09:00 TRACE 43653 --- [api-a] [nio-8080-exec-1] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"9423e7b7ce9a0a2b","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"GET","uri":"http://localhost:8080/a/hello","host":"localhost","path":"/a/hello","scheme":"http","port":"8080","headers":{"accept":["*/*"],"host":["localhost:8080"],"user-agent":["curl/7.81.0"]}}
2024-06-09T00:17:28.891+09:00 TRACE 43653 --- [api-a] [nio-8080-exec-1] org.zalando.logbook.Logbook              : {"origin":"local","type":"request","correlation":"900de281a3b2cba1","protocol":"HTTP/1.1","remote":"localhost","method":"GET","uri":"http://localhost:9080/b/hello","host":"localhost","path":"/b/hello","scheme":"http","port":"9080","headers":{"Content-Length":["0"]}}
2024-06-09T00:17:28.926+09:00 TRACE 43653 --- [api-a] [nio-8080-exec-1] org.zalando.logbook.Logbook              : {"origin":"remote","type":"response","correlation":"900de281a3b2cba1","duration":31,"protocol":"HTTP/1.1","status":200,"headers":{"Connection":["keep-alive"],"Content-Length":["13"],"Content-Type":["text/plain;charset=UTF-8"],"Date":["Sat, 08 Jun 2024 15:17:28 GMT"],"Keep-Alive":["timeout=60"]},"body":"Hello World!!"}
2024-06-09T00:17:28.942+09:00 TRACE 43653 --- [api-a] [nio-8080-exec-1] org.zalando.logbook.Logbook              : {"origin":"local","type":"response","correlation":"9423e7b7ce9a0a2b","duration":161,"protocol":"HTTP/1.1","status":200,"headers":{"Content-Length":["13"],"Content-Type":["text/plain;charset=UTF-8"],"Date":["Sat, 08 Jun 2024 15:17:28 GMT"]},"body":"Hello World!!"}

API-Aで受信したHTTPリクエスト → API-Bへ送信するHTTPリクエスト → API-Bから受信したHTTPレスポンス → API-AのHTTPレスポンス、
という順で並んでいます。

もちろんこの時、API-B側にもログが記録されています。

2024-06-09T00:17:28.910+09:00 TRACE 42814 --- [api-b] [nio-9080-exec-3] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"850715723543613a","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"GET","uri":"http://localhost:9080/b/hello","host":"localhost","path":"/b/hello","scheme":"http","port":"9080","headers":{"accept":["*/*"],"connection":["keep-alive"],"host":["localhost:9080"],"user-agent":["Java/21.0.3"]}}
2024-06-09T00:17:28.915+09:00 TRACE 42814 --- [api-b] [nio-9080-exec-3] org.zalando.logbook.Logbook              : {"origin":"local","type":"response","correlation":"850715723543613a","duration":3,"protocol":"HTTP/1.1","status":200,"headers":{"Connection":["keep-alive"],"Content-Length":["13"],"Content-Type":["text/plain;charset=UTF-8"],"Date":["Sat, 08 Jun 2024 15:17:28 GMT"],"Keep-Alive":["timeout=60"]},"body":"Hello World!!"}

以降はAPI-B側のログは割愛します。

QueryStringをつけてアクセス。

$ curl localhost:8080/a/hello?word=Logbook
Hello Logbook!!

ログ。

2024-06-09T00:19:38.563+09:00 TRACE 43653 --- [api-a] [nio-8080-exec-2] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"e14e787d34dcb37b","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"GET","uri":"http://localhost:8080/a/hello?word=Logbook","host":"localhost","path":"/a/hello","scheme":"http","port":"8080","headers":{"accept":["*/*"],"host":["localhost:8080"],"user-agent":["curl/7.81.0"]}}
2024-06-09T00:19:38.565+09:00 TRACE 43653 --- [api-a] [nio-8080-exec-2] org.zalando.logbook.Logbook              : {"origin":"local","type":"request","correlation":"830c45c9cd75a33a","protocol":"HTTP/1.1","remote":"localhost","method":"GET","uri":"http://localhost:9080/b/hello?word=Logbook","host":"localhost","path":"/b/hello","scheme":"http","port":"9080","headers":{"Content-Length":["0"]}}
2024-06-09T00:19:38.573+09:00 TRACE 43653 --- [api-a] [nio-8080-exec-2] org.zalando.logbook.Logbook              : {"origin":"remote","type":"response","correlation":"830c45c9cd75a33a","duration":7,"protocol":"HTTP/1.1","status":200,"headers":{"Connection":["keep-alive"],"Content-Length":["15"],"Content-Type":["text/plain;charset=UTF-8"],"Date":["Sat, 08 Jun 2024 15:19:38 GMT"],"Keep-Alive":["timeout=60"]},"body":"Hello Logbook!!"}
2024-06-09T00:19:38.575+09:00 TRACE 43653 --- [api-a] [nio-8080-exec-2] org.zalando.logbook.Logbook              : {"origin":"local","type":"response","correlation":"e14e787d34dcb37b","duration":13,"protocol":"HTTP/1.1","status":200,"headers":{"Content-Length":["15"],"Content-Type":["text/plain;charset=UTF-8"],"Date":["Sat, 08 Jun 2024 15:19:38 GMT"]},"body":"Hello Logbook!!"}

OKですね。

次はAPI-Bの@RestController向けのクラスを追加します。

なのですが、またLogbookClientHttpRequestInterceptorを各クラスで設定するのもめんどうです。

ここはRestClientCustomizerで設定することにします。

$ cat src/main/java/org/littlewings/spring/logbook/a/RestClientConfig.java

package org.littlewings.spring.logbook.a;

import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.zalando.logbook.spring.LogbookClientHttpRequestInterceptor;

@Configuration
public class RestClientConfig {
    @Bean
    public RestClientCustomizer restClientCustomizer(LogbookClientHttpRequestInterceptor logbookClientHttpRequestInterceptor) {
        return restClientBuilder ->
                restClientBuilder.requestInterceptor(logbookClientHttpRequestInterceptor);
    }
}

HelloControllerからはLogbookClientHttpRequestInterceptorを追加するコードを削除します。

    /*
    public HelloController(RestClient.Builder restClientBuilder, LogbookClientHttpRequestInterceptor logbookClientHttpRequestInterceptor) {
        this.restClient =
                restClientBuilder
                        .baseUrl("http://localhost:9080")
                        .requestInterceptor(logbookClientHttpRequestInterceptor)
                        .build();
    }
    */

    public HelloController(RestClient.Builder restClientBuilder) {
        this.restClient =
                restClientBuilder
                        .baseUrl("http://localhost:9080")
                        .build();
    }

API-BのCalcControllerに対する@RestControllerを追加。

src/main/java/org/littlewings/spring/logbook/a/CalcController.java

package org.littlewings.spring.logbook.a;

import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriComponentsBuilder;

@RequestMapping("/a/calc")
@RestController
public class CalcController {
    private RestClient restClient;

    public CalcController(RestClient.Builder restClientBuilder) {
        this.restClient =
                restClientBuilder
                        .baseUrl("http://localhost:9080")
                        .build();
    }

    @GetMapping
    public CalcResponse get(CalcRequest request) {
        return restClient
                .get()
                .uri(UriComponentsBuilder.fromPath("/b/calc").queryParam("a", request.a()).queryParam("b", request.b()).build().toUriString())
                .retrieve()
                .body(CalcResponse.class);
    }

    @PostMapping
    public CalcResponse post(@RequestBody CalcRequest request) {
        return restClient
                .post()
                .uri("/b/calc")
                .body(request)
                .retrieve()
                .body(CalcResponse.class);
    }

    public record CalcRequest(int a, int b) {
    }

    public record CalcResponse(int result) {
    }
}

アプリケーションを1度停止して、再度起動。

$ mvn spring-boot:run

まずは修正後の@RestControllerから確認してみましょう。

$ curl localhost:8080/a/hello
Hello World!!

ログ。

2024-06-09T00:24:01.780+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-1] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"a983b2ad61830d0f","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"GET","uri":"http://localhost:8080/a/hello","host":"localhost","path":"/a/hello","scheme":"http","port":"8080","headers":{"accept":["*/*"],"host":["localhost:8080"],"user-agent":["curl/7.81.0"]}}
2024-06-09T00:24:01.834+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-1] org.zalando.logbook.Logbook              : {"origin":"local","type":"request","correlation":"96c944673b11094f","protocol":"HTTP/1.1","remote":"localhost","method":"GET","uri":"http://localhost:9080/b/hello","host":"localhost","path":"/b/hello","scheme":"http","port":"9080","headers":{"Content-Length":["0"]}}
2024-06-09T00:24:01.863+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-1] org.zalando.logbook.Logbook              : {"origin":"remote","type":"response","correlation":"96c944673b11094f","duration":25,"protocol":"HTTP/1.1","status":200,"headers":{"Connection":["keep-alive"],"Content-Length":["13"],"Content-Type":["text/plain;charset=UTF-8"],"Date":["Sat, 08 Jun 2024 15:24:01 GMT"],"Keep-Alive":["timeout=60"]},"body":"Hello World!!"}
2024-06-09T00:24:01.881+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-1] org.zalando.logbook.Logbook              : {"origin":"local","type":"response","correlation":"a983b2ad61830d0f","duration":179,"protocol":"HTTP/1.1","status":200,"headers":{"Content-Length":["13"],"Content-Type":["text/plain;charset=UTF-8"],"Date":["Sat, 08 Jun 2024 15:24:01 GMT"]},"body":"Hello World!!"}

OKですね。

次は追加した@RestControllerです。

GET。

$ curl 'localhost:8080/a/calc?a=5&b=3'
{"result":8}

ログ。

2024-06-09T00:25:02.569+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-2] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"c9c87ff18eaf784a","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"GET","uri":"http://localhost:8080/a/calc?a=5&b=3","host":"localhost","path":"/a/calc","scheme":"http","port":"8080","headers":{"accept":["*/*"],"host":["localhost:8080"],"user-agent":["curl/7.81.0"]}}
2024-06-09T00:25:02.579+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-2] org.zalando.logbook.Logbook              : {"origin":"local","type":"request","correlation":"afae7f506d56ce6c","protocol":"HTTP/1.1","remote":"localhost","method":"GET","uri":"http://localhost:9080/b/calc?a=5&b=3","host":"localhost","path":"/b/calc","scheme":"http","port":"9080","headers":{"Content-Length":["0"]}}
2024-06-09T00:25:02.597+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-2] org.zalando.logbook.Logbook              : {"origin":"remote","type":"response","correlation":"afae7f506d56ce6c","duration":7,"protocol":"HTTP/1.1","status":200,"headers":{"Connection":["keep-alive"],"Content-Type":["application/json"],"Date":["Sat, 08 Jun 2024 15:25:02 GMT"],"Keep-Alive":["timeout=60"],"Transfer-Encoding":["chunked"]},"body":{"result":8}}
2024-06-09T00:25:02.646+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-2] org.zalando.logbook.Logbook              : {"origin":"local","type":"response","correlation":"c9c87ff18eaf784a","duration":78,"protocol":"HTTP/1.1","status":200,"headers":{"Content-Type":["application/json"],"Date":["Sat, 08 Jun 2024 15:25:02 GMT"],"Transfer-Encoding":["chunked"]},"body":{"result":8}}

POST。

$ curl -XPOST -H 'Content-Type: application/json' localhost:8080/a/calc -d '{"a": 5, "b": 3}'
{"result":8}

ログ。

2024-06-09T00:25:59.111+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-6] org.zalando.logbook.Logbook              : {"origin":"remote","type":"request","correlation":"f61193f865f23243","protocol":"HTTP/1.1","remote":"127.0.0.1","method":"POST","uri":"http://localhost:8080/a/calc","host":"localhost","path":"/a/calc","scheme":"http","port":"8080","headers":{"accept":["*/*"],"content-length":["16"],"content-type":["application/json"],"host":["localhost:8080"],"user-agent":["curl/7.81.0"]},"body":{"a":5,"b":3}}
2024-06-09T00:25:59.118+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-6] org.zalando.logbook.Logbook              : {"origin":"local","type":"request","correlation":"91068428bef1d88e","protocol":"HTTP/1.1","remote":"localhost","method":"POST","uri":"http://localhost:9080/b/calc","host":"localhost","path":"/b/calc","scheme":"http","port":"9080","headers":{"Content-Length":["13"],"Content-Type":["application/json"]},"body":{"a":5,"b":3}}
2024-06-09T00:25:59.126+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-6] org.zalando.logbook.Logbook              : {"origin":"remote","type":"response","correlation":"91068428bef1d88e","duration":6,"protocol":"HTTP/1.1","status":200,"headers":{"Connection":["keep-alive"],"Content-Type":["application/json"],"Date":["Sat, 08 Jun 2024 15:25:59 GMT"],"Keep-Alive":["timeout=60"],"Transfer-Encoding":["chunked"]},"body":{"result":8}}
2024-06-09T00:25:59.130+09:00 TRACE 44263 --- [api-a] [nio-8080-exec-6] org.zalando.logbook.Logbook              : {"origin":"local","type":"response","correlation":"f61193f865f23243","duration":18,"protocol":"HTTP/1.1","status":200,"headers":{"Content-Type":["application/json"],"Date":["Sat, 08 Jun 2024 15:25:59 GMT"],"Transfer-Encoding":["chunked"]},"body":{"result":8}}

OKですね。

おわりに

LogbookをSpring Bootに組み込み、HTTPリクエスト・レスポンスをログ出力するようにしてみました。

今回はとりあえず導入するだけだったのですが、適用するだけなら簡単にできましたね。ここからログに記録していいもの、してはいけないもの
などをわけていったり出力条件をカスタマイズなどしていくとそれなりに大変だと思うのですが、まずは押さえるところからということで。

こういうアクセス内容を記録するものは割とよくやることだと思うので、覚えておくと便利かなと。