Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions sentry-spring-7/api/sentry-spring-7.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()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 <init> (Lorg/springframework/cache/CacheManager;Lio/sentry/IScopes;)V
public fun getCache (Ljava/lang/String;)Lorg/springframework/cache/Cache;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<CacheManager>()
val processor = SentryCacheBeanPostProcessor()

val result = processor.postProcessAfterInitialization(cacheManager, "cacheManager")

assertTrue(result is SentryCacheManagerWrapper)
}

@Test
fun `does not double-wrap SentryCacheManagerWrapper`() {
val delegate = mock<CacheManager>()
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
Loading