以下の内容はhttps://kagamihoge.hatenablog.com/entry/2025/06/03/165344より取得しました。


spring-boot 3.4のStructured logging対応

チュートリアル相当

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よりも前ではdependencylogstash-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] は三種類が定義済み。

リファレンスにある通り、プロパティを通して変更可能な項目は各種フォーマットごとに異なる。また、試した限りでは、logstashだけMarkertagsプロパティとして出力する。他にも細々とした違いがあるのだと思われる。

既存のログ出力設定との連携

既存のログ出力設定、たとえば 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_namethにリネーム・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);

JsonWriterStructuredLogFormatterStructuredLogFormatterを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とかで複数コンポーネントのログをまとめて見る時代になったんすねぇ、という感想がある。ただ、ログをどのように出力するかは簡単になったが、何を出力するか、の難しさはアンマリ変わってないかもなぁ……という感想もある。

参考資料




以上の内容はhttps://kagamihoge.hatenablog.com/entry/2025/06/03/165344より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14