Skip to content

Commit 7e66a3d

Browse files
adinauerclaude
andcommitted
feat(spring): Add cache tracing for Spring Boot 2 / Spring 5
Port cache tracing instrumentation from sentry-spring-jakarta to sentry-spring for Spring Boot 2 users. Adds SentryCacheWrapper, SentryCacheManagerWrapper, and SentryCacheBeanPostProcessor in the io.sentry.spring.cache package. Wires auto-configuration in sentry-spring-boot via sentry.enable-cache-tracing=true property. The retrieve() methods are omitted since Spring 5 does not have them (they were added in Spring 6.1). Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 658e082 commit 7e66a3d

File tree

8 files changed

+739
-0
lines changed

8 files changed

+739
-0
lines changed

sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.sentry.spring.SpringProfilesEventProcessor;
2626
import io.sentry.spring.SpringSecuritySentryUserProvider;
2727
import io.sentry.spring.boot.graphql.SentryGraphqlAutoConfiguration;
28+
import io.sentry.spring.cache.SentryCacheBeanPostProcessor;
2829
import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration;
2930
import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration;
3031
import io.sentry.spring.checkin.SentryQuartzConfiguration;
@@ -64,6 +65,7 @@
6465
import org.springframework.boot.context.properties.EnableConfigurationProperties;
6566
import org.springframework.boot.info.GitProperties;
6667
import org.springframework.boot.web.servlet.FilterRegistrationBean;
68+
import org.springframework.cache.CacheManager;
6769
import org.springframework.context.annotation.Bean;
6870
import org.springframework.context.annotation.Conditional;
6971
import org.springframework.context.annotation.Configuration;
@@ -216,6 +218,19 @@ static class GraphqlConfiguration {}
216218
})
217219
static class QuartzConfiguration {}
218220

221+
@Configuration(proxyBeanMethods = false)
222+
@ConditionalOnClass(CacheManager.class)
223+
@ConditionalOnProperty(name = "sentry.enable-cache-tracing", havingValue = "true")
224+
@Open
225+
static class SentryCacheConfiguration {
226+
227+
@Bean
228+
public static @NotNull SentryCacheBeanPostProcessor sentryCacheBeanPostProcessor() {
229+
SentryIntegrationPackageStorage.getInstance().addIntegration("SpringCache");
230+
return new SentryCacheBeanPostProcessor();
231+
}
232+
}
233+
219234
@Configuration(proxyBeanMethods = false)
220235
@ConditionalOnClass(ProceedingJoinPoint.class)
221236
@ConditionalOnProperty(

sentry-spring/api/sentry-spring.api

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,33 @@ public final class io/sentry/spring/SpringSecuritySentryUserProvider : io/sentry
104104
public fun provideUser ()Lio/sentry/protocol/User;
105105
}
106106

