Spring Boot 3のアプリケーションログをGoogle Cloud Logging向けに最適化する

# はじめに
現在、Spring Boot3 x Google Cloudでアプリケーション開発を行っています。

Cloud Logging、便利ですよね。
アプリケーションで標準出力したログを拾ってよしなに表示してくれる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143400/53419289-3a06-978a-1bfa-c1b26a05836e.png)
ただ、そのままだと上記のようにErrorログも標準出力として表示されてしまっていたり、
タイムスタンプがログのPayloadにもそのまま出力されていたり、
いろいろと痒いところに手が届かないので、この辺をいい感じに整備したい。

# 構造化ロギング
Cloud Loggingは、特定のフィールドを含むJSONログを自動的にマッピングして読み込んでくれます。
https://cloud.google.com/logging/docs/structured-logging?hl=ja#special-payload-fields

例えば、JSONログ中の`severity`フィールドにログの重要度(INFO、ERRORなど)を含めると、その重要度に応じてログを振り分けて表示してくれるようになります。

それ以外にも同一リクエストのログをグルーピングするのに利用される`trace`フィールドなど、含めておいた方が後々嬉しいフィールドがあります。

# spring-cloud-gcp-starter-logging
Google CloudはSpring環境向けに様々なモジュールを提供しています。
https://spring.io/projects/spring-cloud-gcp

Cloud Logging向けのstarterモジュールもあるので、これを使っていきます。
https://mvnrepository.com/artifact/com.google.cloud/spring-cloud-gcp-starter-logging

## build.gradle
依存を追加します。
```gradle:build.gradle
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'com.google.cloud:spring-cloud-gcp-starter-logging:4.5.0' // 追加
}
```

## logback-spring.xml
`spring-cloud-gcp-starter-logging`に`logback-json-appender.xml`が定義されているので、それを読み込みます。基本的にこれだけでOK
```xml:logback-spring.xml
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="com/google/cloud/spring/logging/logback-json-appender.xml"/>

    <root level="INFO">
        <appender-ref ref="CONSOLE_JSON"/>
    </root>
</configuration>
```
こいつをresources配下に置いてやるだけ

## DemoApplication
```java:DemoApplication.java
package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class DemoApplication {

    @Autowired
    private DemoService demoService;

    @GetMapping("/")
    public void demo() {
        demoService.exec();
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}
```
```java:DemoService.java
package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class DemoService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DemoService.class);

    public void exec() {
        LOGGER.info("This is INFO.");
        LOGGER.warn("This is WARNING.");
        LOGGER.error("This is ERROR.");
    }

}
```

## デプロイしてログ確認
Severityが適切に反映されていていい感じですね！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143400/2690f51e-0e7f-eee3-b2b3-87f27882df9b.png)

# 起動時のロゴも消したい
`application.properties`(もしくは`yaml`)に以下を記述すればOK
```properties:application.properties
spring.main.banner-mode=off
```

# traceを含める
Google Cloudでは、同一リクエストをグルーピングできる`trace`を含めることができます。
`spring-cloud-gcp-starter-logging`を使えば、こいつも自動的にログに含まれるようになります。便利。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143400/9120d6ce-6e39-bd53-8b2b-092843c51f37.png)
「一致エントリを表示」を選択すると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143400/19fcf687-e2cd-5c9b-73d8-9bf6e385508e.png)
同一リクエストで出力されたログを絞り込めます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143400/af595286-ac9b-7431-48db-a3106ebb3dba.png)

## Asyncを使った場合のtrace
通常、Spring BootのAsync機能を使うと、traceが欠落してしまいます。
これを解決するには、`micrometer-tracing-bridge-brave`を利用して、AsyncExecutorをwrapしてあげます。

```gradle:build.gradle
dependencies {
	implementation 'io.micrometer:micrometer-tracing-bridge-brave:1.1.2' // 追加
}
```
```java:AsyncTraceContextConfig.java
package com.example.demo;

import io.micrometer.context.ContextExecutorService;
import io.micrometer.context.ContextSnapshotFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@EnableAsync
@Configuration(proxyBeanMethods = false)
public class AsyncTraceContextConfig implements AsyncConfigurer {

    private final ThreadPoolTaskExecutor taskExecutor;

    public AsyncTraceContextConfig(ThreadPoolTaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    @Override
    public Executor getAsyncExecutor() {
        return ContextExecutorService.wrap(taskExecutor.getThreadPoolExecutor(),
                () -> ContextSnapshotFactory.builder().build().captureAll());
    }

}
```

# その他
以前は
* logback-jackson (https://mvnrepository.com/artifact/ch.qos.logback.contrib/logback-jackson)
* logstash-logback-encoder (https://github.com/logfellow/logstash-logback-encoder)

この子たちを使っていたりもしたのですが、Google Cloud x Spring Bootに関しては`spring-cloud-gcp-starter-logging`を使ってあげれば、あまり難しい設定も要らずに簡単にログ周りを整理できるので良いなと思いました。