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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Features

- 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))
- Add cache tracing instrumentation for Spring Boot 2, 3, and 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), [#5190](https://github.com/getsentry/sentry-java/pull/5190), [#5191](https://github.com/getsentry/sentry-java/pull/5191))
- Wraps Spring `CacheManager` and `Cache` beans to produce `cache.get`, `cache.put`, `cache.remove`, and `cache.flush` spans
- Set `sentry.enable-cache-tracing` to `true` to enable this feature
- Add JCache (JSR-107) cache tracing via new `sentry-jcache` module ([#5179](https://github.com/getsentry/sentry-java/pull/5179))
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ springboot-starter-aop = { module = "org.springframework.boot:spring-boot-starte
springboot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "springboot2" }
springboot-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot2" }
springboot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot2" }
springboot-starter-cache = { module = "org.springframework.boot:spring-boot-starter-cache", version.ref = "springboot2" }
springboot3-otel = { module = "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter", version.ref = "otelInstrumentation" }
springboot3-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springboot3" }
springboot3-starter-graphql = { module = "org.springframework.boot:spring-boot-starter-graphql", version.ref = "springboot3" }
Expand Down
2 changes: 2 additions & 0 deletions sentry-samples/sentry-samples-spring-boot/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ dependencies {
implementation(libs.springboot.starter.security)
implementation(libs.springboot.starter.web)
implementation(libs.springboot.starter.webflux)
implementation(libs.springboot.starter.cache)
implementation(libs.springboot.starter.websocket)
implementation(libs.caffeine)
implementation(Config.Libs.aspectj)
implementation(Config.Libs.kotlinReflect)
implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.sentry.samples.spring.boot;

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.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;
Expand All @@ -18,6 +19,7 @@
import org.springframework.web.reactive.function.client.WebClient;

@SpringBootApplication
@EnableCaching
@EnableScheduling
public class SentryDemoApplication {
public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.sentry.samples.spring.boot;

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<Long, Todo> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ sentry.profile-session-sample-rate=1.0
sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces
sentry.profile-lifecycle=TRACE

# Cache tracing
sentry.enable-cache-tracing=true
spring.cache.cache-names=todos
spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s

# Database configuration
spring.datasource.url=jdbc:p6spy:hsqldb:mem:testdb
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.sentry.spring.SpringProfilesEventProcessor;
import io.sentry.spring.SpringSecuritySentryUserProvider;
import io.sentry.spring.boot.graphql.SentryGraphqlAutoConfiguration;
import io.sentry.spring.cache.SentryCacheBeanPostProcessor;
import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration;
import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration;
import io.sentry.spring.checkin.SentryQuartzConfiguration;
Expand Down Expand Up @@ -64,6 +65,7 @@
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.info.GitProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
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 @@ -216,6 +218,19 @@ static class GraphqlConfiguration {}
})
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
27 changes: 27 additions & 0 deletions sentry-spring/api/sentry-spring.api
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,33 @@ public final class io/sentry/spring/SpringSecuritySentryUserProvider : io/sentry
public fun provideUser ()Lio/sentry/protocol/User;
}

public final class io/sentry/spring/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/spring/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;
public fun getCacheNames ()Ljava/util/Collection;
}

public final class io/sentry/spring/cache/SentryCacheWrapper : org/springframework/cache/Cache {
public fun <init> (Lorg/springframework/cache/Cache;Lio/sentry/IScopes;)V
public fun clear ()V
public fun evict (Ljava/lang/Object;)V
public fun evictIfPresent (Ljava/lang/Object;)Z
public fun get (Ljava/lang/Object;)Lorg/springframework/cache/Cache$ValueWrapper;
public fun get (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;
public fun get (Ljava/lang/Object;Ljava/util/concurrent/Callable;)Ljava/lang/Object;
public fun getName ()Ljava/lang/String;
public fun getNativeCache ()Ljava/lang/Object;
public fun invalidate ()Z
public fun put (Ljava/lang/Object;Ljava/lang/Object;)V
public fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Lorg/springframework/cache/Cache$ValueWrapper;
}

public abstract interface annotation class io/sentry/spring/checkin/SentryCheckIn : java/lang/annotation/Annotation {
public abstract fun heartbeat ()Z
public abstract fun monitorSlug ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.sentry.spring.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,37 @@
package io.sentry.spring.cache;

import io.sentry.IScopes;
import java.util.Collection;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;

/** Wraps a Spring {@link CacheManager} to return Sentry-instrumented caches. */
@ApiStatus.Internal
public final class SentryCacheManagerWrapper implements CacheManager {

private final @NotNull CacheManager delegate;
private final @NotNull IScopes scopes;

public SentryCacheManagerWrapper(
final @NotNull CacheManager delegate, final @NotNull IScopes scopes) {
this.delegate = delegate;
this.scopes = scopes;
}

@Override
public @Nullable Cache getCache(final @NotNull String name) {
final Cache cache = delegate.getCache(name);
if (cache == null || cache instanceof SentryCacheWrapper) {
return cache;
}
return new SentryCacheWrapper(cache, scopes);
}

@Override
public @NotNull Collection<String> getCacheNames() {
return delegate.getCacheNames();
}
}
Loading
Loading