Spring Boot でログを出力する方法

# Spring Boot でログを出力する方法

## 目的

Spring Boot でログを出力するを手順を学んで理解を深めます。  

## 実現すること

ローカル環境の Spring Boot アプリにログを出力する設定を組み込み、標準出力とファイルにログを出力します。  
ログを出力する SLF4J 実装ライブラリには [Logback](https://logback.qos.ch/) を使用します。  

## 技術背景

SLF4J (Simple Logging Facade for Java) とは？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

### Java アプリケーションのロギング

[SLF4J](https://www.slf4j.org/) (Simple Logging Facade for Java) は、Java アプリケーションで使用されるオープンソースのロギングフレームワークです。SLF4J は、ログ記述に関する Java のAPI を抽象化して、アプリケーションのコードをロギングフレームワークに依存しない形で書くことを可能にします。つまり、アプリケーションが複数のロギングフレームワークに依存することなく、ロギング機能を使用できるようになります。  


[Logback](https://logback.qos.ch/) は、SLF4J API を実装するバックエンドの1つです。つまり、SLF4J API を使用してログを出力するアプリケーションで、Logback をバックエンドとして使用することができます。Logback は、高速で柔軟性があり、多くの機能を提供するロギングフレームワークであり、SLF4J と組み合わせて使用することで、一貫性のあるログ記述を維持することができます。  

したがって、SLF4J と Logback は、アプリケーションのロギング機能を改善するために一緒に使用されることが一般的であり、組み合わせて使用することで、柔軟性、パフォーマンス、一貫性のあるログ出力を提供することができます。

</details>

## 開発環境

+ Windows 11 Home 22H2 を使用しています。  
+ [WSL](https://learn.microsoft.com/ja-jp/windows/wsl/about) の Ubuntu を操作していきますので macOS の方も参考にして頂けます。  

<details>

WSL (Microsoft Store アプリ版)

```sh
> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
```

Ubuntu

```sh
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
```

Java JDK [※ 最小構成 Java JDK の導入と Hello World!](https://qiita.com/hiroxpepe/items/8650664e466906a57996)

```sh
$ java -version
openjdk version "11.0.17" 2022-10-18
OpenJDK Runtime Environment (build 11.0.17+8-post-Ubuntu-1ubuntu222.04)
OpenJDK 64-Bit Server VM (build 11.0.17+8-post-Ubuntu-1ubuntu222.04, mixed mode, sharing)
```

Maven [※ 最小構成 Maven の導入と Hello World!](https://qiita.com/hiroxpepe/items/40f7f199b9be7f9e3b5b)

```sh
$ mvn -version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 11.0.17, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
```

</details>

※ この記事では基本的に Ubuntu のターミナルで操作を行います。

## ログを出力する手順

### 最小限の Spring Boot アプリを作成する。  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

#### プロジェクトフォルダの作成

```sh
$ cd ~
$ mkdir -p tmp/hello-spring-log
$ cd ~/tmp/hello-spring-log
```

※ ~/tmp/hello-spring-log をプロジェクトフォルダとします。  

#### アプリケーションクラスの作成

※ 構成を単純にする為に全ての要素を記述しています。  

```sh
$ mkdir -p src/main/java/com/example/spring
$ vim src/main/java/com/example/spring/SpringbootApplication.java
```

ファイルの内容

```java:SpringbootApplication.java
package com.example.spring;

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 SpringbootApplication {

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

    @GetMapping("/hello")
    public String hello() {
        return "Hello World!";
    }
}
```

#### pom.xml の作成

```sh
$ vim pom.xml
```

ファイルの内容

```xml: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>2.7.8</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>hello-spring-log</artifactId>
    <version>1.0</version>
    <name>hello-spring-log</name>

    <properties>
        <java.version>11</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

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

    <build>
        <finalName>app</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
```

#### Spring Boot アプリを実行

Java アプリビルド

```sh
$ mvn clean install
```

Java アプリ起動 (Ctrl + C で停止します)

```sh
$ mvn spring-boot:run
```

別ターミナルからコマンドで確認

```sh
$ curl http://localhost:8080/hello
Hello World!
```

Spring Boot アプリから "Hello World!" が取得出来ました。  

</details>

### ログを処理するライブラリを追加

pom.xml を編集します。

```sh
$ vim pom.xml
```

ファイルの内容

```xml: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>2.7.8</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>hello-spring-log</artifactId>
    <version>1.0</version>
    <name>hello-spring-log</name>

    <properties>
        <java.version>11</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

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

        <!-- Log -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.6</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.6</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>app</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
```

<details>
<summary>説明</summary>

※ 追加箇所を一部抜粋

```xml
<!-- Log -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.6</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.6</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
```

LogBack を使用する場合、通常 logback-core と logback-classic の2つの依存関係を含める必要があります。これらの依存関係には、ログ記録機能と SLF4J（Simple Logging Facade for Java）の実装が含まれています。したがって logback-classic は SLF4J に依存し、logback-core は logback-classic に依存します。

</details>

### Spring Boot の設定ファイル作成

```
$ mkdir -p src/main/resources
$ vim src/main/resources/application.properties
```

ファイルの内容

```sh:application.properties
spring.profiles.active=develop
```

<details>
<summary>説明</summary>

spring.profiles.active は、Spring Boot アプリのプロファイルを設定するためのプロパティの一つです。このプロパティは、アプリの起動時にアクティブにするプロファイルを指定するために使用されます。  

例えば develop プロファイルには、開発時に便利な機能やロギングレベルが DEBUG に設定されるような設定が含まれる場合があります。一方、本番環境での動作に適した設定が含まれる production プロファイルを指定することもできます。

</details>

### ログ設定ファイルを追加

※ Spring Boot アプリケーションでは、デフォルトで src/main/resources ディレクトリにある logback-spring.xml ファイルが自動的に読み込まれます。  

logback-spring.xml を作成します。

```sh
$ vim src/main/resources/logback-spring.xml
```

ファイルの内容

```xml:logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <springProfile name="develop">
        <include resource="org/springframework/boot/logging/logback/defaults.xml" />
        <include resource="org/springframework/boot/logging/logback/console-appender.xml" />

        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>[%-5le] %lo{0}.%M:%L - %msg%n</pattern>
            </encoder>
        </appender>

        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>./log/app.log</file>
            <append>true</append>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>./log/app.%d{yyyy-MM-dd}_%i.log.gz</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>5MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <maxHistory>31</maxHistory>
                <cleanHistoryOnStart>true</cleanHistoryOnStart>
            </rollingPolicy>
            <encoder>
                <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS} [%-5le] [%t] %C.%M:%L - %m%n</pattern>
            </encoder>
        </appender>

        <logger name="org.springframework" level="INFO" />
        <logger name="com.example" level="DEBUG" />

        <root level="WARN">
            <appender-ref ref="STDOUT" />
            <appender-ref ref="FILE" />
        </root>
    </springProfile>

</configuration>
```

<details>
<summary>説明</summary>

```xml:logback-spring.xml ※一部抜粋
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
```

この設定は Spring Boot ロギングのデフォルト設定ファイルをインクルードしています。  

[公式 Spring Boot ロギングのデフォルト設定](https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback)  

```xml:logback-spring.xml ※一部抜粋
<pattern>[%-5le] %lo{0}.%M:%L - %msg%n</pattern>
```

この設定はログ出力のフォーマットを指定しています。  

|表記|意味|
|:--|:--|
|%le |ログイベントのレベル|
|%lo |ロガー名            |
|%M  |メソッド名          |
|%L  |行番号              |
|%msg|ログメッセージ本体  |
|%n  |改行                |

```xml:logback-spring.xml 
<pattern>%date{yyyy-MM-dd HH:mm:ss.SSS} [%-5le] [%t] %C.%M:%L - %m%n</pattern>
```

この設定はログ出力のフォーマットを指定しています。  

|表記|意味|
|:--|:--|
|%date{yyyy-MM-dd HH:mm:ss.SSS}|ログが出力された日時を指定されたフォーマットで表示します。|
|%-5le                         |ログレベルを5文字の幅で表示します。%-5は左揃えを指定するためのフラグです。|
|%t                            |ログを出力したスレッド名を表示します。|
|%C.%M:%L                      |ログを出力したクラス、メソッド、行番号を表示します。|
|%m                            |ログメッセージを表示します。|
|%n                            |改行を挿入します。|

```xml:logback-spring.xml ※一部抜粋
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>./log/app.%d{yyyy-MM-dd}_%i.log.gz</fileNamePattern>
    <timeBasedFileNamingAndTriggeringPolicy
        class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
        <maxFileSize>5MB</maxFileSize>
    </timeBasedFileNamingAndTriggeringPolicy>
    <maxHistory>31</maxHistory>
    <cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
```

この設定は、時間単位でログファイルをローテーションする方法を指定しています。

+ ログファイル名は ./log/app.%d{yyyy-MM-dd}_%i.log.gz ファイル名パターンに従って命名されます。
+ また SizeAndTimeBasedFNATP クラスを使用して、ファイルサイズと時間に基づくローテーションを設定しています。
+ ログファイルの最大サイズは 5MB に設定されており、maxHistory プロパティを使用してログファイルの最大保存日数を 31 日に設定しています。
+ cleanHistoryOnStart プロパティはアプリケーションの起動時に古いログファイルを自動的に削除するかどうかを設定します。  

</details>

### Java コードにログを出力するコードを追加

```sh
$ vim src/main/java/com/example/spring/SpringbootApplication.java
```

ファイルの内容

```java:SpringbootApplication.java
package com.example.spring;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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 SpringbootApplication {

    private static final Logger log = LoggerFactory.getLogger(SpringbootApplication.class);

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

    @GetMapping("/hello")
    public String hello() {
        log.info("Hello World!");
        return "Hello World!";
    }
}
```

<details>
<summary>説明</summary>

```java:SpringbootApplication.java ※一部抜粋
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
```

SLF4J を使用して Java のログを出力するために必要なクラス、インタフェースをインポートしています。

```java:SpringbootApplication.java ※一部抜粋
private static final Logger log = LoggerFactory.getLogger(SpringbootApplication.class);
```

LoggerFactory.getLogger() メソッドには、ロギング機能を使用するクラスの名前を渡します。通常、クラス単位でロガーを定義するためクラスの完全修飾名を指定します。このロガーオブジェクトは、SLF4J を通じてログ出力のために使用されます。  

```java:SpringbootApplication.java ※一部抜粋
log.info("Hello World!");
```

SLF4J の Logger インタフェース info() メソッドを呼び出してログにメッセージ "Hello World!" を出力しています。  

#### SLF4J のログレベル

|レベル|概要|
|:--|:--|
|TRACE|デバッグに使用される非常に詳細な情報|
|DEBUG|デバッグ情報|
|INFO |一般的な情報|
|WARN |警告情報|
|ERROR|エラー情報|
|FATAL|致命的なエラー|

</details>

### ディレクトリ・ファイル構成

```sh
$ tree -I log -I target
.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── spring
        │               └── SpringbootApplication.java
        └── resources
            ├── application.properties
            └── logback-spring.xml
```

#### Spring Boot アプリを実行

Java アプリビルド

```sh
$ mvn clean install
```

Java アプリ起動 (Ctrl + C で停止します)

```sh
$ mvn spring-boot:run
```

別ターミナルのコマンドで確認

```sh
$ curl http://localhost:8080/hello
Hello World!
```

ターミナルに以下のようにログが表示されました。

```sh
[INFO ] SpringbootApplication.hello:23 - Hello World!
```

また、/log/app.log に以下のように記述されました。

```sh
2023-02-21 17:25:33.785 [INFO ] [http-nio-8080-exec-1] com.example.spring.SpringbootApplication.hello:23 - Hello World!
```

### 非同期でログを出力する設定

<details>
<summary>こちらを展開してご覧いただけます。</summary>

#### 非同期バージョン logback-spring.xml

```xml:logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <springProfile name="develop">
        <include resource="org/springframework/boot/logging/logback/defaults.xml" />
        <include resource="org/springframework/boot/logging/logback/console-appender.xml" />

        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>[%-5le] %lo{0}.%M:%L - %msg%n</pattern>
            </encoder>
        </appender>

        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>./log/app.log</file>
            <append>true</append>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>./log/app.%d{yyyy-MM-dd}_%i.log.gz</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>5MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <maxHistory>31</maxHistory>
                <cleanHistoryOnStart>true</cleanHistoryOnStart>
            </rollingPolicy>
            <encoder>
                <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS} [%-5le] [%t] %C.%M:%L - %m%n</pattern>
            </encoder>
        </appender>

        <logger name="org.springframework" level="INFO" />
        <logger name="com.example" level="DEBUG" />

        <appender name="ASYNC_STDOUT" class="ch.qos.logback.classic.AsyncAppender">
            <appender-ref ref="STDOUT" />
            <includeCallerData>true</includeCallerData>
        </appender>

        <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
            <queueSize>512</queueSize>
            <neverBlock>false</neverBlock>
            <appender-ref ref="FILE" />
            <includeCallerData>true</includeCallerData>
        </appender>

        <root level="WARN">
            <appender-ref ref="ASYNC_STDOUT" />
            <appender-ref ref="ASYNC_FILE" />
        </root>
    </springProfile>

</configuration>
```

+ この例では、AsyncAppender を定義して、FILE アペンダーと STDOUT アペンダーを非同期に呼び出すように設定しています。

+ \<queueSize\> は、AsyncAppender が使用するキューのサイズを指定します。キューはログイベントを一時的に保持するために使用されます。設定値は必要に応じて調整できますが、設定値を大きくしすぎるとメモリ消費量が増加することがあります。  

+ \<neverBlock\> は、キューがいっぱいの場合にログイベントをブロックするかどうかを制御します。この値が true の場合、キューがいっぱいでもログイベントをブロックせずに破棄します。false の場合、キューがいっぱいの場合にはログイベントをブロックします。  

+ \<includeCallerData\> は、Logback のロギングイベントに呼び出し元情報（caller data）を含めるように設定するための設定です。呼び出し元情報とは、ログイベントが発生した場所（クラス名、メソッド名、行番号）を示す情報です。この設定を有効にすると、呼び出し元情報をロギングイベントに含めることができます。ただし、この設定は、パフォーマンスの観点から、非同期アペンダーを使用する場合には注意が必要です。  

</details>

## まとめ

+ Ubuntu の 最小構成の Java 開発環境で Spring Boot アプリにログ出力機能を組みこむことが出来ました。