Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
- New APIs are `Sentry.setAttribute`, `Sentry.setAttributes`, `Sentry.removeAttribute`
- Support collections and arrays in attribute type inference ([#5124](https://github.com/getsentry/sentry-java/pull/5124))
- Add support for `SENTRY_SAMPLE_RATE` environment variable / `sample-rate` property ([#5112](https://github.com/getsentry/sentry-java/pull/5112))
- Add cache tracing instrumentation for Spring Boot 4 ([#5172](https://github.com/getsentry/sentry-java/pull/5172), [#5173](https://github.com/getsentry/sentry-java/pull/5173), [#5174](https://github.com/getsentry/sentry-java/pull/5174))
- 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 @@ -231,6 +232,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 @@ -284,6 +286,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 @@ -1179,6 +1182,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