diff --git a/CHANGELOG.md b/CHANGELOG.md index dba0f4dcdf..f090f50f70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ### Features +- Add cache tracing instrumentation for Spring Boot 4 ([#5137](https://github.com/getsentry/sentry-java/pull/5137), [#5141](https://github.com/getsentry/sentry-java/pull/5141), [#5142](https://github.com/getsentry/sentry-java/pull/5142)) + - Wraps Spring `CacheManager` and `Cache` beans to produce `cache.get`, `cache.put`, `cache.remove`, and `cache.flush` spans + - Enable via `sentry.enable-cache-tracing=true` + - Create `sentry-opentelemetry-otlp` and `sentry-opentelemetry-otlp-spring` modules for combining OpenTelemetry SDK OTLP export with Sentry SDK ([#5100](https://github.com/getsentry/sentry-java/pull/5100)) - OpenTelemetry is configured to send spans to Sentry directly using an OTLP endpoint. - Sentry only uses trace and span ID from OpenTelemetry (via `OpenTelemetryOtlpEventProcessor`) but will not send spans through OpenTelemetry nor use OpenTelemetry `Context` for `Scopes` propagation. diff --git a/sentry-spring-7/api/sentry-spring-7.api b/sentry-spring-7/api/sentry-spring-7.api index 41514d9555..b37747cf76 100644 --- a/sentry-spring-7/api/sentry-spring-7.api +++ b/sentry-spring-7/api/sentry-spring-7.api @@ -104,6 +104,12 @@ public final class io/sentry/spring7/SpringSecuritySentryUserProvider : io/sentr public fun provideUser ()Lio/sentry/protocol/User; } +public final class io/sentry/spring7/cache/SentryCacheBeanPostProcessor : org/springframework/beans/factory/config/BeanPostProcessor, org/springframework/core/PriorityOrdered { + public fun ()V + public fun getOrder ()I + public fun postProcessAfterInitialization (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; +} + public final class io/sentry/spring7/cache/SentryCacheManagerWrapper : org/springframework/cache/CacheManager { public fun (Lorg/springframework/cache/CacheManager;Lio/sentry/IScopes;)V public fun getCache (Ljava/lang/String;)Lorg/springframework/cache/Cache; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheBeanPostProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheBeanPostProcessor.java new file mode 100644 index 0000000000..b6569a9953 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheBeanPostProcessor.java @@ -0,0 +1,29 @@ +package io.sentry.spring7.cache; + +import io.sentry.ScopesAdapter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cache.CacheManager; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; + +/** Wraps {@link CacheManager} beans in {@link SentryCacheManagerWrapper} for instrumentation. */ +@ApiStatus.Internal +public final class SentryCacheBeanPostProcessor implements BeanPostProcessor, PriorityOrdered { + + @Override + public @NotNull Object postProcessAfterInitialization( + final @NotNull Object bean, final @NotNull String beanName) throws BeansException { + if (bean instanceof CacheManager && !(bean instanceof SentryCacheManagerWrapper)) { + return new SentryCacheManagerWrapper((CacheManager) bean, ScopesAdapter.getInstance()); + } + return bean; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheBeanPostProcessorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheBeanPostProcessorTest.kt new file mode 100644 index 0000000000..54c5e696d6 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheBeanPostProcessorTest.kt @@ -0,0 +1,44 @@ +package io.sentry.spring7.cache + +import io.sentry.IScopes +import kotlin.test.Test +import kotlin.test.assertSame +import kotlin.test.assertTrue +import org.mockito.kotlin.mock +import org.springframework.cache.CacheManager + +class SentryCacheBeanPostProcessorTest { + + private val scopes: IScopes = mock() + + @Test + fun `wraps CacheManager beans in SentryCacheManagerWrapper`() { + val cacheManager = mock() + val processor = SentryCacheBeanPostProcessor() + + val result = processor.postProcessAfterInitialization(cacheManager, "cacheManager") + + assertTrue(result is SentryCacheManagerWrapper) + } + + @Test + fun `does not double-wrap SentryCacheManagerWrapper`() { + val delegate = mock() + val alreadyWrapped = SentryCacheManagerWrapper(delegate, scopes) + val processor = SentryCacheBeanPostProcessor() + + val result = processor.postProcessAfterInitialization(alreadyWrapped, "cacheManager") + + assertSame(alreadyWrapped, result) + } + + @Test + fun `does not wrap non-CacheManager beans`() { + val someBean = "not a cache manager" + val processor = SentryCacheBeanPostProcessor() + + val result = processor.postProcessAfterInitialization(someBean, "someBean") + + assertSame(someBean, result) + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java index 1b804e8cb8..ae9e3ac50f 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java @@ -25,6 +25,7 @@ import io.sentry.spring7.SentryWebConfiguration; import io.sentry.spring7.SpringProfilesEventProcessor; import io.sentry.spring7.SpringSecuritySentryUserProvider; +import io.sentry.spring7.cache.SentryCacheBeanPostProcessor; import io.sentry.spring7.checkin.SentryCheckInAdviceConfiguration; import io.sentry.spring7.checkin.SentryCheckInPointcutConfiguration; import io.sentry.spring7.checkin.SentryQuartzConfiguration; @@ -65,6 +66,7 @@ import org.springframework.boot.restclient.autoconfigure.RestTemplateAutoConfiguration; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; +import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -229,6 +231,19 @@ static class Graphql22Configuration {} }) static class QuartzConfiguration {} + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(CacheManager.class) + @ConditionalOnProperty(name = "sentry.enable-cache-tracing", havingValue = "true") + @Open + static class SentryCacheConfiguration { + + @Bean + public static @NotNull SentryCacheBeanPostProcessor sentryCacheBeanPostProcessor() { + SentryIntegrationPackageStorage.getInstance().addIntegration("SpringCache"); + return new SentryCacheBeanPostProcessor(); + } + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ProceedingJoinPoint.class) @ConditionalOnProperty( diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt index 67200c2cd6..7107a6d261 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt @@ -38,6 +38,7 @@ import io.sentry.spring7.SentryUserFilter import io.sentry.spring7.SentryUserProvider import io.sentry.spring7.SpringProfilesEventProcessor import io.sentry.spring7.SpringSecuritySentryUserProvider +import io.sentry.spring7.cache.SentryCacheBeanPostProcessor import io.sentry.spring7.tracing.SentryTracingFilter import io.sentry.spring7.tracing.SpringServletTransactionNameProvider import io.sentry.spring7.tracing.TransactionNameProvider @@ -199,6 +200,7 @@ class SentryAutoConfigurationTest { "sentry.ignored-transactions=transactionName1,transactionNameB", "sentry.enable-backpressure-handling=false", "sentry.enable-database-transaction-tracing=true", + "sentry.enable-cache-tracing=true", "sentry.enable-spotlight=true", "sentry.spotlight-connection-url=http://local.sentry.io:1234", "sentry.force-init=true", @@ -252,6 +254,7 @@ class SentryAutoConfigurationTest { .containsOnly(FilterString("transactionName1"), FilterString("transactionNameB")) assertThat(options.isEnableBackpressureHandling).isEqualTo(false) assertThat(options.isEnableDatabaseTransactionTracing).isEqualTo(true) + assertThat(options.isEnableCacheTracing).isEqualTo(true) assertThat(options.isForceInit).isEqualTo(true) assertThat(options.isGlobalHubMode).isEqualTo(true) assertThat(options.isCaptureOpenTelemetryEvents).isEqualTo(true) @@ -1163,6 +1166,33 @@ class SentryAutoConfigurationTest { } } + @Test + fun `SentryCacheBeanPostProcessor is registered when enable-cache-tracing is true`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.enable-cache-tracing=true", + ) + .run { assertThat(it).hasSingleBean(SentryCacheBeanPostProcessor::class.java) } + } + + @Test + fun `SentryCacheBeanPostProcessor is not registered when enable-cache-tracing is missing`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it).doesNotHaveBean(SentryCacheBeanPostProcessor::class.java) + } + } + + @Test + fun `SentryCacheBeanPostProcessor is not registered when enable-cache-tracing is false`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.enable-cache-tracing=false", + ) + .run { assertThat(it).doesNotHaveBean(SentryCacheBeanPostProcessor::class.java) } + } + @Configuration(proxyBeanMethods = false) open class CustomSchedulerFactoryBeanCustomizerConfiguration { class MyJobListener : JobListener {