Spring Boot + KotlinでSentryことはじめ

SpringBoot + Kotlinで構成されたアプリケーションにSentryを導入する検証を行ったためメモがてら知見を残します。
2023.05.24

prismatix事業部の太田です。

Spring Boot + Kotlinで構成されたアプリケーションにSentryを導入する検証を行ったため、メモがてら知見を残します。

バージョンなど

  • Spring Boot 3.1.0
  • Kotlin 1.8.21
  • JDK OpenJDK Runtime Environment Corretto-17.0.7.7.1 (build 17.0.7+7-LTS)

導入

専用のSDKが用意されているので導入します。

また、logging統合のライブラリを追加すると、ログ出力した内容もSentryに送信する設定を自動で行ってくれます。

build.gradle

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	// Sentry SDK 2系の場合は別のライブラリなので注意
	implementation("io.sentry:sentry-spring-boot-starter-jakarta:6.19.1")
        // logback統合
	implementation("io.sentry:sentry-logback:6.19.1")
	
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Sentryの管理画面から取得したdsnを設定します。

build.gradle

sentry:
  dsn: ${SENTRY_DSN}

これで送信する準備が整いました。

ExceptionHandlerで捕捉される例外についてもSentryに送信する

上記の設定では、unhandled exception、及びlogbackで出力されたerrorログの内容がSentryに送信されます。 一方で、Spring BootでWebアプリケーションを実装している場合、@ControllerAdvice@ExceptionHandlerを用いた例外ハンドリングを行っていることが多いのではないでしょうか。

以下の設定を追加することで、@ExceptionHandlerによって捕捉される例外についてもSentryに送信することができます。

build.gradle

sentry:
  dsn: ${SENTRY_DSN}
  sentry.exception-resolver-order=-2147483647 // the value of org.springframework.core.Ordered#HIGHEST_PRECEDENCE

Sentryが実装しているHandlerExceptionResolverの優先度を高めることで、ハンドリング順をコントロールしているようです。

実行サンプル

以下のように、@ExceptionHandlerで例外をエラーログ出力に変換した場合と、そのままSpringの領域にthrowした場合のコードで実験してみました。

SampleController.kt

package com.example.demo

import org.slf4j.LoggerFactory
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.GetMapping

@Controller
class SampleController {

     @GetMapping("/sample/handled")
     fun sampleHandled() {
         throw HandledRuntimeException("handled exception sample")
     }

     @GetMapping("/sample/unhandled")
     fun sampleUnhandled() {
         throw UnhandledRuntimeException("unhandled exception sample")
     }
}

@ControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(HandledRuntimeException::class)
    fun handleRuntimeException(e: HandledRuntimeException) {
        LoggerFactory.getLogger(this::class.java).error("exception handler sample")
    }
}

class HandledRuntimeException(message: String) : RuntimeException(message)

class UnhandledRuntimeException(message: String) : RuntimeException(message)

結果

以下のように、エラーログについてはハンドリングされる順番に関わらずSentryへ送信されましたが、例外についてはハンドリングされる順番によって結果が変化しました。

sentry.exception-resolver-order error log handled exception unhandled exception
最優先(-2147483647) 送信される 送信される 送信される
設定なし 送信される 送信されない 送信される

最後に

この記事ではSpring Bootのよくある実装パターンである@ExceptionHandlerとSentryの関係性について見てきました。

エラー監視は便利ですが、大量に出力されると運用が辛いため、想定外のエラーだけ検知されるようにうまく量をコントロールできる設定にできると良いですね。

参考

https://docs.sentry.io/platforms/java/guides/spring-boot/

https://stackoverflow.com/questions/48401974/how-to-have-my-own-error-handlers-before-sentry-in-a-spring-application