こちらはJava Advent Calendar 2024の8日目です。昨日はkumazoさんの「Java Foreign Function & Memory API で WebGPU を叩く」でした。
Spring Boot 3.4で構造化ロギングのサポートが入りました。以前は構造化ログにするためにはecs-logging-javaやlogstash-logback-encoderを使う必要がありましたが、とうとうSpring Bootの依存のみで可能になったわけです。
Springレベル3くらいのプログラマとして早速試してみます。
なおリファレンスはこちら。
docs.spring.io
ロギングフォーマット
構造化ログフォーマットは以下の3つがサポートされています。
- Elastic Common Schema (ECS)
- Graylog Extended Log Format (GELF)
- Logstash
ECSはElasticがログ解析用に策定したJSON仕様ですね。Logstashはlogstash-logback-encoderのフォーマットのようですが、 @timestamp などECSと一部共通している部分もあります。
まずはSpring BootデフォルトのLogbackで試してみます。
構造化ロギングを有効化するにはプロパティの logging.structured.format.console を変更します。
logging:
structured:
format:
console: ecs
ログ出力のJavaコードは特別な設定を必要としませんが、せっかくなのでSLF4J 2.0から追加されたFluent APIを使い、独自定義のフィールドに値を出力してみます。
package com.example.springmvcexample;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(path = "/greeting")
public class GreetingController {
final AtomicInteger counter = new AtomicInteger();
private static final Logger log = LoggerFactory.getLogger(GreetingController.class);
@GetMapping
public String greet(@RequestParam String name) {
int count = counter.getAndIncrement();
log.atInfo()
.addKeyValue("greeting.count", count)
.log("this is slf4j logger");
return "Hello " + name + "!";
}
}
これを実行すると以下のJSONが出力されます。(見やすいように改行を入れています)
{
"@timestamp": "2024-12-08T04:07:40.882835Z",
"log.level": "INFO",
"process.pid": 37694,
"process.thread.name": "http-nio-8080-exec-1",
"service.name": "spring-mvc-example",
"log.logger": "com.example.springmvcexample.GreetingController",
"message": "this is slf4j logger",
"greeting.count": 0,
"ecs.version": "8.11"
}
@timestamp, message, greeting.count 以外にも各メタデータが自動的に追加されているのが分かります。
SLF4J + Log4j2 (は上手くいかない)
Spring BootはLogback以外にもLog4j2をサポートしています。Log4j2を入れるにはpom.xmlを以下のように編集します。
<dependencies>
...
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-log4j2</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-logging</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
</dependencies>
コードは先程と同じくSLF4Jを使います。再度実行してみると…
{
"@timestamp": "2024-12-08T04:14:55.193894Z",
"log.level": "INFO",
"process.pid": 37864,
"process.thread.name": "http-nio-8080-exec-1",
"service.name": "spring-mvc-example",
"log.logger": "com.example.springmvcexample.GreetingController",
"message": "this is slf4j logger",
"greeting.count": "0",
"ecs.version": "8.11"
}
一見問題ないように見えますが、greeting.countがnumberではなくstringになってしまっています。例えばElasticsearchでこのフィールドのsumを取りたい場合に、フィールドのtypeがstringだと集計できないという問題が発生してしまいます。
SLF4JがLog4j2にブリッジする際にaddKeyValue(key, value)で追加された値をLog4j2のThreadContextMapに入れているのですが、ThreadContextMapがKey-ValueをString-Stringで保持しているため元のvalueがIntegerなどでもtoStringされてしまうのが原因で、この問題が発生しているようです。(Issueもありますが、特に動きはない状況です。)
ではSLF4Jを使わずLog4j2のAPIを直接呼び出した場合はどうでしょうか。StringMapMessageを使って同様のログを出してみます。
package com.example.springmvcexample;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.message.StringMapMessage;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(path = "/greeting")
public class GreetingController {
final AtomicInteger counter = new AtomicInteger();
private static final org.apache.logging.log4j.Logger log4jLogger = LogManager.getLogger();
@GetMapping
public String greet(@RequestParam String name) {
int count = counter.getAndIncrement();
log4jLogger.info(new StringMapMessage()
.with("message", "this is log4j logger")
.with("greeting.count", count));
return "Hello " + name + "!";
}
}
今度は次のJSONが出力されました。message フィールドが、StringMapMessageをJSONシリアライズしたオブジェクトになってしまっています。
{
"@timestamp": "2024-12-08T04:27:56.847793Z",
"log.level": "INFO",
"process.pid": 37970,
"process.thread.name": "http-nio-8080-exec-1",
"service.name": "spring-mvc-example",
"log.logger": "com.example.springmvcexample.GreetingController",
"message": {
"greeting.count": 0,
"message": "this is log4j logger"
},
"ecs.version": "8.11"
}
同じコードでも ecs-logging-javaのlog4j2-ecs-layoutを使うと問題なく出力されるのですが、Spring Bootだとまだそこまではサポートされていないようです。
まとめ
これから新規で作るSpring Bootプロジェクトは、依存ライブラリを減らすために構造化ロギングサポートを使ったほうが良いです。一方で、すでに3rd-partyライブラリを使って構造化ロギングを実現している場合は、上記のLog4j2のような非互換性の問題が出てくる可能性があるため、無理に乗り換える必要は無いと思います。
明日は@Telethaさんの記事です。それでは良い年末を!