107+
public final class io/sentry/spring/cache/SentryCacheBeanPostProcessor : org/springframework/beans/factory/config/BeanPostProcessor, org/springframework/core/PriorityOrdered {
108+
public fun <init> ()V
109+
public fun getOrder ()I
110+
public fun postProcessAfterInitialization (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;
111+
}
112+
113+
public final class io/sentry/spring/cache/SentryCacheManagerWrapper : org/springframework/cache/CacheManager {
114+
public fun <init> (Lorg/springframework/cache/CacheManager;Lio/sentry/IScopes;)V
115+
public fun getCache (Ljava/lang/String;)Lorg/springframework/cache/Cache;
116+
public fun getCacheNames ()Ljava/util/Collection;
117+
}
118+
119+
public final class io/sentry/spring/cache/SentryCacheWrapper : org/springframework/cache/Cache {
120+
public fun <init> (Lorg/springframework/cache/Cache;Lio/sentry/IScopes;)V
121+
public fun clear ()V
122+
public fun evict (Ljava/lang/Object;)V
123+
public fun evictIfPresent (Ljava/lang/Object;)Z
124+
public fun get (Ljava/lang/Object;)Lorg/springframework/cache/Cache$ValueWrapper;
125+
public fun get (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;
126+
public fun get (Ljava/lang/Object;Ljava/util/concurrent/Callable;)Ljava/lang/Object;
127+
public fun getName ()Ljava/lang/String;
128+
public fun getNativeCache ()Ljava/lang/Object;
129+
public fun invalidate ()Z
130+
public fun put (Ljava/lang/Object;Ljava/lang/Object;)V
131+
public fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Lorg/springframework/cache/Cache$ValueWrapper;
132+
}
133+
107134
public abstract interface annotation class io/sentry/spring/checkin/SentryCheckIn : java/lang/annotation/Annotation {
108135
public abstract fun heartbeat ()Z
109136
public abstract fun monitorSlug ()Ljava/lang/String;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.sentry.spring.cache;
2+
3+
import io.sentry.ScopesAdapter;
4+
import org.jetbrains.annotations.ApiStatus;
5+
import org.jetbrains.annotations.NotNull;
6+
import org.springframework.beans.BeansException;
7+
import org.springframework.beans.factory.config.BeanPostProcessor;
8+
import org.springframework.cache.CacheManager;
9+
import org.springframework.core.Ordered;
10+
import org.springframework.core.PriorityOrdered;
11+
12+
/** Wraps {@link CacheManager} beans in {@link SentryCacheManagerWrapper} for instrumentation. */
13+
@ApiStatus.Internal
14+
public final class SentryCacheBeanPostProcessor implements BeanPostProcessor, PriorityOrdered {
15+
16+
@Override
17+
public @NotNull Object postProcessAfterInitialization(
18+
final @NotNull Object bean, final @NotNull String beanName) throws BeansException {
19+
if (bean instanceof CacheManager && !(bean instanceof SentryCacheManagerWrapper)) {
20+
return new SentryCacheManagerWrapper((CacheManager) bean, ScopesAdapter.getInstance());
21+
}
22+
return bean;
23+
}
24+
25+
@Override
26+
public int getOrder() {
27+
return Ordered.LOWEST_PRECEDENCE;
28+
}
29+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.sentry.spring.cache;
2+
3+
import io.sentry.IScopes;
4+
import java.util.Collection;
5+
import org.jetbrains.annotations.ApiStatus;
6+
import org.jetbrains.annotations.NotNull;
7+
import org.jetbrains.annotations.Nullable;
8+
import org.springframework.cache.Cache;
9+
import org.springframework.cache.CacheManager;
10+
11+
/** Wraps a Spring {@link CacheManager} to return Sentry-instrumented caches. */
12+
@ApiStatus.Internal
13+
public final class SentryCacheManagerWrapper implements CacheManager {
14+
15+
private final @NotNull CacheManager delegate;
16+
private final @NotNull IScopes scopes;
17+
18+
public SentryCacheManagerWrapper(
19+
final @NotNull CacheManager delegate, final @NotNull IScopes scopes) {
20+
this.delegate = delegate;
21+
this.scopes = scopes;
22+
}
23+
24+
@Override
25+
public @Nullable Cache getCache(final @NotNull String name) {
26+
final Cache cache = delegate.getCache(name);
27+
if (cache == null || cache instanceof SentryCacheWrapper) {
28+
return cache;
29+
}
30+
return new SentryCacheWrapper(cache, scopes);
31+
}
32+
33+
@Override
34+
public @NotNull Collection<String> getCacheNames() {
35+
return delegate.getCacheNames();
36+
}
37+
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package io.sentry.spring.cache;
2+
3+
import io.sentry.IScopes;
4+
import io.sentry.ISpan;
5+
import io.sentry.SpanDataConvention;
6+
import io.sentry.SpanOptions;
7+
import io.sentry.SpanStatus;
8+
import java.util.Arrays;
9+
import java.util.concurrent.Callable;
10+
import java.util.concurrent.atomic.AtomicBoolean;
11+
import org.jetbrains.annotations.ApiStatus;
12+
import org.jetbrains.annotations.NotNull;
13+
import org.jetbrains.annotations.Nullable;
14+
import org.springframework.cache.Cache;
15+
16+
/** Wraps a Spring {@link Cache} to create Sentry spans for cache operations. */
17+
@ApiStatus.Internal
18+
public final class SentryCacheWrapper implements Cache {
19+
20+
private static final String TRACE_ORIGIN = "auto.cache.spring";
21+
22+
private final @NotNull Cache delegate;
23+
private final @NotNull IScopes scopes;
24+
25+
public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes scopes) {
26+
this.delegate = delegate;
27+
this.scopes = scopes;
28+
}
29+
30+
@Override
31+
public @NotNull String getName() {
32+
return delegate.getName();
33+
}
34+
35+
@Override
36+
public @NotNull Object getNativeCache() {
37+
return delegate.getNativeCache();
38+
}
39+
40+
@Override
41+
public @Nullable ValueWrapper get(final @NotNull Object key) {
42+
final ISpan span = startSpan("cache.get", key);
43+
if (span == null) {
44+
return delegate.get(key);
45+
}
46+
try {
47+
final ValueWrapper result = delegate.get(key);
48+
span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null);
49+
span.setStatus(SpanStatus.OK);
50+
return result;
51+
} catch (Throwable e) {
52+
span.setStatus(SpanStatus.INTERNAL_ERROR);
53+
span.setThrowable(e);
54+
throw e;
55+
} finally {
56+
span.finish();
57+
}
58+
}
59+
60+
@Override
61+
public @Nullable <T> T get(final @NotNull Object key, final @Nullable Class<T> type) {
62+
final ISpan span = startSpan("cache.get", key);
63+
if (span == null) {
64+
return delegate.get(key, type);
65+
}
66+
try {
67+
final T result = delegate.get(key, type);
68+
span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null);
69+
span.setStatus(SpanStatus.OK);
70+
return result;
71+
} catch (Throwable e) {
72+
span.setStatus(SpanStatus.INTERNAL_ERROR);
73+
span.setThrowable(e);
74+
throw e;
75+
} finally {
76+
span.finish();
77+
}
78+
}
79+
80+
@Override
81+
public @Nullable <T> T get(final @NotNull Object key, final @NotNull Callable<T> valueLoader) {
82+
final ISpan span = startSpan("cache.get", key);
83+
if (span == null) {
84+
return delegate.get(key, valueLoader);
85+
}
86+
try {
87+
final AtomicBoolean loaderInvoked = new AtomicBoolean(false);
88+
final T result =
89+
delegate.get(
90+
key,
91+
() -> {
92+
loaderInvoked.set(true);
93+
return valueLoader.call();
94+
});
95+
span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get());
96+
span.setStatus(SpanStatus.OK);
97+
return result;
98+
} catch (Throwable e) {
99+
span.setStatus(SpanStatus.INTERNAL_ERROR);
100+
span.setThrowable(e);
101+
throw e;
102+
} finally {
103+
span.finish();
104+
}
105+
}
106+
107+
@Override
108+
public void put(final @NotNull Object key, final @Nullable Object value) {
109+
final ISpan span = startSpan("cache.put", key);
110+
if (span == null) {
111+
delegate.put(key, value);
112+
return;
113+
}
114+
try {
115+
delegate.put(key, value);
116+
span.setStatus(SpanStatus.OK);
117+
} catch (Throwable e) {
118+
span.setStatus(SpanStatus.INTERNAL_ERROR);
119+
span.setThrowable(e);
120+
throw e;
121+
} finally {
122+
span.finish();
123+
}
124+
}
125+
126+
// putIfAbsent is not instrumented — we cannot know ahead of time whether the put
127+
// will actually happen, and emitting a cache.put span for a no-op would be misleading.
128+
// This matches sentry-python and sentry-javascript which also skip conditional puts.
129+
// We must override to bypass the default implementation which calls this.get() + this.put().
130+
@Override
131+
public @Nullable ValueWrapper putIfAbsent(
132+
final @NotNull Object key, final @Nullable Object value) {
133+
return delegate.putIfAbsent(key, value);
134+
}
135+
136+
@Override
137+
public void evict(final @NotNull Object key) {
138+
final ISpan span = startSpan("cache.remove", key);
139+
if (span == null) {
140+
delegate.evict(key);
141+
return;
142+
}
143+
try {
144+
delegate.evict(key);
145+
span.setStatus(SpanStatus.OK);
146+
} catch (Throwable e) {
147+
span.setStatus(SpanStatus.INTERNAL_ERROR);
148+
span.setThrowable(e);
149+
throw e;
150+
} finally {
151+
span.finish();
152+
}
153+
}
154+
155+
@Override
156+
public boolean evictIfPresent(final @NotNull Object key) {
157+
final ISpan span = startSpan("cache.remove", key);
158+
if (span == null) {
159+
return delegate.evictIfPresent(key);
160+
}
161+
try {
162+
final boolean result = delegate.evictIfPresent(key);
163+
span.setStatus(SpanStatus.OK);
164+
return result;
165+
} catch (Throwable e) {
166+
span.setStatus(SpanStatus.INTERNAL_ERROR);
167+
span.setThrowable(e);
168+
throw e;
169+
} finally {
170+
span.finish();
171+
}
172+
}
173+
174+
@Override
175+
public void clear() {
176+
final ISpan span = startSpan("cache.flush", null);
177+
if (span == null) {
178+
delegate.clear();
179+
return;
180+
}
181+
try {
182+
delegate.clear();
183+
span.setStatus(SpanStatus.OK);
184+
} catch (Throwable e) {
185+
span.setStatus(SpanStatus.INTERNAL_ERROR);
186+
span.setThrowable(e);
187+
throw e;
188+
} finally {
189+
span.finish();
190+
}
191+
}
192+
193+
@Override
194+
public boolean invalidate() {
195+
final ISpan span = startSpan("cache.flush", null);
196+
if (span == null) {
197+
return delegate.invalidate();
198+
}
199+
try {
200+
final boolean result = delegate.invalidate();
201+
span.setStatus(SpanStatus.OK);
202+
return result;
203+
} catch (Throwable e) {
204+
span.setStatus(SpanStatus.INTERNAL_ERROR);
205+
span.setThrowable(e);
206+
throw e;
207+
} finally {
208+
span.finish();
209+
}
210+
}
211+
212+
private @Nullable ISpan startSpan(final @NotNull String operation, final @Nullable Object key) {
213+
if (!scopes.getOptions().isEnableCacheTracing()) {
214+
return null;
215+
}
216+
217+
final ISpan activeSpan = scopes.getSpan();
218+
if (activeSpan == null || activeSpan.isNoOp()) {
219+
return null;
220+
}
221+
222+
final SpanOptions spanOptions = new SpanOptions();
223+
spanOptions.setOrigin(TRACE_ORIGIN);
224+
final String keyString = key != null ? String.valueOf(key) : null;
225+
final ISpan span = activeSpan.startChild(operation, keyString, spanOptions);
226+
if (keyString != null) {
227+
span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString));
228+
}
229+
return span;
230+
}
231+
}

0 commit comments

Comments
 (0)