diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 61fbefd9152..e1bee1c8eba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -99,6 +99,7 @@ androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version androidx-browser = { module = "androidx.browser:browser", version = "1.8.0" } async-profiler = { module = "tools.profiler:async-profiler", version.ref = "asyncProfiler" } async-profiler-jfr-converter = { module = "tools.profiler:jfr-converter", version.ref = "asyncProfiler" } +caffeine = { module = "com.github.ben-manes.caffeine:caffeine" } coil-compose = { module = "io.coil-kt:coil-compose", version = "2.6.0" } commons-compress = {module = "org.apache.commons:commons-compress", version = "1.25.0"} context-propagation = { module = "io.micrometer:context-propagation", version = "1.1.0" } @@ -193,6 +194,7 @@ springboot4-starter-restclient = { module = "org.springframework.boot:spring-boo springboot4-starter-webclient = { module = "org.springframework.boot:spring-boot-starter-webclient", version.ref = "springboot4" } springboot4-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot4" } springboot4-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot4" } +springboot4-starter-cache = { module = "org.springframework.boot:spring-boot-starter-cache", version.ref = "springboot4" } timber = { module = "com.jakewharton.timber:timber", version = "4.7.1" } # Animalsniffer signature diff --git a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts index a7fa57dac83..f43cc47cc6d 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts @@ -57,6 +57,10 @@ dependencies { implementation(projects.sentryQuartz) implementation(projects.sentryAsyncProfiler) + // cache tracing + implementation(libs.springboot4.starter.cache) + implementation(libs.caffeine) + // database query tracing implementation(projects.sentryJdbc) runtimeOnly(libs.hsqldb) diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CacheController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CacheController.java new file mode 100644 index 00000000000..3c2e66442de --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CacheController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot4; + +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/cache/") +public class CacheController { + private final TodoService todoService; + + public CacheController(TodoService todoService) { + this.todoService = todoService; + } + + @GetMapping("{id}") + Todo get(@PathVariable Long id) { + return todoService.get(id); + } + + @PostMapping + Todo save(@RequestBody Todo todo) { + return todoService.save(todo); + } + + @DeleteMapping("{id}") + void delete(@PathVariable Long id) { + todoService.delete(id); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java index 71463a9a819..13d97fa8442 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java @@ -9,6 +9,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.restclient.RestTemplateBuilder; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.quartz.CronTriggerFactoryBean; @@ -19,6 +20,7 @@ import org.springframework.web.reactive.function.client.WebClient; @SpringBootApplication +@EnableCaching @EnableScheduling public class SentryDemoApplication { public static void main(String[] args) { diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoService.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoService.java new file mode 100644 index 00000000000..c837ab8398a --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoService.java @@ -0,0 +1,29 @@ +package io.sentry.samples.spring.boot4; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +@Service +public class TodoService { + private final Map store = new ConcurrentHashMap<>(); + + @Cacheable(value = "todos", key = "#id") + public Todo get(Long id) { + return store.get(id); + } + + @CachePut(value = "todos", key = "#todo.id") + public Todo save(Todo todo) { + store.put(todo.getId(), todo); + return todo; + } + + @CacheEvict(value = "todos", key = "#id") + public void delete(Long id) { + store.remove(id); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties index 9ba7a54aaf8..8198059343a 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties @@ -20,6 +20,9 @@ sentry.logs.enabled=true sentry.profile-session-sample-rate=1.0 sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces sentry.profile-lifecycle=TRACE +sentry.enable-cache-tracing=true +spring.cache.cache-names=todos +spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s # Uncomment and set to true to enable aot compatibility # This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt new file mode 100644 index 00000000000..7853750a8fb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -0,0 +1,51 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class CacheSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `cache put and get produce spans`() { + val restClient = testHelper.restClient + + // Save a todo (triggers @CachePut -> cache.put span) + val todo = Todo(1L, "test-todo", false) + restClient.saveCachedTodo(todo) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, _ -> + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.put") + } + + testHelper.reset() + + // Get the todo (triggers @Cacheable -> cache.get span, should be a hit) + restClient.getCachedTodo(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, _ -> + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.get") + } + } + + @Test + fun `cache evict produces span`() { + val restClient = testHelper.restClient + + restClient.deleteCachedTodo(1L) + + testHelper.ensureTransactionReceived { transaction, _ -> + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.remove") + } + } +} diff --git a/sentry-system-test-support/api/sentry-system-test-support.api b/sentry-system-test-support/api/sentry-system-test-support.api index 51ef7da55d9..83a9f288d0c 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -548,7 +548,9 @@ public final class io/sentry/systemtest/util/RestTestClient : io/sentry/systemte public static synthetic fun createPerson$default (Lio/sentry/systemtest/util/RestTestClient;Lio/sentry/systemtest/Person;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; public final fun createPersonDistributedTracing (Lio/sentry/systemtest/Person;Ljava/util/Map;)Lio/sentry/systemtest/Person; public static synthetic fun createPersonDistributedTracing$default (Lio/sentry/systemtest/util/RestTestClient;Lio/sentry/systemtest/Person;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; + public final fun deleteCachedTodo (J)V public final fun errorWithFeatureFlag (Ljava/lang/String;)Ljava/lang/String; + public final fun getCachedTodo (J)Lio/sentry/systemtest/Todo; public final fun getCountMetric ()Ljava/lang/String; public final fun getDistributionMetric (J)Ljava/lang/String; public final fun getGaugeMetric (J)Ljava/lang/String; @@ -558,6 +560,7 @@ public final class io/sentry/systemtest/util/RestTestClient : io/sentry/systemte public final fun getTodo (J)Lio/sentry/systemtest/Todo; public final fun getTodoRestClient (J)Lio/sentry/systemtest/Todo; public final fun getTodoWebclient (J)Lio/sentry/systemtest/Todo; + public final fun saveCachedTodo (Lio/sentry/systemtest/Todo;)Lio/sentry/systemtest/Todo; } public final class io/sentry/systemtest/util/SentryMockServerClient : io/sentry/systemtest/util/LoggingInsecureRestClient { diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt index bdaa2333f21..da552ff93bc 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt @@ -50,6 +50,24 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl return callTyped(request, true) } + fun getCachedTodo(id: Long): Todo? { + val request = Request.Builder().url("$backendBaseUrl/cache/$id") + + return callTyped(request, true) + } + + fun saveCachedTodo(todo: Todo): Todo? { + val request = Request.Builder().url("$backendBaseUrl/cache/").post(toRequestBody(todo)) + + return callTyped(request, true) + } + + fun deleteCachedTodo(id: Long) { + val request = Request.Builder().url("$backendBaseUrl/cache/$id").delete() + + call(request, true) + } + fun checkFeatureFlag(flagKey: String): FeatureFlagResponse? { val request = Request.Builder().url("$backendBaseUrl/feature-flag/check/$flagKey")