今回はFluent Bitを少し離れてSpringBootのログ関連のはなし。
前回「アプリ達が構造的なデータ(ようするにJSONデータ)をログに出力する」ことで、Fluent Bitがその値を見ながら転送先を振り分ける、なんて事を見てきましたが、今回はそのアプリがJSONログを出力する方法についてです。
前提知識
- いままでの記事をひととおり見ている方
- JavaのLoggerの仕組み、とくにLogback等をある程度知っている方
今回のソース
$ git clone --branch fluentbit03 https://github.com/masatomix/spring-boot-sample-tomcat.git
$
でダウンロード出来ます。セットアップなどはいままでの記事と同じなので割愛します。
まず、 JSON 形式のログはどのように出力していたか
前回ですでにJSON形式のログを出力していましたが、logstash-logback-encoderを使っていました。ソースを見てもらうと分かりますが、下記の通りLoggerの Appender に、LogstashEncoderというEncoderをかましてあります。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
... 割愛
<!--production環境用設定 -->
<springProfile name="production">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- <includeMdc>false</includeMdc> MDCを無視 -->
</encoder>
</appender>
<appender name="STDOUT_FW" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
</encoder>
</appender>
</springProfile>
<!--FWのLogger-->
<logger name="nu.mine.kino.advice" additivity="false">
<appender-ref ref="STDOUT_FW"/>
</logger>
<logger name="nu.mine.kino.interceptor" additivity="false">
<appender-ref ref="STDOUT_FW"/>
</logger>
<!--その他のLogger-->
<root>
<appender-ref ref="STDOUT" />
</root>
</configuration>
基本はこれだけで、SpringBootアプリケーションが出力するログを、JSON形式にすることができます1。
シンプルですね。
MDC機構で、任意のプロパティを出力する
さてMDC (Mapped Diagnostic Context) 機構です。MDC機構はKey/Value形式でログに出力したいデータを保持できる機構です。MDC自体は、JSONデータを出力する時以外にも利用可能なのですが、LogstashEncoder を使っていると自動的にKey/Value形式のデータがJSONにプロパティとして追加されるようになっています。
今回のコードだとnu.mine.kino.advice.GlobalExceptionHandler とかの
@ExceptionHandler(ClientException.class)
public ResponseEntity<ErrorResponse> handleClientException(ClientException exception) {
var body = new ErrorResponse("BAD_REQUEST", exception.getMessage());
try {
MDC.put(MDCKey.APP_TYPE.key(), "FW"); // MDCKey.APP_TYPE.key(),はただの「appType」という文字列
log.warn(exception.getMessage(), exception);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
} finally {
MDC.remove(MDCKey.APP_TYPE.key());
}
}
この辺のコードがMDCを利用したソースです。上記の処理は、
- アプリが ClientException という業務例外をスローしたときに自動的に呼ばれ
- BAD_REQUEST(400番) なHTTP Status に変換して返す
- その際にWARNなレベルのログを出力する
というものですが、このログ出力の前に MDC.put("appType", "FW") と値を設定することで、ログに
{
"@timestamp": "2025-09-15T13:20:55.614663441Z",
"@version": "1",
"message": "クライアント起因の例外が発生しました",
"logger_name": "nu.mine.kino.advice.GlobalExceptionHandler",
"thread_name": "http-nio-8080-exec-1",
"level": "WARN",
"level_value": 30000,
"stack_trace": "省略",
"requestId": "4834acd0-e7b7-4ae5-a700-3dee241d03a3",
"appType": "FW"
}
と、個別のKey/Value値「appType=FW」が出力されるようになります。
細かいはなし
MDC機構はスレッドごとに個別の空間をもつ仕組みとなっています2。また、SpringBootなどのWebアプリ(Tomcat) はリクエストごとにスレッドを起動して処理をしますが、スレッドは使い回される(スレッドプールという) 可能性があるため、毎回MDCをクリアしておかないといけないようです。ということでfinally節にてMDCにセットしたプロパティのremoveを行っています。
SpringBootのWebアプリケーションにおいては、このように必要なプロパティをMDCに設定する事で、JSONデータに任意のプロパティを設定する事ができました。
一応、実際にやってみる。
今回はシンプルに、Dockerなどを使わず SpringBootを直接起動しています(特に深い意味はありません)。
$ cd spring-boot-sample-tomcat/
$ mvn spring-boot:run -Dspring-boot.run.profiles=production
// プロファイルがproductionのときだけLogstashEncoderが有効になるようにしてあるので、プロファイルを指定
...
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.2)
...
起動したのでさきほどの処理が呼ばれるリクエストを呼んでみます。
$ curl "http://localhost:8080/clientException" -i
HTTP/1.1 400
X-Request-Id: 6791e888-7a8c-4997-9924-67c3676eb51c
vary: accept-encoding
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 15 Sep 2025 13:36:38 GMT
Connection: close
{"code":"BAD_REQUEST","message":"クライアント起因の例外が発生しました"}
$
呼び元に400番の業務エラーが返されましたね。さてサーバログは...
{
"@timestamp": "2025-09-15T13:36:38.082494351Z",
"@version": "1",
"message": "クライアント起因の例外が発生しました",
"logger_name": "nu.mine.kino.advice.GlobalExceptionHandler",
"thread_name": "http-nio-8080-exec-1",
"level": "WARN",
"level_value": 30000,
"stack_trace": "略",
"requestId": "6791e888-7a8c-4997-9924-67c3676eb51c",
"appType": "FW" ← 出てる!
}
{
"@timestamp": "2025-09-15T13:36:38.11354701Z",
"@version": "1",
"message": "FWログ出力",
"logger_name": "nu.mine.kino.interceptor.MDCLoggingInterceptor",
"thread_name": "http-nio-8080-exec-1",
"level": "INFO",
"level_value": 20000,
"logType": "request_response",
"requestId": "6791e888-7a8c-4997-9924-67c3676eb51c",
"appType": "FW",
"request": {
"uri": "/clientException",
"method": "GET"
},
"response": {
"duration": "35",
"status": "400"
}
}
確かに出力されました。別のログも出ていますが、またの機会に。
まとめ
今回は短いけどここまで。本当はいろいろ書きたくて
- アプリチームが投げる例外について、フレームワークチームがExceptionHandlerを使って、適切なHTTPのResponse Statusに載せ替えて返す
- アプリチームは 業務例外を投げさえすればOK。FW側が例外を判定して所定のステータスのエラーを返却することができます。
- ログ出力もFWにお任せすることで、業務例外はWARNなどのログ記録だけをしてもらい、システム例外はCloudWatch Alarmに検知してもらう、などが可能です。
- アプリでは自由にログ出力できるようにしつつ、Request/Responseのログは常にFWが別の場所へ出力してあげる。
- 上記の2行目のログのように logTypeというログの種類を判別するプロパティを追加したり
- requestId というリクエストにユニークな値を出力したり
- Request/Response のパラメタをログに出力したり、できます。
- (さらには上記のRequest/Responseのようにネストされたデータの場合はMDCじゃない別の機構を用いる)
- それらの処理を実現するための SpringのInterceptor機能
などなどをやりたいのですが、量が膨大になりすぎるので今回はここまでにして、上記のコンテンツは次回以降にしたいと思います。
今回の内容をまとめると
- SpringBootアプリのログ出力について
logstash-logback-encoderを使うと簡単にJSON形式のログ出力ができました。 - Logの設定は、SpringBoot起動時のプロファイルを用いて切り替えることができました。本番と開発でログ出力形式を変更できるってことですね。
- MDC機構にて、Key/Value形式の情報をセットして、それをログに埋め込むことができることを学びました。
などなどです。お疲れさまでした!