Spring BootでLogbackを利用したパターンログとJSONログの出力
APIサーバーを運用していると、運用状況についてログを残したり、デバッグのためにログを残すことが多い。
伝統的にSpringではログを残す時にフォーマットが存在するが、Raw Text形式で残すことが多く、このような場合、筆者としては可視性の面ではJSON形式でログを残すよりも良いと考える。
しかし、ログを検索し、フィルタリングする場合には話が異なり、AWS CloudWatchのようにログを検索できるプラットフォームの場合、Raw Text形式でロギングするとどの関数でどのデータが出力されたのかフィルタリングが難しい。
特に複合的に複数の条件で検索しなければならない場合はさらに難しい。
もちろん通常、ログに特定のキーワードを追加してそのキーワードと一緒に検索すれば問題はないが、JSON形式でログを残せば形式的に検索がより容易になる。 (詳細な実装は分からないが、おそらくインデキシングのような部分も設定すれば利点があるのではないかと思う。)
これを実現するために、ローカルではより可視性の高いRaw Text形式で、デプロイ環境ではJSON形式でログを分けて残す方法を見てみよう。
Logback
LogbackはSLF4Jの実装であり、Spring Bootで基本的に使用されているロギングライブラリである。
基本的にspring-boot-starter-web
依存関係を追加するとLogbackが自動的に追加されるため、特別な依存関係の追加は必要ない。
Slf4j
Slf4jはSimple Logging Facade for Javaの略称で、Javaのロギングライブラリを抽象化したインターフェイスである。
文字通りインターフェイスであるため、実際の実装はLogback、Log4j、Log4j2、JULなど様々なロギングライブラリを使用できる。
このように設定する理由は、特定のロギングライブラリで脆弱性が発見された場合や、他のロギングライブラリに変更が必要な場合に、実装だけを変更すれば済むためだと考えられる。
Logback設定
基本的にLogbackの設定はlogback-spring.xml
ファイルを生成して設定できる。
この設定は大きく、Appender、Logger、Encoderで構成される。
Appender
Appenderはログをどこに出力するかを決定する役割をする。
基本的にConsoleAppender
、FileAppender
、RollingFileAppender
、SyslogAppender
など様々なAppenderが存在する。
Logger
Loggerはログを残す対象を決定する役割をする。
Loggerは名前を持ち、その名前を持つLoggerだけにログを残す。
Encoder
Encoderはログをどのような形式で出力するかを決定する役割をする。
基本的にPatternLayoutEncoder
を使用するとRaw Text形式でログを残すことができ、JsonEncoder
を使用するとJSON形式でログを残すことができる。
筆者はローカルではPatternLayoutEncoder
を使用し、デプロイ環境ではJsonLayout
を使用してログを残す方法を案内する。
JsonLayout
JsonLayout
はLogbackが提供するLayoutで、ログをJSON形式で出力できるようにする。
JsonLayout
を使用するためにはlogback-json-classic
とlogback-jackson
依存関係を追加する必要がある。
dependencies {
implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5'
implementation 'ch.qos.logback.contrib:logback-jackson:0.1.5'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
}
logback-spring.xml
その後、resources
ディレクトリ内にlogback-spring.xml
ファイルを生成し、以下のように設定する。
Appender設定
まずJSON形式でログを出力するAppenderを設定する。
取得した依存関係はlayoutパートで使用する。
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<appender class="ch.qos.logback.core.ConsoleAppender" name="CONSOLE_JSON">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.contrib.json.classic.JsonLayout">
<appendLineSeparator>true</appendLineSeparator>
<jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter"/>
<timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</timestampFormat>
<timestampFormatTimezoneId>Etc/Utc</timestampFormatTimezoneId>
</layout>
</encoder>
</appender>
<appender class="ch.qos.logback.core.ConsoleAppender" name="CONSOLE_STDOUT">
<encoder>
<pattern>[%thread] %highlight([%-slevel]) %cyan(%logger{15}) - %msg%n</pattern>
</encoder>
</appender>
<!-- 中略 -->
</configuration>
まだappenderのみ設定したもので、実際にログを使用する部分はまだ設定していない。
パターンは簡潔にLogger、Messageのみ出力するように設定した。
CONSOLE_JSON
Appender
CONSOLE_JSON
AppenderはJSON形式でログを出力するように設定した。
その後の追加設定は以下の通り。
timestampFormat
: 日付の形式を指定する。(筆者はRFC3339形式で指定した。)timestampFormatTimezoneId
はタイムゾーンを指定する。
CONSOLE_STDOUT
Appender
CONSOLE_STDOUT
AppenderはRaw Text形式でログを出力するように設定した。
Logger設定
続いてプロファイルに応じてログを出力するAppenderを設定する。
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<!-- 中略 -->
<springProfile name="qa, dev">
<root level="INFO">
<appender-ref ref="CONSOLE_JSON"/>
</root>
</springProfile>
<springProfile name="local">
<root level="INFO">
<appender-ref ref="CONSOLE_STDOUT"/>
</root>
</springProfile>
</configuration>
上記の設定では、springProfile
タグを使用してプロファイルに応じてログを出力するAppenderを設定した。
qa
、dev
プロファイルではCONSOLE_JSON
Appenderを使用するように設定した。local
プロファイルではCONSOLE_STDOUT
Appenderを使用するように設定した。
例コード
package com.example.demo.product.service;
import com.example.demo.product.domain.dto.ProductDto;
import com.example.demo.product.repository.ProductRepository;
import com.example.demo.product.util.EventNumberPicker;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
public List<ProductDto> listProducts() {
log.info("Hello World");
return productRepository
.findAll()
.stream()
.map(product -> new ProductDto(product, EventNumberPicker.pick(1, 1000)))
.toList();
}
}
結果
環境別にロギングをこのように分ければ、ローカルでは可視性を確保しつつ、デプロイ環境では検索をより便利にすることができるだろう。
CONSOLE_STDOUT
CONSOLE_JSON
個人的には汚いと思うが、検索は便利になるだろう。