チュートリアル相当
Spring Boot 3.4のstructured loggingサポート紹介エントリ https://spring.io/blog/2024/08/23/structured-logging-in-spring-boot-3-4 をひととおり試す。
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.6'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
src/main/resources/application.properties に以下を追加。
logging.structured.format.console=logstash
適当にRestControllerを作成して実行する。
{"@timestamp":"2025-05-25T16:13:19.1703503+09:00","@version":"1","message":"info","logger_name":"org.example.app.App","thread_name":"http-nio-8080-exec-4","level":"INFO","level_value":20000}
3.4よりも前ではdependencyにlogstash-logback-encoder追加が必要だったが不要になったのが分かる。
コンソールに加えてファイル出力。
logging.structured.format.file=logstash logging.file.name=log.json
MDCとかFluent Logging APIとかでログ出力にフィールド追加。
import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j public class App { @GetMapping("/test") public String sample() { MDC.put("userId", "111"); log.info("MDC"); MDC.remove("userId"); log.atInfo().setMessage("fluent-logging-api").addKeyValue("userId", "222").log(); // (以下略)
以下はログ出力例。
{(省略),"message":"MDC",(省略),"userId":"111"}
{(省略),"message":"fluent-logging-api",(省略),"userId":"222"}
ECSが定義する各種フィールドの利用。
logging.structured.format.console=ecs logging.structured.ecs.service.name=MyService logging.structured.ecs.service.version=1 logging.structured.ecs.service.environment=Production logging.structured.ecs.service.node-name=Primary
以下はログ出力例(見やすく整形済)。
{ "@timestamp":"2025-05-25T07:36:14.541178800Z", "log.level":"INFO", "process.pid":3032, "process.thread.name":"http-nio-8080-exec-1", "service.name":"MyService", "service.version":"1", "service.environment":"Production", "service.node.name":"Primary", "log.logger":"org.example.app.App", "message":"info", "ecs.version":"8.11" }
事前定義以外の独自ログ出力フォーマットをするためのカスタマイズ。
まずStructuredLogFormatter<ILoggingEvent>の実装を用意する。
package org.example.app; import ch.qos.logback.classic.spi.ILoggingEvent; import org.springframework.boot.logging.structured.StructuredLogFormatter; public class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> { @Override public String format(ILoggingEvent event) { return "time=" + event.getTimeStamp() + " level=" + event.getLevel() + " message=" + event.getMessage() + "\n"; } }
上記をログ出力に使用するよう設定する。
logging.structured.format.console=org.example.app.MyStructuredLoggingFormatter
以下はログ出力例。
time=1748159156000 level=INFO message=info
以下で独自jsonに出来るのでログ出力フォーマットを自由にいじれるのが分かる。
public class MyStructuredLoggingFormatter implements StructuredLogFormatter<ILoggingEvent> { @Override public String format(ILoggingEvent event) { return """ { "time":"%s", "level":"%s", "message":"%s" } """.formatted( event.getTimeStamp(), event.getLevel(), event.getMessage()); } }
色々試す
https://docs.spring.io/spring-boot/reference/features/logging.html#features.logging.structured のリファレンスを基に色々と試す。
事前定義済みフォーマットecs/gelf/logstash
logging.structured.format.[console|file] は三種類が定義済み。
ecs- Elastic Common Schema (ECS)gelf- Graylog Extended Log Format (GELF)logstash- Logstash
リファレンスにある通り、プロパティを通して変更可能な項目は各種フォーマットごとに異なる。また、試した限りでは、logstashだけMarkerをtagsプロパティとして出力する。他にも細々とした違いがあるのだと思われる。
既存のログ出力設定との連携
既存のログ出力設定、たとえば logback-spring.xmlなど、との連携にはappenderを修正する。以下のように src/main/resources/logback-spring.xml をすればコンソールログ出力がecsになる。
<?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="org.springframework.boot.logging.logback.StructuredLogEncoder"> <format>ecs</format> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> </configuration>
上はformatを直接指定しているが、リファレンスの例にもある通り、以下でプロパティ値を参照できる。
<encoder class="org.springframework.boot.logging.logback.StructuredLogEncoder"> <format>${CONSOLE_LOG_STRUCTURED_FORMAT}</format> <charset>${CONSOLE_LOG_CHARSET}</charset> </encoder>
stacktrace
この機能は3.4.6だと動かなかったんで3.5.0にしたら使えた。
logging.structured.json.stacktrace.root
なんか上手くいかない。https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/StandardStackTracePrinter.java#L103-L109 を見る限りだと java.lang.NullPointerException: null message 的なのを先に出すか後に出すか、だと思うのだけど。
logging.structured.json.stacktrace.max-length
以下は100の指定例。
"java.lang.NullPointerException: null message. at org.example.app.App.ho(App.java:30) at java...."
logging.structured.json.stacktrace.max-throwable-depth
以下は2の指定例。
"java.lang.NullPointerException: null message. at org.example.app.App.ho(App.java:30) at java.base\/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ... 47 filtered"
logging.structured.json.stacktrace.include-common-frames
ちゃんと確認してないけど、たぶん... 2 moreってなるヤツを含めるか削るか……だと思う。
logging.structured.json.stacktrace.include-hashes
stacktraceにhash値を含める。算出アルゴリズムまでは確認してないけど、同じ内容の例外は同じhashになるので、頻出例外を追跡したい場合とかに使うらしい。アイデア的には https://qiita.com/kawachi/items/103ed6798818bb271d52 と同様と思われる。
"<#51a27bee> java.lang.NullPointerException: null message.
項目の追加・削除・リネーム
デフォルトに少々手を加える程度であればプロパティで制御する。以下は、level_valueを削除・thread_nameをthにリネーム・formatを追加、している。
logging.structured.json.exclude=level_value
logging.structured.json.rename.thread_name=th
logging.structured.json.add.format=${logging.structured.format.console}
独自フォーマット
MDCやFluent Logging APIで項目追加を出来るが、それに加えて、StructuredLoggingJsonMembersCustomizerという手段もある。
import org.springframework.boot.json.JsonWriter.Members; import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer; public class MyStructuredLoggingJsonMembersCustomizer implements StructuredLoggingJsonMembersCustomizer<String> { @Override public void customize(Members<String> members) { members.add("custom-key", "aaaa"); } }
interfaceの実装クラスを以下のように指定する。
logging.structured.json.customizer=org.example.app.MyStructuredLoggingJsonMembersCustomizer
https://docs.spring.io/spring-boot/api/java/org/springframework/boot/logging/structured/StructuredLoggingJsonMembersCustomizer.html によると、コンストラクタでEnvironmentで受け取れるし、logbackであればThrowableProxyConverterも使用可能。
import ch.qos.logback.classic.pattern.ThrowableProxyConverter; import ch.qos.logback.classic.spi.ILoggingEvent; import org.springframework.boot.json.JsonWriter.Members; import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer; import org.springframework.core.env.Environment; public class MyStructuredLoggingJsonMembersCustomizer implements StructuredLoggingJsonMembersCustomizer<ILoggingEvent> { final Environment env; final ThrowableProxyConverter converter; public MyStructuredLoggingJsonMembersCustomizer(Environment env, ThrowableProxyConverter converter) { this.env = env; this.converter = converter; } @Override public void customize(Members<ILoggingEvent> members) { members.add("my-stacktrace", event -> { String myStacktrace = converter.convert(event); return myStacktrace; }); } }
上記例の通りstacktraceのカスタマイズが出来る。各種プロパティではどうしても実現が難しいログ出力の場合はこの方法が候補になると思われる。
また、最初の方に出てきた通り、独自フォーマットにはStructuredLogFormatter<ILoggingEvent>実装を用意する方法もある。spring-bootの定義済み実装が参考になると思われる。たとえばlogstash用の https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogstashStructuredLogFormatter.java を見るとJsonWriterにあれこれ設定をしており、
class LogstashStructuredLogFormatter extends JsonWriterStructuredLogFormatter<ILoggingEvent> { // 省略 private static void jsonMembers(ThrowableProxyConverter throwableProxyConverter, JsonWriter.Members<ILoggingEvent> members) { members.add("@timestamp", ILoggingEvent::getInstant).as(LogstashStructuredLogFormatter::asTimestamp); members.add("@version", "1"); members.add("message", ILoggingEvent::getFormattedMessage);
JsonWriterStructuredLogFormatterはStructuredLogFormatterをimplementsして上のjsonWriterで単に出力しているだけ。
public abstract class JsonWriterStructuredLogFormatter<E> implements StructuredLogFormatter<E> { // (略) @Override public String format(E event) { return this.jsonWriter.writeToString(event);
ただまぁここまで根本から変更するとなるとecs, gelf, logstash の事前定義とは一体何だったのか、という話ではある。
感想とか
サーバにログインしてログファイルをtailしてた時代から、kibanaとかで複数コンポーネントのログをまとめて見る時代になったんすねぇ、という感想がある。ただ、ログをどのように出力するかは簡単になったが、何を出力するか、の難しさはアンマリ変わってないかもなぁ……という感想もある。