From 89254351f6db9219450551fb80cc55a8b6f9a45c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 2 Mar 2026 16:45:16 +0100 Subject: [PATCH 01/48] feat(spring): [Cache Tracing 1] Add SentryCacheWrapper and SentryCacheManagerWrapper Co-Authored-By: Claude Opus 4.6 --- sentry-spring-7/api/sentry-spring-7.api | 21 ++ .../cache/SentryCacheManagerWrapper.java | 37 +++ .../spring7/cache/SentryCacheWrapper.java | 229 ++++++++++++++++ .../cache/SentryCacheManagerWrapperTest.kt | 48 ++++ .../spring7/cache/SentryCacheWrapperTest.kt | 259 ++++++++++++++++++ sentry/api/sentry.api | 2 + .../java/io/sentry/SpanDataConvention.java | 2 + 7 files changed, 598 insertions(+) create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt diff --git a/sentry-spring-7/api/sentry-spring-7.api b/sentry-spring-7/api/sentry-spring-7.api index 3a57c13e835..41514d95552 100644 --- a/sentry-spring-7/api/sentry-spring-7.api +++ b/sentry-spring-7/api/sentry-spring-7.api @@ -104,6 +104,27 @@ public final class io/sentry/spring7/SpringSecuritySentryUserProvider : io/sentr public fun provideUser ()Lio/sentry/protocol/User; } +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; + public fun getCacheNames ()Ljava/util/Collection; +} + +public final class io/sentry/spring7/cache/SentryCacheWrapper : org/springframework/cache/Cache { + public fun (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/spring7/checkin/SentryCheckIn : java/lang/annotation/Annotation { public abstract fun heartbeat ()Z public abstract fun monitorSlug ()Ljava/lang/String; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java new file mode 100644 index 00000000000..5a52734756b --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java @@ -0,0 +1,37 @@ +package io.sentry.spring7.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) { + return null; + } + return new SentryCacheWrapper(cache, scopes); + } + + @Override + public @NotNull Collection getCacheNames() { + return delegate.getCacheNames(); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java new file mode 100644 index 00000000000..068435bf800 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -0,0 +1,229 @@ +package io.sentry.spring7.cache; + +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.SpanDataConvention; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import java.util.Arrays; +import java.util.concurrent.Callable; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.cache.Cache; + +/** Wraps a Spring {@link Cache} to create Sentry spans for cache operations. */ +@ApiStatus.Internal +public final class SentryCacheWrapper implements Cache { + + private static final String TRACE_ORIGIN = "auto.cache.spring"; + + private final @NotNull Cache delegate; + private final @NotNull IScopes scopes; + + public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes scopes) { + this.delegate = delegate; + this.scopes = scopes; + } + + @Override + public @NotNull String getName() { + return delegate.getName(); + } + + @Override + public @NotNull Object getNativeCache() { + return delegate.getNativeCache(); + } + + @Override + public @Nullable ValueWrapper get(final @NotNull Object key) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.get(key); + } + try { + final ValueWrapper result = delegate.get(key); + span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public @Nullable T get(final @NotNull Object key, final @Nullable Class type) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.get(key, type); + } + try { + final T result = delegate.get(key, type); + span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public @Nullable T get(final @NotNull Object key, final @NotNull Callable valueLoader) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.get(key, valueLoader); + } + try { + final T result = delegate.get(key, valueLoader); + // valueLoader is called on miss, so the method always returns a value + span.setData(SpanDataConvention.CACHE_HIT_KEY, true); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public void put(final @NotNull Object key, final @Nullable Object value) { + final ISpan span = startSpan("cache.put", key); + if (span == null) { + delegate.put(key, value); + return; + } + try { + delegate.put(key, value); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public @Nullable ValueWrapper putIfAbsent( + final @NotNull Object key, final @Nullable Object value) { + final ISpan span = startSpan("cache.put", key); + if (span == null) { + return delegate.putIfAbsent(key, value); + } + try { + final ValueWrapper result = delegate.putIfAbsent(key, value); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public void evict(final @NotNull Object key) { + final ISpan span = startSpan("cache.remove", key); + if (span == null) { + delegate.evict(key); + return; + } + try { + delegate.evict(key); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public boolean evictIfPresent(final @NotNull Object key) { + final ISpan span = startSpan("cache.remove", key); + if (span == null) { + return delegate.evictIfPresent(key); + } + try { + final boolean result = delegate.evictIfPresent(key); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public void clear() { + final ISpan span = startSpan("cache.flush", null); + if (span == null) { + delegate.clear(); + return; + } + try { + delegate.clear(); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public boolean invalidate() { + final ISpan span = startSpan("cache.flush", null); + if (span == null) { + return delegate.invalidate(); + } + try { + final boolean result = delegate.invalidate(); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + private @Nullable ISpan startSpan(final @NotNull String operation, final @Nullable Object key) { + final ISpan activeSpan = scopes.getSpan(); + if (activeSpan == null || activeSpan.isNoOp()) { + return null; + } + + final SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(TRACE_ORIGIN); + final ISpan span = activeSpan.startChild(operation, getName(), spanOptions); + if (key != null) { + span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(String.valueOf(key))); + } + return span; + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt new file mode 100644 index 00000000000..ca56be7fbc9 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt @@ -0,0 +1,48 @@ +package io.sentry.spring7.cache + +import io.sentry.IScopes +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.cache.Cache +import org.springframework.cache.CacheManager + +class SentryCacheManagerWrapperTest { + + private val scopes: IScopes = mock() + private val delegate: CacheManager = mock() + + @Test + fun `getCache wraps returned cache in SentryCacheWrapper`() { + val cache = mock() + whenever(delegate.getCache("test")).thenReturn(cache) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.getCache("test") + + assertTrue(result is SentryCacheWrapper) + } + + @Test + fun `getCache returns null when delegate returns null`() { + whenever(delegate.getCache("missing")).thenReturn(null) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.getCache("missing") + + assertNull(result) + } + + @Test + fun `getCacheNames delegates to underlying cache manager`() { + whenever(delegate.cacheNames).thenReturn(listOf("cache1", "cache2")) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.cacheNames + + assertEquals(listOf("cache1", "cache2"), result) + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt new file mode 100644 index 00000000000..8187b487e8b --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -0,0 +1,259 @@ +package io.sentry.spring7.cache + +import io.sentry.IScopes +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanDataConvention +import io.sentry.SpanStatus +import io.sentry.TransactionContext +import java.util.concurrent.Callable +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.cache.Cache + +class SentryCacheWrapperTest { + + private lateinit var scopes: IScopes + private lateinit var delegate: Cache + + @BeforeTest + fun setup() { + scopes = mock() + delegate = mock() + whenever(scopes.options).thenReturn(SentryOptions()) + whenever(delegate.name).thenReturn("testCache") + } + + private fun createTransaction(): SentryTracer { + val tx = SentryTracer(TransactionContext("tx", "op"), scopes) + whenever(scopes.span).thenReturn(tx) + return tx + } + + // -- get(Object key) -- + + @Test + fun `get with ValueWrapper creates span with cache hit true on hit`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) + + val result = wrapper.get("myKey") + + assertEquals(valueWrapper, result) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.get", span.operation) + assertEquals("testCache", span.description) + assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("auto.cache.spring", span.spanContext.origin) + } + + @Test + fun `get with ValueWrapper creates span with cache hit false on miss`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + val result = wrapper.get("myKey") + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- get(Object key, Class) -- + + @Test + fun `get with type creates span with cache hit true on hit`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey", String::class.java)).thenReturn("value") + + val result = wrapper.get("myKey", String::class.java) + + assertEquals("value", result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + @Test + fun `get with type creates span with cache hit false on miss`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey", String::class.java)).thenReturn(null) + + val result = wrapper.get("myKey", String::class.java) + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- get(Object key, Callable) -- + + @Test + fun `get with callable creates span with cache hit true`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val callable = Callable { "loaded" } + whenever(delegate.get("myKey", callable)).thenReturn("loaded") + + val result = wrapper.get("myKey", callable) + + assertEquals("loaded", result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- put -- + + @Test + fun `put creates cache put span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + + wrapper.put("myKey", "myValue") + + verify(delegate).put("myKey", "myValue") + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + } + + // -- putIfAbsent -- + + @Test + fun `putIfAbsent creates cache put span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(null) + + wrapper.putIfAbsent("myKey", "myValue") + + assertEquals(1, tx.spans.size) + assertEquals("cache.put", tx.spans.first().operation) + } + + // -- evict -- + + @Test + fun `evict creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + + wrapper.evict("myKey") + + verify(delegate).evict("myKey") + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.remove", span.operation) + assertEquals(SpanStatus.OK, span.status) + } + + // -- evictIfPresent -- + + @Test + fun `evictIfPresent creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.evictIfPresent("myKey")).thenReturn(true) + + val result = wrapper.evictIfPresent("myKey") + + assertTrue(result) + assertEquals(1, tx.spans.size) + assertEquals("cache.remove", tx.spans.first().operation) + } + + // -- clear -- + + @Test + fun `clear creates cache flush span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + + wrapper.clear() + + verify(delegate).clear() + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.flush", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) + } + + // -- invalidate -- + + @Test + fun `invalidate creates cache flush span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.invalidate()).thenReturn(true) + + val result = wrapper.invalidate() + + assertTrue(result) + assertEquals(1, tx.spans.size) + assertEquals("cache.flush", tx.spans.first().operation) + } + + // -- no span when no active transaction -- + + @Test + fun `does not create span when there is no active transaction`() { + whenever(scopes.span).thenReturn(null) + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + wrapper.get("myKey") + + verify(delegate).get("myKey") + } + + // -- error handling -- + + @Test + fun `sets error status and throwable on exception`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("cache error") + whenever(delegate.get("myKey")).thenThrow(exception) + + assertFailsWith { wrapper.get("myKey") } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + } + + // -- delegation -- + + @Test + fun `getName delegates to underlying cache`() { + val wrapper = SentryCacheWrapper(delegate, scopes) + assertEquals("testCache", wrapper.name) + } + + @Test + fun `getNativeCache delegates to underlying cache`() { + val nativeCache = Object() + whenever(delegate.nativeCache).thenReturn(nativeCache) + val wrapper = SentryCacheWrapper(delegate, scopes) + + assertEquals(nativeCache, wrapper.nativeCache) + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4399b191d21..1835f7043ca 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4277,6 +4277,8 @@ public final class io/sentry/SpanContext$JsonKeys { public abstract interface class io/sentry/SpanDataConvention { public static final field BLOCKED_MAIN_THREAD_KEY Ljava/lang/String; + public static final field CACHE_HIT_KEY Ljava/lang/String; + public static final field CACHE_KEY_KEY Ljava/lang/String; public static final field CALL_STACK_KEY Ljava/lang/String; public static final field CONTRIBUTES_TTFD Ljava/lang/String; public static final field CONTRIBUTES_TTID Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/SpanDataConvention.java b/sentry/src/main/java/io/sentry/SpanDataConvention.java index c4329f6dcad..8a31a7c70ff 100644 --- a/sentry/src/main/java/io/sentry/SpanDataConvention.java +++ b/sentry/src/main/java/io/sentry/SpanDataConvention.java @@ -26,4 +26,6 @@ public interface SpanDataConvention { String HTTP_START_TIMESTAMP = "http.start_timestamp"; String HTTP_END_TIMESTAMP = "http.end_timestamp"; String PROFILER_ID = "profiler_id"; + String CACHE_HIT_KEY = "cache.hit"; + String CACHE_KEY_KEY = "cache.key"; } From 7735a817dcd83f9efe6f900a45a122bb690268b3 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 5 Mar 2026 15:17:06 +0100 Subject: [PATCH 02/48] collection: Cache Tracing From da5bde079338d48f6fe0a9d9c8bfd6f421e599dd Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 9 Mar 2026 09:06:23 +0100 Subject: [PATCH 03/48] fix(cache): Fix span description, putIfAbsent, and Callable hit detection - Use cache key as span description instead of cache name, matching the spec and other SDKs (Python, JavaScript) - Skip instrumentation for putIfAbsent since we cannot know if a write actually occurred; override to bypass default get()+put() delegation - Wrap valueLoader Callable in get(key, Callable) to detect cache hit/miss instead of always reporting hit=true - Update tests to match new behavior Co-Authored-By: Claude --- .../spring7/cache/SentryCacheWrapper.java | 40 +++++++++---------- .../spring7/cache/SentryCacheWrapperTest.kt | 37 ++++++++++++----- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 068435bf800..f2ed581793f 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -7,6 +7,7 @@ import io.sentry.SpanStatus; import java.util.Arrays; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -83,9 +84,15 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return delegate.get(key, valueLoader); } try { - final T result = delegate.get(key, valueLoader); - // valueLoader is called on miss, so the method always returns a value - span.setData(SpanDataConvention.CACHE_HIT_KEY, true); + final AtomicBoolean loaderInvoked = new AtomicBoolean(false); + final T result = + delegate.get( + key, + () -> { + loaderInvoked.set(true); + return valueLoader.call(); + }); + span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -116,24 +123,14 @@ public void put(final @NotNull Object key, final @Nullable Object value) { } } + // putIfAbsent is not instrumented — we cannot know ahead of time whether the put + // will actually happen, and emitting a cache.put span for a no-op would be misleading. + // This matches sentry-python and sentry-javascript which also skip conditional puts. + // We must override to bypass the default implementation which calls this.get() + this.put(). @Override public @Nullable ValueWrapper putIfAbsent( final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.put", key); - if (span == null) { - return delegate.putIfAbsent(key, value); - } - try { - final ValueWrapper result = delegate.putIfAbsent(key, value); - span.setStatus(SpanStatus.OK); - return result; - } catch (Throwable e) { - span.setStatus(SpanStatus.INTERNAL_ERROR); - span.setThrowable(e); - throw e; - } finally { - span.finish(); - } + return delegate.putIfAbsent(key, value); } @Override @@ -220,9 +217,10 @@ public boolean invalidate() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); - final ISpan span = activeSpan.startChild(operation, getName(), spanOptions); - if (key != null) { - span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(String.valueOf(key))); + final String keyString = key != null ? String.valueOf(key) : null; + final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + if (keyString != null) { + span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } return span; } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index 8187b487e8b..2d59bdc8710 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -13,6 +13,8 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull import kotlin.test.assertTrue +import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -52,7 +54,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) val span = tx.spans.first() assertEquals("cache.get", span.operation) - assertEquals("testCache", span.description) + assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) @@ -103,19 +105,36 @@ class SentryCacheWrapperTest { // -- get(Object key, Callable) -- @Test - fun `get with callable creates span with cache hit true`() { + fun `get with callable creates span with cache hit true on hit`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) - val callable = Callable { "loaded" } - whenever(delegate.get("myKey", callable)).thenReturn("loaded") + // Simulate cache hit: delegate returns value without invoking the loader + whenever(delegate.get(eq("myKey"), any>())).thenReturn("cached") - val result = wrapper.get("myKey", callable) + val result = wrapper.get("myKey", Callable { "loaded" }) - assertEquals("loaded", result) + assertEquals("cached", result) assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with callable creates span with cache hit false on miss`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + // Simulate cache miss: delegate invokes the loader callable + whenever(delegate.get(eq("myKey"), any>())).thenAnswer { invocation -> + val loader = invocation.getArgument>(1) + loader.call() + } + + val result = wrapper.get("myKey", Callable { "loaded" }) + + assertEquals("loaded", result) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + // -- put -- @Test @@ -136,15 +155,15 @@ class SentryCacheWrapperTest { // -- putIfAbsent -- @Test - fun `putIfAbsent creates cache put span`() { + fun `putIfAbsent delegates without creating span`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(null) wrapper.putIfAbsent("myKey", "myValue") - assertEquals(1, tx.spans.size) - assertEquals("cache.put", tx.spans.first().operation) + verify(delegate).putIfAbsent("myKey", "myValue") + assertEquals(0, tx.spans.size) } // -- evict -- From 5e081fc9d911ddd9e5ca1a6a9e79e87b03328168 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 2 Mar 2026 16:48:20 +0100 Subject: [PATCH 04/48] feat(core): [Cache Tracing 2] Add enableCacheTracing option Co-Authored-By: Claude Opus 4.6 --- .../spring7/cache/SentryCacheWrapper.java | 4 ++++ .../spring7/cache/SentryCacheWrapperTest.kt | 19 ++++++++++++++- sentry/api/sentry.api | 4 ++++ .../main/java/io/sentry/ExternalOptions.java | 11 +++++++++ .../main/java/io/sentry/SentryOptions.java | 24 +++++++++++++++++++ .../java/io/sentry/ExternalOptionsTest.kt | 14 +++++++++++ .../test/java/io/sentry/SentryOptionsTest.kt | 7 ++++++ 7 files changed, 82 insertions(+), 1 deletion(-) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index f2ed581793f..63e4ef67745 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -210,6 +210,10 @@ public boolean invalidate() { } private @Nullable ISpan startSpan(final @NotNull String operation, final @Nullable Object key) { + if (!scopes.getOptions().isEnableCacheTracing()) { + return null; + } + final ISpan activeSpan = scopes.getSpan(); if (activeSpan == null || activeSpan.isNoOp()) { return null; diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index 2d59bdc8710..dbd3d50c2af 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -24,12 +24,14 @@ class SentryCacheWrapperTest { private lateinit var scopes: IScopes private lateinit var delegate: Cache + private lateinit var options: SentryOptions @BeforeTest fun setup() { scopes = mock() delegate = mock() - whenever(scopes.options).thenReturn(SentryOptions()) + options = SentryOptions().apply { isEnableCacheTracing = true } + whenever(scopes.options).thenReturn(options) whenever(delegate.name).thenReturn("testCache") } @@ -242,6 +244,21 @@ class SentryCacheWrapperTest { verify(delegate).get("myKey") } + // -- no span when option is disabled -- + + @Test + fun `does not create span when enableCacheTracing is false`() { + options.isEnableCacheTracing = false + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + wrapper.get("myKey") + + verify(delegate).get("myKey") + assertEquals(0, tx.spans.size) + } + // -- error handling -- @Test diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index f8712819b1f..35f463bfce9 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -520,6 +520,7 @@ public final class io/sentry/ExternalOptions { public fun getTracesSampleRate ()Ljava/lang/Double; public fun isCaptureOpenTelemetryEvents ()Ljava/lang/Boolean; public fun isEnableBackpressureHandling ()Ljava/lang/Boolean; + public fun isEnableCacheTracing ()Ljava/lang/Boolean; public fun isEnableDatabaseTransactionTracing ()Ljava/lang/Boolean; public fun isEnableLogs ()Ljava/lang/Boolean; public fun isEnableMetrics ()Ljava/lang/Boolean; @@ -536,6 +537,7 @@ public final class io/sentry/ExternalOptions { public fun setDist (Ljava/lang/String;)V public fun setDsn (Ljava/lang/String;)V public fun setEnableBackpressureHandling (Ljava/lang/Boolean;)V + public fun setEnableCacheTracing (Ljava/lang/Boolean;)V public fun setEnableDatabaseTransactionTracing (Ljava/lang/Boolean;)V public fun setEnableDeduplication (Ljava/lang/Boolean;)V public fun setEnableLogs (Ljava/lang/Boolean;)V @@ -3660,6 +3662,7 @@ public class io/sentry/SentryOptions { public fun isEnableAppStartProfiling ()Z public fun isEnableAutoSessionTracking ()Z public fun isEnableBackpressureHandling ()Z + public fun isEnableCacheTracing ()Z public fun isEnableDatabaseTransactionTracing ()Z public fun isEnableDeduplication ()Z public fun isEnableEventSizeLimiting ()Z @@ -3718,6 +3721,7 @@ public class io/sentry/SentryOptions { public fun setEnableAppStartProfiling (Z)V public fun setEnableAutoSessionTracking (Z)V public fun setEnableBackpressureHandling (Z)V + public fun setEnableCacheTracing (Z)V public fun setEnableDatabaseTransactionTracing (Z)V public fun setEnableDeduplication (Z)V public fun setEnableEventSizeLimiting (Z)V diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 8f16bcede01..dade1f140c8 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -57,6 +57,7 @@ public final class ExternalOptions { private @Nullable Boolean sendDefaultPii; private @Nullable Boolean enableBackpressureHandling; private @Nullable Boolean enableDatabaseTransactionTracing; + private @Nullable Boolean enableCacheTracing; private @Nullable Boolean globalHubMode; private @Nullable Boolean forceInit; private @Nullable Boolean captureOpenTelemetryEvents; @@ -162,6 +163,8 @@ public final class ExternalOptions { options.setEnableDatabaseTransactionTracing( propertiesProvider.getBooleanProperty("enable-database-transaction-tracing")); + options.setEnableCacheTracing(propertiesProvider.getBooleanProperty("enable-cache-tracing")); + options.setGlobalHubMode(propertiesProvider.getBooleanProperty("global-hub-mode")); options.setCaptureOpenTelemetryEvents( @@ -523,6 +526,14 @@ public void setEnableDatabaseTransactionTracing( return enableDatabaseTransactionTracing; } + public void setEnableCacheTracing(final @Nullable Boolean enableCacheTracing) { + this.enableCacheTracing = enableCacheTracing; + } + + public @Nullable Boolean isEnableCacheTracing() { + return enableCacheTracing; + } + public void setGlobalHubMode(final @Nullable Boolean globalHubMode) { this.globalHubMode = globalHubMode; } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index a831a11ea8e..b8f1a067595 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -490,6 +490,9 @@ public class SentryOptions { /** Whether database transaction spans (BEGIN, COMMIT, ROLLBACK) should be traced. */ private boolean enableDatabaseTransactionTracing = false; + /** Whether cache operations (get, put, evict, clear) should be traced. */ + private boolean enableCacheTracing = false; + /** Date provider to retrieve the current date from. */ @ApiStatus.Internal private final @NotNull LazyEvaluator dateProvider = @@ -2630,6 +2633,24 @@ public void setEnableDatabaseTransactionTracing(boolean enableDatabaseTransactio this.enableDatabaseTransactionTracing = enableDatabaseTransactionTracing; } + /** + * Whether cache operations (get, put, evict, clear) should be traced. + * + * @return true if cache operations should be traced + */ + public boolean isEnableCacheTracing() { + return enableCacheTracing; + } + + /** + * Whether cache operations (get, put, evict, clear) should be traced. + * + * @param enableCacheTracing true if cache operations should be traced + */ + public void setEnableCacheTracing(boolean enableCacheTracing) { + this.enableCacheTracing = enableCacheTracing; + } + /** * Whether Sentry is enabled. * @@ -3468,6 +3489,9 @@ public void merge(final @NotNull ExternalOptions options) { if (options.isEnableDatabaseTransactionTracing() != null) { setEnableDatabaseTransactionTracing(options.isEnableDatabaseTransactionTracing()); } + if (options.isEnableCacheTracing() != null) { + setEnableCacheTracing(options.isEnableCacheTracing()); + } if (options.getMaxRequestBodySize() != null) { setMaxRequestBodySize(options.getMaxRequestBodySize()); } diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 9612a052624..298eff34ba0 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -331,6 +331,20 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with enableCacheTracing set to true`() { + withPropertiesFile("enable-cache-tracing=true") { options -> + assertTrue(options.isEnableCacheTracing == true) + } + } + + @Test + fun `creates options with enableCacheTracing set to false`() { + withPropertiesFile("enable-cache-tracing=false") { options -> + assertTrue(options.isEnableCacheTracing == false) + } + } + @Test fun `creates options with cron defaults`() { withPropertiesFile( diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 960b2838e2a..7a350893b18 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -400,6 +400,7 @@ class SentryOptionsTest { externalOptions.ignoredErrors = listOf("Some error", "Another .*") externalOptions.isEnableBackpressureHandling = false externalOptions.isEnableDatabaseTransactionTracing = true + externalOptions.isEnableCacheTracing = true externalOptions.maxRequestBodySize = SentryOptions.RequestSize.MEDIUM externalOptions.isSendDefaultPii = true externalOptions.isForceInit = true @@ -465,6 +466,7 @@ class SentryOptionsTest { ) assertFalse(options.isEnableBackpressureHandling) assertTrue(options.isEnableDatabaseTransactionTracing) + assertTrue(options.isEnableCacheTracing) assertTrue(options.isForceInit) assertNotNull(options.cron) assertEquals(10L, options.cron?.defaultCheckinMargin) @@ -701,6 +703,11 @@ class SentryOptionsTest { assertFalse(SentryOptions().isEnableDatabaseTransactionTracing) } + @Test + fun `when options are initialized, enableCacheTracing is set to false by default`() { + assertFalse(SentryOptions().isEnableCacheTracing) + } + @Test fun `when options are initialized, metrics is enabled by default`() { assertTrue(SentryOptions().metrics.isEnabled) From 381590845b325e8f4a7703d671c1ff7518a03f9a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 2 Mar 2026 16:54:22 +0100 Subject: [PATCH 05/48] feat(spring): [Cache Tracing 3] Add BeanPostProcessor and auto-configuration Co-Authored-By: Claude Opus 4.6 --- sentry-spring-7/api/sentry-spring-7.api | 6 +++ .../cache/SentryCacheBeanPostProcessor.java | 29 ++++++++++++ .../cache/SentryCacheBeanPostProcessorTest.kt | 44 +++++++++++++++++++ .../spring/boot4/SentryAutoConfiguration.java | 15 +++++++ .../boot4/SentryAutoConfigurationTest.kt | 30 +++++++++++++ 5 files changed, 124 insertions(+) create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheBeanPostProcessor.java create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheBeanPostProcessorTest.kt diff --git a/sentry-spring-7/api/sentry-spring-7.api b/sentry-spring-7/api/sentry-spring-7.api index 41514d95552..b37747cf765 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 00000000000..b6569a9953b --- /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 00000000000..54c5e696d6a --- /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 1b804e8cb8d..ae9e3ac50fe 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 a5566ef2f30..7f30c860bb3 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 @@ -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", @@ -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) @@ -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 { From 84f4889dad01d14e2901608322c0409751a23853 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 3 Mar 2026 06:30:16 +0100 Subject: [PATCH 06/48] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6820a9a43..05c0b8e1acb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,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 ([#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. From fc794242260a82d0325992eea20bcdfb3b4ff0a4 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 9 Mar 2026 09:24:11 +0100 Subject: [PATCH 07/48] fix: Update changelog PR references Co-Authored-By: Claude --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c0b8e1acb..d1ca5dabff4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ - 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 ([#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)) +- 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)) From ae0f569c4cc55d4148c37dfeda5834bd67441f25 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 5 Mar 2026 15:51:02 +0100 Subject: [PATCH 08/48] feat(samples): [Cache Tracing 4] Add cache tracing e2e sample Co-Authored-By: Claude Opus 4.6 --- .../samples/spring/boot4/CacheController.java | 34 +++++++++++++ .../spring/boot4/SentryDemoApplication.java | 9 ++++ .../samples/spring/boot4/TodoService.java | 29 +++++++++++ .../src/main/resources/application.properties | 1 + .../io/sentry/systemtest/CacheSystemTest.kt | 51 +++++++++++++++++++ .../api/sentry-system-test-support.api | 3 ++ .../sentry/systemtest/util/RestTestClient.kt | 18 +++++++ 7 files changed, 145 insertions(+) create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CacheController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CacheController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CacheController.java new file mode 100644 index 00000000000..3c2e66442de --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CacheController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot4; + +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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java index 71463a9a819..0debf405c63 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java @@ -9,6 +9,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.restclient.RestTemplateBuilder; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.quartz.CronTriggerFactoryBean; @@ -19,12 +22,18 @@ import org.springframework.web.reactive.function.client.WebClient; @SpringBootApplication +@EnableCaching @EnableScheduling public class SentryDemoApplication { public static void main(String[] args) { SpringApplication.run(SentryDemoApplication.class, args); } + @Bean + CacheManager cacheManager() { + return new ConcurrentMapCacheManager("todos"); + } + @Bean RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoService.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoService.java new file mode 100644 index 00000000000..c837ab8398a --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoService.java @@ -0,0 +1,29 @@ +package io.sentry.samples.spring.boot4; + +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 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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties index 9ba7a54aaf8..17c204e67f6 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties @@ -20,6 +20,7 @@ sentry.logs.enabled=true sentry.profile-session-sample-rate=1.0 sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces sentry.profile-lifecycle=TRACE +sentry.enable-cache-tracing=true # Uncomment and set to true to enable aot compatibility # This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt new file mode 100644 index 00000000000..7853750a8fb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -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") + } + } +} diff --git a/sentry-system-test-support/api/sentry-system-test-support.api b/sentry-system-test-support/api/sentry-system-test-support.api index 51ef7da55d9..83a9f288d0c 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -548,7 +548,9 @@ public final class io/sentry/systemtest/util/RestTestClient : io/sentry/systemte public static synthetic fun createPerson$default (Lio/sentry/systemtest/util/RestTestClient;Lio/sentry/systemtest/Person;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; public final fun createPersonDistributedTracing (Lio/sentry/systemtest/Person;Ljava/util/Map;)Lio/sentry/systemtest/Person; public static synthetic fun createPersonDistributedTracing$default (Lio/sentry/systemtest/util/RestTestClient;Lio/sentry/systemtest/Person;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; + public final fun deleteCachedTodo (J)V public final fun errorWithFeatureFlag (Ljava/lang/String;)Ljava/lang/String; + public final fun getCachedTodo (J)Lio/sentry/systemtest/Todo; public final fun getCountMetric ()Ljava/lang/String; public final fun getDistributionMetric (J)Ljava/lang/String; public final fun getGaugeMetric (J)Ljava/lang/String; @@ -558,6 +560,7 @@ public final class io/sentry/systemtest/util/RestTestClient : io/sentry/systemte public final fun getTodo (J)Lio/sentry/systemtest/Todo; public final fun getTodoRestClient (J)Lio/sentry/systemtest/Todo; public final fun getTodoWebclient (J)Lio/sentry/systemtest/Todo; + public final fun saveCachedTodo (Lio/sentry/systemtest/Todo;)Lio/sentry/systemtest/Todo; } public final class io/sentry/systemtest/util/SentryMockServerClient : io/sentry/systemtest/util/LoggingInsecureRestClient { diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt index bdaa2333f21..da552ff93bc 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt @@ -50,6 +50,24 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl return callTyped(request, true) } + fun getCachedTodo(id: Long): Todo? { + val request = Request.Builder().url("$backendBaseUrl/cache/$id") + + return callTyped(request, true) + } + + fun saveCachedTodo(todo: Todo): Todo? { + val request = Request.Builder().url("$backendBaseUrl/cache/").post(toRequestBody(todo)) + + return callTyped(request, true) + } + + fun deleteCachedTodo(id: Long) { + val request = Request.Builder().url("$backendBaseUrl/cache/$id").delete() + + call(request, true) + } + fun checkFeatureFlag(flagKey: String): FeatureFlagResponse? { val request = Request.Builder().url("$backendBaseUrl/feature-flag/check/$flagKey") From cf3b325a060b9f07378378d2286eed7ede5785a5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 9 Mar 2026 06:32:44 +0100 Subject: [PATCH 09/48] ref(samples): Replace ConcurrentMapCacheManager with Caffeine Use Caffeine as the cache provider instead of a plain ConcurrentMapCacheManager. Spring Boot auto-configures CaffeineCacheManager when Caffeine is on the classpath, so the explicit CacheManager bean is no longer needed. Co-Authored-By: Claude --- .../sentry-samples-spring-boot-4/build.gradle.kts | 3 +++ .../sentry/samples/spring/boot4/SentryDemoApplication.java | 7 ------- .../src/main/resources/application.properties | 2 ++ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts index a7fa57dac83..39a9f59a404 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts @@ -57,6 +57,9 @@ dependencies { implementation(projects.sentryQuartz) implementation(projects.sentryAsyncProfiler) + // cache tracing + implementation("com.github.ben-manes.caffeine:caffeine") + // database query tracing implementation(projects.sentryJdbc) runtimeOnly(libs.hsqldb) diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java index 0debf405c63..13d97fa8442 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java @@ -9,9 +9,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.restclient.RestTemplateBuilder; -import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.concurrent.ConcurrentMapCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.quartz.CronTriggerFactoryBean; @@ -29,11 +27,6 @@ public static void main(String[] args) { SpringApplication.run(SentryDemoApplication.class, args); } - @Bean - CacheManager cacheManager() { - return new ConcurrentMapCacheManager("todos"); - } - @Bean RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties index 17c204e67f6..8198059343a 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties @@ -21,6 +21,8 @@ sentry.profile-session-sample-rate=1.0 sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces sentry.profile-lifecycle=TRACE sentry.enable-cache-tracing=true +spring.cache.cache-names=todos +spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s # Uncomment and set to true to enable aot compatibility # This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) From 7c549331f1a6d717706bee9112806e25bdc0f995 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 9 Mar 2026 13:17:11 +0100 Subject: [PATCH 10/48] fix dependencies; move to toml --- gradle/libs.versions.toml | 2 ++ sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd2a471f695..7d9f0e02a37 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,6 +101,7 @@ androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version androidx-browser = { module = "androidx.browser:browser", version = "1.8.0" } async-profiler = { module = "tools.profiler:async-profiler", version.ref = "asyncProfiler" } async-profiler-jfr-converter = { module = "tools.profiler:jfr-converter", version.ref = "asyncProfiler" } +caffeine = { module = "com.github.ben-manes.caffeine:caffeine" } coil-compose = { module = "io.coil-kt:coil-compose", version = "2.6.0" } commons-compress = {module = "org.apache.commons:commons-compress", version = "1.25.0"} context-propagation = { module = "io.micrometer:context-propagation", version = "1.1.0" } @@ -196,6 +197,7 @@ springboot4-starter-restclient = { module = "org.springframework.boot:spring-boo springboot4-starter-webclient = { module = "org.springframework.boot:spring-boot-starter-webclient", version.ref = "springboot4" } springboot4-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot4" } springboot4-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot4" } +springboot4-starter-cache = { module = "org.springframework.boot:spring-boot-starter-cache", version.ref = "springboot4" } timber = { module = "com.jakewharton.timber:timber", version = "4.7.1" } # Animalsniffer signature diff --git a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts index 39a9f59a404..f43cc47cc6d 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts @@ -58,7 +58,8 @@ dependencies { implementation(projects.sentryAsyncProfiler) // cache tracing - implementation("com.github.ben-manes.caffeine:caffeine") + implementation(libs.springboot4.starter.cache) + implementation(libs.caffeine) // database query tracing implementation(projects.sentryJdbc) From 6822d8ac805bee39b840ea8a9b11ac785b893804 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 9 Mar 2026 16:23:08 +0100 Subject: [PATCH 11/48] feat(jcache): Add SentryJCacheWrapper for JCache (JSR-107) cache tracing --- README.md | 1 + buildSrc/src/main/java/Config.kt | 1 + gradle/libs.versions.toml | 1 + sentry-jcache/README.md | 13 + sentry-jcache/api/sentry-jcache.api | 37 ++ sentry-jcache/build.gradle.kts | 90 ++++ .../io/sentry/jcache/SentryJCacheWrapper.java | 476 +++++++++++++++++ .../sentry/jcache/SentryJCacheWrapperTest.kt | 493 ++++++++++++++++++ settings.gradle.kts | 1 + 9 files changed, 1113 insertions(+) create mode 100644 sentry-jcache/README.md create mode 100644 sentry-jcache/api/sentry-jcache.api create mode 100644 sentry-jcache/build.gradle.kts create mode 100644 sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java create mode 100644 sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt diff --git a/README.md b/README.md index 31285e2be29..25fedc8217f 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Sentry SDK for Java and Android | sentry-graphql | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql?style=for-the-badge&logo=sentry&color=green) | | sentry-graphql-core | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-core?style=for-the-badge&logo=sentry&color=green) | | sentry-graphql-22 | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-graphql-22?style=for-the-badge&logo=sentry&color=green) | +| sentry-jcache | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-jcache?style=for-the-badge&logo=sentry&color=green) | | sentry-quartz | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-quartz?style=for-the-badge&logo=sentry&color=green) | | sentry-openfeign | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeign?style=for-the-badge&logo=sentry&color=green) | | sentry-openfeature | ![Maven Central Version](https://img.shields.io/maven-central/v/io.sentry/sentry-openfeature?style=for-the-badge&logo=sentry&color=green) | diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 72892df5a9a..b5d1dafeb74 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -77,6 +77,7 @@ object Config { val SENTRY_GRAPHQL_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql" val SENTRY_GRAPHQL_CORE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql-core" val SENTRY_GRAPHQL22_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql22" + val SENTRY_JCACHE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jcache" val SENTRY_QUARTZ_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.quartz" val SENTRY_JDBC_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jdbc" val SENTRY_OPENFEATURE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.openfeature" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e1bee1c8eba..1e57b961fda 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -145,6 +145,7 @@ otel-semconv = { module = "io.opentelemetry.semconv:opentelemetry-semconv", vers otel-semconv-incubating = { module = "io.opentelemetry.semconv:opentelemetry-semconv-incubating", version.ref = "otelSemanticConventionsAlpha" } p6spy = { module = "p6spy:p6spy", version = "3.9.1" } epitaph = { module = "com.abovevacant:epitaph", version = "0.1.0" } +jcache = { module = "javax.cache:cache-api", version = "1.1.1" } quartz = { module = "org.quartz-scheduler:quartz", version = "2.3.0" } reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } diff --git a/sentry-jcache/README.md b/sentry-jcache/README.md new file mode 100644 index 00000000000..071950a7a2c --- /dev/null +++ b/sentry-jcache/README.md @@ -0,0 +1,13 @@ +# sentry-jcache + +This module provides an integration for JCache (JSR-107). + +JCache is a standard API — you need a provider implementation at runtime. Common implementations include: + +- [Caffeine](https://github.com/ben-manes/caffeine) (via `com.github.ben-manes.caffeine:jcache`) +- [Ehcache 3](https://www.ehcache.org/) (via `org.ehcache:ehcache`) +- [Hazelcast](https://hazelcast.com/) +- [Apache Ignite](https://ignite.apache.org/) +- [Infinispan](https://infinispan.org/) + +Please consult the documentation on how to install and use this integration in the Sentry Docs for [Java](https://docs.sentry.io/platforms/java/tracing/instrumentation/jcache/). diff --git a/sentry-jcache/api/sentry-jcache.api b/sentry-jcache/api/sentry-jcache.api new file mode 100644 index 00000000000..61f1031267f --- /dev/null +++ b/sentry-jcache/api/sentry-jcache.api @@ -0,0 +1,37 @@ +public final class io/sentry/jcache/BuildConfig { + public static final field SENTRY_JCACHE_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/jcache/SentryJCacheWrapper : javax/cache/Cache { + public fun (Ljavax/cache/Cache;Lio/sentry/IScopes;)V + public fun clear ()V + public fun close ()V + public fun containsKey (Ljava/lang/Object;)Z + public fun deregisterCacheEntryListener (Ljavax/cache/configuration/CacheEntryListenerConfiguration;)V + public fun get (Ljava/lang/Object;)Ljava/lang/Object; + public fun getAll (Ljava/util/Set;)Ljava/util/Map; + public fun getAndPut (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun getAndRemove (Ljava/lang/Object;)Ljava/lang/Object; + public fun getAndReplace (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun getCacheManager ()Ljavax/cache/CacheManager; + public fun getConfiguration (Ljava/lang/Class;)Ljavax/cache/configuration/Configuration; + public fun getName ()Ljava/lang/String; + public fun invoke (Ljava/lang/Object;Ljavax/cache/processor/EntryProcessor;[Ljava/lang/Object;)Ljava/lang/Object; + public fun invokeAll (Ljava/util/Set;Ljavax/cache/processor/EntryProcessor;[Ljava/lang/Object;)Ljava/util/Map; + public fun isClosed ()Z + public fun iterator ()Ljava/util/Iterator; + public fun loadAll (Ljava/util/Set;ZLjavax/cache/integration/CompletionListener;)V + public fun put (Ljava/lang/Object;Ljava/lang/Object;)V + public fun putAll (Ljava/util/Map;)V + public fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Z + public fun registerCacheEntryListener (Ljavax/cache/configuration/CacheEntryListenerConfiguration;)V + public fun remove (Ljava/lang/Object;)Z + public fun remove (Ljava/lang/Object;Ljava/lang/Object;)Z + public fun removeAll ()V + public fun removeAll (Ljava/util/Set;)V + public fun replace (Ljava/lang/Object;Ljava/lang/Object;)Z + public fun replace (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Z + public fun unwrap (Ljava/lang/Class;)Ljava/lang/Object; +} + diff --git a/sentry-jcache/build.gradle.kts b/sentry-jcache/build.gradle.kts new file mode 100644 index 00000000000..a9393a7d905 --- /dev/null +++ b/sentry-jcache/build.gradle.kts @@ -0,0 +1,90 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + id("io.sentry.javadoc") + alias(libs.plugins.kotlin.jvm) + jacoco + alias(libs.plugins.errorprone) + alias(libs.plugins.gradle.versions) + alias(libs.plugins.buildconfig) +} + +tasks.withType().configureEach { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 +} + +dependencies { + api(projects.sentry) + compileOnly(libs.jcache) + + compileOnly(libs.jetbrains.annotations) + compileOnly(libs.nopen.annotations) + errorprone(libs.errorprone.core) + errorprone(libs.nopen.checker) + errorprone(libs.nullaway) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(libs.jcache) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) +} + +configure { test { java.srcDir("src/test/java") } } + +jacoco { toolVersion = libs.versions.jacoco.get() } + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.jcache") + buildConfigField( + "String", + "SENTRY_JCACHE_SDK_NAME", + "\"${Config.Sentry.SENTRY_JCACHE_SDK_NAME}\"", + ) + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} + +tasks.jar { + manifest { + attributes( + "Sentry-Version-Name" to project.version, + "Sentry-SDK-Name" to Config.Sentry.SENTRY_JCACHE_SDK_NAME, + "Sentry-SDK-Package-Name" to "maven:io.sentry:sentry-jcache", + "Implementation-Vendor" to "Sentry", + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + ) + } +} diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java new file mode 100644 index 00000000000..da11872f0b8 --- /dev/null +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -0,0 +1,476 @@ +package io.sentry.jcache; + +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.SpanDataConvention; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.CacheEntryListenerConfiguration; +import javax.cache.configuration.Configuration; +import javax.cache.integration.CompletionListener; +import javax.cache.processor.EntryProcessor; +import javax.cache.processor.EntryProcessorException; +import javax.cache.processor.EntryProcessorResult; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Wraps a JCache {@link Cache} to create Sentry spans for cache operations. + * + * @param the type of key + * @param the type of value + */ +@ApiStatus.Experimental +public final class SentryJCacheWrapper implements Cache { + + private static final String TRACE_ORIGIN = "auto.cache.jcache"; + + private final @NotNull Cache delegate; + private final @NotNull IScopes scopes; + + public SentryJCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes scopes) { + this.delegate = delegate; + this.scopes = scopes; + } + + // -- read operations -- + + @Override + public V get(final K key) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.get(key); + } + try { + final V result = delegate.get(key); + span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public Map getAll(final Set keys) { + final ISpan span = startSpanForKeys("cache.get", keys); + if (span == null) { + return delegate.getAll(keys); + } + try { + final Map result = delegate.getAll(keys); + span.setData(SpanDataConvention.CACHE_HIT_KEY, !result.isEmpty()); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public boolean containsKey(final K key) { + return delegate.containsKey(key); + } + + // -- write operations -- + + @Override + public void put(final K key, final V value) { + final ISpan span = startSpan("cache.put", key); + if (span == null) { + delegate.put(key, value); + return; + } + try { + delegate.put(key, value); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public V getAndPut(final K key, final V value) { + final ISpan span = startSpan("cache.put", key); + if (span == null) { + return delegate.getAndPut(key, value); + } + try { + final V result = delegate.getAndPut(key, value); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public void putAll(final Map map) { + final ISpan span = startSpanForKeys("cache.put", map.keySet()); + if (span == null) { + delegate.putAll(map); + return; + } + try { + delegate.putAll(map); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + // putIfAbsent is not instrumented — we cannot know ahead of time whether the put + // will actually happen, and emitting a cache.put span for a no-op would be misleading. + @Override + public boolean putIfAbsent(final K key, final V value) { + return delegate.putIfAbsent(key, value); + } + + @Override + public boolean replace(final K key, final V oldValue, final V newValue) { + final ISpan span = startSpan("cache.put", key); + if (span == null) { + return delegate.replace(key, oldValue, newValue); + } + try { + final boolean result = delegate.replace(key, oldValue, newValue); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public boolean replace(final K key, final V value) { + final ISpan span = startSpan("cache.put", key); + if (span == null) { + return delegate.replace(key, value); + } + try { + final boolean result = delegate.replace(key, value); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public V getAndReplace(final K key, final V value) { + final ISpan span = startSpan("cache.put", key); + if (span == null) { + return delegate.getAndReplace(key, value); + } + try { + final V result = delegate.getAndReplace(key, value); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + // -- remove operations -- + + @Override + public boolean remove(final K key) { + final ISpan span = startSpan("cache.remove", key); + if (span == null) { + return delegate.remove(key); + } + try { + final boolean result = delegate.remove(key); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public boolean remove(final K key, final V oldValue) { + final ISpan span = startSpan("cache.remove", key); + if (span == null) { + return delegate.remove(key, oldValue); + } + try { + final boolean result = delegate.remove(key, oldValue); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public V getAndRemove(final K key) { + final ISpan span = startSpan("cache.remove", key); + if (span == null) { + return delegate.getAndRemove(key); + } + try { + final V result = delegate.getAndRemove(key); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public void removeAll(final Set keys) { + final ISpan span = startSpanForKeys("cache.remove", keys); + if (span == null) { + delegate.removeAll(keys); + return; + } + try { + delegate.removeAll(keys); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public void removeAll() { + final ISpan span = startSpan("cache.remove", null); + if (span == null) { + delegate.removeAll(); + return; + } + try { + delegate.removeAll(); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + // -- flush operations -- + + @Override + public void clear() { + final ISpan span = startSpan("cache.flush", null); + if (span == null) { + delegate.clear(); + return; + } + try { + delegate.clear(); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public void close() { + delegate.close(); + } + + // -- entry processor operations -- + + @Override + public T invoke( + final K key, final EntryProcessor entryProcessor, final Object... arguments) + throws EntryProcessorException { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.invoke(key, entryProcessor, arguments); + } + try { + final T result = delegate.invoke(key, entryProcessor, arguments); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public Map> invokeAll( + final Set keys, + final EntryProcessor entryProcessor, + final Object... arguments) { + final ISpan span = startSpanForKeys("cache.get", keys); + if (span == null) { + return delegate.invokeAll(keys, entryProcessor, arguments); + } + try { + final Map> result = + delegate.invokeAll(keys, entryProcessor, arguments); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + // -- passthrough operations -- + + @Override + public void loadAll( + final Set keys, + final boolean replaceExistingValues, + final CompletionListener completionListener) { + delegate.loadAll(keys, replaceExistingValues, completionListener); + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public CacheManager getCacheManager() { + return delegate.getCacheManager(); + } + + @Override + public > C getConfiguration(final Class clazz) { + return delegate.getConfiguration(clazz); + } + + @Override + public boolean isClosed() { + return delegate.isClosed(); + } + + @Override + public T unwrap(final Class clazz) { + return delegate.unwrap(clazz); + } + + @Override + public void registerCacheEntryListener( + final CacheEntryListenerConfiguration cacheEntryListenerConfiguration) { + delegate.registerCacheEntryListener(cacheEntryListenerConfiguration); + } + + @Override + public void deregisterCacheEntryListener( + final CacheEntryListenerConfiguration cacheEntryListenerConfiguration) { + delegate.deregisterCacheEntryListener(cacheEntryListenerConfiguration); + } + + @Override + public Iterator> iterator() { + return delegate.iterator(); + } + + // -- span helpers -- + + private @Nullable ISpan startSpan(final @NotNull String operation, final @Nullable Object key) { + if (!scopes.getOptions().isEnableCacheTracing()) { + return null; + } + + final ISpan activeSpan = scopes.getSpan(); + if (activeSpan == null || activeSpan.isNoOp()) { + return null; + } + + final SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(TRACE_ORIGIN); + final String keyString = key != null ? String.valueOf(key) : null; + final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + if (keyString != null) { + span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); + } + return span; + } + + private @Nullable ISpan startSpanForKeys( + final @NotNull String operation, final @NotNull Set keys) { + if (!scopes.getOptions().isEnableCacheTracing()) { + return null; + } + + final ISpan activeSpan = scopes.getSpan(); + if (activeSpan == null || activeSpan.isNoOp()) { + return null; + } + + final SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(TRACE_ORIGIN); + final ISpan span = activeSpan.startChild(operation, delegate.getName(), spanOptions); + span.setData( + SpanDataConvention.CACHE_KEY_KEY, + keys.stream().map(String::valueOf).collect(Collectors.toList())); + return span; + } +} diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt new file mode 100644 index 00000000000..218cbc37d00 --- /dev/null +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -0,0 +1,493 @@ +package io.sentry.jcache + +import io.sentry.IScopes +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanDataConvention +import io.sentry.SpanStatus +import io.sentry.TransactionContext +import javax.cache.Cache +import javax.cache.CacheManager +import javax.cache.configuration.CacheEntryListenerConfiguration +import javax.cache.configuration.Configuration +import javax.cache.integration.CompletionListener +import javax.cache.processor.EntryProcessor +import javax.cache.processor.EntryProcessorResult +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class SentryJCacheWrapperTest { + + private lateinit var scopes: IScopes + private lateinit var delegate: Cache + private lateinit var options: SentryOptions + + @BeforeTest + fun setup() { + scopes = mock() + delegate = mock() + options = SentryOptions().apply { isEnableCacheTracing = true } + whenever(scopes.options).thenReturn(options) + whenever(delegate.name).thenReturn("testCache") + } + + private fun createTransaction(): SentryTracer { + val tx = SentryTracer(TransactionContext("tx", "op"), scopes) + whenever(scopes.span).thenReturn(tx) + return tx + } + + // -- get(K key) -- + + @Test + fun `get creates span with cache hit true on hit`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn("value") + + val result = wrapper.get("myKey") + + assertEquals("value", result) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.get", span.operation) + assertEquals("myKey", span.description) + assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("auto.cache.jcache", span.spanContext.origin) + } + + @Test + fun `get creates span with cache hit false on miss`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + val result = wrapper.get("myKey") + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- getAll -- + + @Test + fun `getAll creates span with cache hit true when results exist`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + val keys = setOf("k1", "k2") + whenever(delegate.getAll(keys)).thenReturn(mapOf("k1" to "v1")) + + val result = wrapper.getAll(keys) + + assertEquals(mapOf("k1" to "v1"), result) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.get", span.operation) + assertEquals("testCache", span.description) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> + assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) + } + + @Test + fun `getAll creates span with cache hit false when empty`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + val keys = setOf("k1") + whenever(delegate.getAll(keys)).thenReturn(emptyMap()) + + wrapper.getAll(keys) + + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- put -- + + @Test + fun `put creates cache put span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + + wrapper.put("myKey", "myValue") + + verify(delegate).put("myKey", "myValue") + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + } + + // -- getAndPut -- + + @Test + fun `getAndPut creates cache put span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.getAndPut("myKey", "newValue")).thenReturn("oldValue") + + val result = wrapper.getAndPut("myKey", "newValue") + + assertEquals("oldValue", result) + assertEquals(1, tx.spans.size) + assertEquals("cache.put", tx.spans.first().operation) + } + + // -- putAll -- + + @Test + fun `putAll creates cache put span with all keys`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + val entries = mapOf("k1" to "v1", "k2" to "v2") + + wrapper.putAll(entries) + + verify(delegate).putAll(entries) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals("testCache", span.description) + val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> + assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) + } + + // -- putIfAbsent -- + + @Test + fun `putIfAbsent delegates without creating span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(true) + + val result = wrapper.putIfAbsent("myKey", "myValue") + + assertTrue(result) + verify(delegate).putIfAbsent("myKey", "myValue") + assertEquals(0, tx.spans.size) + } + + // -- replace(K, V, V) -- + + @Test + fun `replace with old value creates cache put span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.replace("myKey", "old", "new")).thenReturn(true) + + val result = wrapper.replace("myKey", "old", "new") + + assertTrue(result) + assertEquals(1, tx.spans.size) + assertEquals("cache.put", tx.spans.first().operation) + } + + // -- replace(K, V) -- + + @Test + fun `replace creates cache put span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.replace("myKey", "value")).thenReturn(true) + + val result = wrapper.replace("myKey", "value") + + assertTrue(result) + assertEquals(1, tx.spans.size) + assertEquals("cache.put", tx.spans.first().operation) + } + + // -- getAndReplace -- + + @Test + fun `getAndReplace creates cache put span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.getAndReplace("myKey", "newValue")).thenReturn("oldValue") + + val result = wrapper.getAndReplace("myKey", "newValue") + + assertEquals("oldValue", result) + assertEquals(1, tx.spans.size) + assertEquals("cache.put", tx.spans.first().operation) + } + + // -- remove(K) -- + + @Test + fun `remove creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.remove("myKey")).thenReturn(true) + + val result = wrapper.remove("myKey") + + assertTrue(result) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.remove", span.operation) + assertEquals(SpanStatus.OK, span.status) + } + + // -- remove(K, V) -- + + @Test + fun `remove with value creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.remove("myKey", "myValue")).thenReturn(true) + + val result = wrapper.remove("myKey", "myValue") + + assertTrue(result) + assertEquals(1, tx.spans.size) + assertEquals("cache.remove", tx.spans.first().operation) + } + + // -- getAndRemove -- + + @Test + fun `getAndRemove creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.getAndRemove("myKey")).thenReturn("value") + + val result = wrapper.getAndRemove("myKey") + + assertEquals("value", result) + assertEquals(1, tx.spans.size) + assertEquals("cache.remove", tx.spans.first().operation) + } + + // -- removeAll(Set) -- + + @Test + fun `removeAll with keys creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + val keys = setOf("k1", "k2") + + wrapper.removeAll(keys) + + verify(delegate).removeAll(keys) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.remove", span.operation) + assertEquals("testCache", span.description) + } + + // -- removeAll() -- + + @Test + fun `removeAll creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + + wrapper.removeAll() + + verify(delegate).removeAll() + assertEquals(1, tx.spans.size) + assertEquals("cache.remove", tx.spans.first().operation) + } + + // -- clear -- + + @Test + fun `clear creates cache flush span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + + wrapper.clear() + + verify(delegate).clear() + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.flush", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) + } + + // -- invoke -- + + @Test + fun `invoke creates cache get span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + val processor = mock>() + whenever(delegate.invoke("myKey", processor)).thenReturn("result") + + val result = wrapper.invoke("myKey", processor) + + assertEquals("result", result) + assertEquals(1, tx.spans.size) + assertEquals("cache.get", tx.spans.first().operation) + } + + // -- invokeAll -- + + @Test + fun `invokeAll creates cache get span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + val processor = mock>() + val keys = setOf("k1", "k2") + val resultMap = mock>>() + whenever(delegate.invokeAll(keys, processor)).thenReturn(resultMap) + + val result = wrapper.invokeAll(keys, processor) + + assertEquals(resultMap, result) + assertEquals(1, tx.spans.size) + assertEquals("cache.get", tx.spans.first().operation) + } + + // -- passthrough operations -- + + @Test + fun `containsKey delegates without creating span`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.containsKey("myKey")).thenReturn(true) + + assertTrue(wrapper.containsKey("myKey")) + assertEquals(0, tx.spans.size) + } + + @Test + fun `getName delegates to underlying cache`() { + val wrapper = SentryJCacheWrapper(delegate, scopes) + assertEquals("testCache", wrapper.name) + } + + @Test + fun `getCacheManager delegates to underlying cache`() { + val manager = mock() + whenever(delegate.cacheManager).thenReturn(manager) + val wrapper = SentryJCacheWrapper(delegate, scopes) + assertEquals(manager, wrapper.cacheManager) + } + + @Test + fun `isClosed delegates to underlying cache`() { + whenever(delegate.isClosed).thenReturn(false) + val wrapper = SentryJCacheWrapper(delegate, scopes) + assertFalse(wrapper.isClosed) + } + + @Test + fun `close delegates to underlying cache`() { + val wrapper = SentryJCacheWrapper(delegate, scopes) + wrapper.close() + verify(delegate).close() + } + + @Test + fun `registerCacheEntryListener delegates to underlying cache`() { + val wrapper = SentryJCacheWrapper(delegate, scopes) + val config = mock>() + wrapper.registerCacheEntryListener(config) + verify(delegate).registerCacheEntryListener(config) + } + + @Test + fun `deregisterCacheEntryListener delegates to underlying cache`() { + val wrapper = SentryJCacheWrapper(delegate, scopes) + val config = mock>() + wrapper.deregisterCacheEntryListener(config) + verify(delegate).deregisterCacheEntryListener(config) + } + + @Test + fun `iterator delegates to underlying cache`() { + val iter = mock>>() + whenever(delegate.iterator()).thenReturn(iter) + val wrapper = SentryJCacheWrapper(delegate, scopes) + assertEquals(iter, wrapper.iterator()) + } + + @Test + fun `loadAll delegates to underlying cache`() { + val wrapper = SentryJCacheWrapper(delegate, scopes) + val keys = setOf("k1") + val listener = mock() + wrapper.loadAll(keys, true, listener) + verify(delegate).loadAll(keys, true, listener) + } + + @Test + fun `getConfiguration delegates to underlying cache`() { + val config = mock>() + whenever( + delegate.getConfiguration(Configuration::class.java as Class>) + ) + .thenReturn(config) + val wrapper = SentryJCacheWrapper(delegate, scopes) + assertEquals( + config, + wrapper.getConfiguration(Configuration::class.java as Class>), + ) + } + + @Test + fun `unwrap delegates to underlying cache`() { + whenever(delegate.unwrap(String::class.java)).thenReturn("unwrapped") + val wrapper = SentryJCacheWrapper(delegate, scopes) + assertEquals("unwrapped", wrapper.unwrap(String::class.java)) + } + + // -- no span when no active transaction -- + + @Test + fun `does not create span when there is no active transaction`() { + whenever(scopes.span).thenReturn(null) + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + wrapper.get("myKey") + + verify(delegate).get("myKey") + } + + // -- no span when option is disabled -- + + @Test + fun `does not create span when enableCacheTracing is false`() { + options.isEnableCacheTracing = false + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + wrapper.get("myKey") + + verify(delegate).get("myKey") + assertEquals(0, tx.spans.size) + } + + // -- error handling -- + + @Test + fun `sets error status and throwable on exception`() { + val tx = createTransaction() + val wrapper = SentryJCacheWrapper(delegate, scopes) + val exception = RuntimeException("cache error") + whenever(delegate.get("myKey")).thenThrow(exception) + + assertFailsWith { wrapper.get("myKey") } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0e9987b4ae4..8d431d5fbdf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -66,6 +66,7 @@ include( "sentry-opentelemetry:sentry-opentelemetry-agentless-spring", "sentry-opentelemetry:sentry-opentelemetry-otlp", "sentry-opentelemetry:sentry-opentelemetry-otlp-spring", + "sentry-jcache", "sentry-quartz", "sentry-okhttp", "sentry-openfeature", From 4b6759ded1fc2f2ad8db8806cbc715e2d264b691 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 9 Mar 2026 16:27:43 +0100 Subject: [PATCH 12/48] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 586ea5b2736..05b201703c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add JCache (JSR-107) cache tracing via new `sentry-jcache` module ([#5179](https://github.com/getsentry/sentry-java/pull/5179)) + ## 8.34.1 ### Fixes From 93be5c8b96c0fb09d544d4bd9a4f944d894e44e9 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 06:07:43 +0100 Subject: [PATCH 13/48] fix(jcache): Make replace and getAndReplace passthrough (no span) Like putIfAbsent, these are conditional writes that may be no-ops. Emitting a cache.put span for them would be misleading. --- .../io/sentry/jcache/SentryJCacheWrapper.java | 51 +++---------------- .../sentry/jcache/SentryJCacheWrapperTest.kt | 24 ++++----- 2 files changed, 17 insertions(+), 58 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index da11872f0b8..a6e2e54b1de 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -154,61 +154,22 @@ public boolean putIfAbsent(final K key, final V value) { return delegate.putIfAbsent(key, value); } + // replace and getAndReplace are not instrumented — like putIfAbsent, they are conditional + // writes (only happen if the key exists / value matches). Emitting a cache.put span for a + // potential no-op would be misleading. @Override public boolean replace(final K key, final V oldValue, final V newValue) { - final ISpan span = startSpan("cache.put", key); - if (span == null) { - return delegate.replace(key, oldValue, newValue); - } - try { - final boolean result = delegate.replace(key, oldValue, newValue); - span.setStatus(SpanStatus.OK); - return result; - } catch (Throwable e) { - span.setStatus(SpanStatus.INTERNAL_ERROR); - span.setThrowable(e); - throw e; - } finally { - span.finish(); - } + return delegate.replace(key, oldValue, newValue); } @Override public boolean replace(final K key, final V value) { - final ISpan span = startSpan("cache.put", key); - if (span == null) { - return delegate.replace(key, value); - } - try { - final boolean result = delegate.replace(key, value); - span.setStatus(SpanStatus.OK); - return result; - } catch (Throwable e) { - span.setStatus(SpanStatus.INTERNAL_ERROR); - span.setThrowable(e); - throw e; - } finally { - span.finish(); - } + return delegate.replace(key, value); } @Override public V getAndReplace(final K key, final V value) { - final ISpan span = startSpan("cache.put", key); - if (span == null) { - return delegate.getAndReplace(key, value); - } - try { - final V result = delegate.getAndReplace(key, value); - span.setStatus(SpanStatus.OK); - return result; - } catch (Throwable e) { - span.setStatus(SpanStatus.INTERNAL_ERROR); - span.setThrowable(e); - throw e; - } finally { - span.finish(); - } + return delegate.getAndReplace(key, value); } // -- remove operations -- diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt index 218cbc37d00..ae560cc39f6 100644 --- a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -178,10 +178,10 @@ class SentryJCacheWrapperTest { assertEquals(0, tx.spans.size) } - // -- replace(K, V, V) -- + // -- replace (passthrough, no span — conditional write) -- @Test - fun `replace with old value creates cache put span`() { + fun `replace with old value delegates without creating span`() { val tx = createTransaction() val wrapper = SentryJCacheWrapper(delegate, scopes) whenever(delegate.replace("myKey", "old", "new")).thenReturn(true) @@ -189,14 +189,12 @@ class SentryJCacheWrapperTest { val result = wrapper.replace("myKey", "old", "new") assertTrue(result) - assertEquals(1, tx.spans.size) - assertEquals("cache.put", tx.spans.first().operation) + verify(delegate).replace("myKey", "old", "new") + assertEquals(0, tx.spans.size) } - // -- replace(K, V) -- - @Test - fun `replace creates cache put span`() { + fun `replace delegates without creating span`() { val tx = createTransaction() val wrapper = SentryJCacheWrapper(delegate, scopes) whenever(delegate.replace("myKey", "value")).thenReturn(true) @@ -204,14 +202,14 @@ class SentryJCacheWrapperTest { val result = wrapper.replace("myKey", "value") assertTrue(result) - assertEquals(1, tx.spans.size) - assertEquals("cache.put", tx.spans.first().operation) + verify(delegate).replace("myKey", "value") + assertEquals(0, tx.spans.size) } - // -- getAndReplace -- + // -- getAndReplace (passthrough, no span — conditional write) -- @Test - fun `getAndReplace creates cache put span`() { + fun `getAndReplace delegates without creating span`() { val tx = createTransaction() val wrapper = SentryJCacheWrapper(delegate, scopes) whenever(delegate.getAndReplace("myKey", "newValue")).thenReturn("oldValue") @@ -219,8 +217,8 @@ class SentryJCacheWrapperTest { val result = wrapper.getAndReplace("myKey", "newValue") assertEquals("oldValue", result) - assertEquals(1, tx.spans.size) - assertEquals("cache.put", tx.spans.first().operation) + verify(delegate).getAndReplace("myKey", "newValue") + assertEquals(0, tx.spans.size) } // -- remove(K) -- From 038e7926d53f817dc41683827684f7f632e0b3f8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 06:19:06 +0100 Subject: [PATCH 14/48] fix(jcache): Check for NoOp span after startChild startChild can return a NoOp span (e.g. when span limit is reached). Skip instrumentation in that case to avoid unnecessary work. --- .../src/main/java/io/sentry/jcache/SentryJCacheWrapper.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index a6e2e54b1de..8b46edf390b 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -409,6 +409,9 @@ public Iterator> iterator() { spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + if (span.isNoOp()) { + return null; + } if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } @@ -429,6 +432,9 @@ public Iterator> iterator() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final ISpan span = activeSpan.startChild(operation, delegate.getName(), spanOptions); + if (span.isNoOp()) { + return null; + } span.setData( SpanDataConvention.CACHE_KEY_KEY, keys.stream().map(String::valueOf).collect(Collectors.toList())); From 0d4de4b478193e56ac2085e69277d0865e24fe01 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 06:27:13 +0100 Subject: [PATCH 15/48] fix(jcache): Use cache.flush for removeAll() without keys removeAll() with no args removes all entries, which is semantically equivalent to clear(). Use cache.flush instead of cache.remove. The keyed removeAll(Set) remains cache.remove. --- .../src/main/java/io/sentry/jcache/SentryJCacheWrapper.java | 2 +- .../test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 8b46edf390b..61c829726f6 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -252,7 +252,7 @@ public void removeAll(final Set keys) { @Override public void removeAll() { - final ISpan span = startSpan("cache.remove", null); + final ISpan span = startSpan("cache.flush", null); if (span == null) { delegate.removeAll(); return; diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt index ae560cc39f6..375e0f0ca54 100644 --- a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -288,7 +288,7 @@ class SentryJCacheWrapperTest { // -- removeAll() -- @Test - fun `removeAll creates cache remove span`() { + fun `removeAll without keys creates cache flush span`() { val tx = createTransaction() val wrapper = SentryJCacheWrapper(delegate, scopes) @@ -296,7 +296,7 @@ class SentryJCacheWrapperTest { verify(delegate).removeAll() assertEquals(1, tx.spans.size) - assertEquals("cache.remove", tx.spans.first().operation) + assertEquals("cache.flush", tx.spans.first().operation) } // -- clear -- From 8434b4ebbcb318d00ac738e3296a427ea2408418 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 10:27:29 +0100 Subject: [PATCH 16/48] feat(samples): Add JCache cache tracing demo to console sample Co-Authored-By: Claude Opus 4.6 --- gradle/libs.versions.toml | 1 + .../sentry-samples-console/build.gradle.kts | 3 ++ .../java/io/sentry/samples/console/Main.java | 50 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1e57b961fda..54731df1f24 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,6 +100,7 @@ androidx-browser = { module = "androidx.browser:browser", version = "1.8.0" } async-profiler = { module = "tools.profiler:async-profiler", version.ref = "asyncProfiler" } async-profiler-jfr-converter = { module = "tools.profiler:jfr-converter", version.ref = "asyncProfiler" } caffeine = { module = "com.github.ben-manes.caffeine:caffeine" } +caffeine-jcache = { module = "com.github.ben-manes.caffeine:jcache", version = "3.2.0" } coil-compose = { module = "io.coil-kt:coil-compose", version = "2.6.0" } commons-compress = {module = "org.apache.commons:commons-compress", version = "1.25.0"} context-propagation = { module = "io.micrometer:context-propagation", version = "1.1.0" } diff --git a/sentry-samples/sentry-samples-console/build.gradle.kts b/sentry-samples/sentry-samples-console/build.gradle.kts index 5737e8effe0..0dc6183b4fc 100644 --- a/sentry-samples/sentry-samples-console/build.gradle.kts +++ b/sentry-samples/sentry-samples-console/build.gradle.kts @@ -35,6 +35,9 @@ tasks.withType().configureEach { dependencies { implementation(projects.sentry) implementation(projects.sentryAsyncProfiler) + implementation(projects.sentryJcache) + implementation(libs.jcache) + implementation(libs.caffeine.jcache) testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(projects.sentry) diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index fd21476f402..15888cb5702 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -2,9 +2,14 @@ import io.sentry.*; import io.sentry.clientreport.DiscardReason; +import io.sentry.jcache.SentryJCacheWrapper; import io.sentry.protocol.Message; import io.sentry.protocol.User; import java.util.Collections; +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.Caching; +import javax.cache.configuration.MutableConfiguration; public class Main { @@ -88,6 +93,9 @@ public static void main(String[] args) throws InterruptedException { // Set what percentage of traces should be collected options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces + // Enable cache tracing to create spans for cache operations + options.setEnableCacheTracing(true); + // Determine traces sample rate based on the sampling context // options.setTracesSampler( // context -> { @@ -162,6 +170,12 @@ public static void main(String[] args) throws InterruptedException { Sentry.captureEvent(event, hint); } + // Cache tracing with JCache (JSR-107) + // + // Wrapping a JCache Cache with SentryJCacheWrapper creates cache.get, cache.put, + // cache.remove, and cache.flush spans as children of the active transaction. + demonstrateCacheTracing(); + // Performance feature // // Transactions collect execution time of the piece of code that's executed between the start @@ -189,6 +203,42 @@ public static void main(String[] args) throws InterruptedException { // Sentry.close(); } + private static void demonstrateCacheTracing() { + // Create a JCache CacheManager and Cache using standard JSR-107 API + CacheManager cacheManager = Caching.getCachingProvider().getCacheManager(); + MutableConfiguration config = + new MutableConfiguration().setTypes(String.class, String.class); + Cache rawCache = cacheManager.createCache("myCache", config); + + // Wrap with SentryJCacheWrapper to enable cache tracing + Cache cache = new SentryJCacheWrapper<>(rawCache, Sentry.getCurrentScopes()); + + // All cache operations inside a transaction produce child spans + ITransaction transaction = Sentry.startTransaction("cache-demo", "demo"); + try (ISentryLifecycleToken ignored = transaction.makeCurrent()) { + // cache.put span + cache.put("greeting", "hello"); + + // cache.get span (hit — returns "hello", cache.hit = true) + cache.get("greeting"); + + // cache.get span (miss — returns null, cache.hit = false) + cache.get("nonexistent"); + + // cache.remove span + cache.remove("greeting"); + + // cache.flush span + cache.clear(); + } finally { + transaction.finish(); + } + + // Clean up + cacheManager.destroyCache("myCache"); + cacheManager.close(); + } + private static void captureMetrics() { Sentry.metrics().count("countMetric"); Sentry.metrics().gauge("gaugeMetric", 5.0); From 8be4571c7d19b52b52fef802eea259dc8f0cd27a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 13:03:44 +0100 Subject: [PATCH 17/48] changelog Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 586ea5b2736..73dd6cad1b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ - 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` + - Set `sentry.enable-cache-tracing` to `true` to enable this feature - 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. From 7453d6845621276018752f2ac72983ed611a2814 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 13:05:15 +0100 Subject: [PATCH 18/48] changelog Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0274ee6fec4..f589b7e2a2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Features - Add JCache (JSR-107) cache tracing via new `sentry-jcache` module ([#5179](https://github.com/getsentry/sentry-java/pull/5179)) + - Wraps JCache `Cache` with `SentryJCacheWrapper` to produce `cache.get`, `cache.put`, `cache.remove`, and `cache.flush` spans + - Set `sentry.enable-cache-tracing` to `true` to enable this feature ## 8.34.1 From 89a572d4f3d9ef90c3edfcebdec758d3d8d81cc6 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 13:45:01 +0100 Subject: [PATCH 19/48] fix(core): Use correct cache span op terminology in Javadoc Co-Authored-By: Claude Opus 4.6 --- sentry/src/main/java/io/sentry/SentryOptions.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index b8f1a067595..0d9782a144a 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -490,7 +490,7 @@ public class SentryOptions { /** Whether database transaction spans (BEGIN, COMMIT, ROLLBACK) should be traced. */ private boolean enableDatabaseTransactionTracing = false; - /** Whether cache operations (get, put, evict, clear) should be traced. */ + /** Whether cache operations (get, put, remove, flush) should be traced. */ private boolean enableCacheTracing = false; /** Date provider to retrieve the current date from. */ @@ -2634,7 +2634,7 @@ public void setEnableDatabaseTransactionTracing(boolean enableDatabaseTransactio } /** - * Whether cache operations (get, put, evict, clear) should be traced. + * Whether cache operations (get, put, remove, flush) should be traced. * * @return true if cache operations should be traced */ @@ -2643,7 +2643,7 @@ public boolean isEnableCacheTracing() { } /** - * Whether cache operations (get, put, evict, clear) should be traced. + * Whether cache operations (get, put, remove, flush) should be traced. * * @param enableCacheTracing true if cache operations should be traced */ From c03021ae88459dca83246c4aea1efeba1a8d149d Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 13:50:23 +0100 Subject: [PATCH 20/48] changelog --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73dd6cad1b6..ef46ab18c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### 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)) + - 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 + ## 8.34.1 ### Fixes @@ -22,9 +30,6 @@ - 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 - - Set `sentry.enable-cache-tracing` to `true` to enable this feature - 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. From 0b1b83af471e6929a21f8e72dafe7a83c2de3286 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 14:10:46 +0100 Subject: [PATCH 21/48] fix(spring7): Avoid double-wrapping caches in SentryCacheManagerWrapper Co-Authored-By: Claude Opus 4.6 --- .../spring7/cache/SentryCacheManagerWrapper.java | 4 ++-- .../spring7/cache/SentryCacheManagerWrapperTest.kt | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java index 5a52734756b..97ac313bd91 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheManagerWrapper.java @@ -24,8 +24,8 @@ public SentryCacheManagerWrapper( @Override public @Nullable Cache getCache(final @NotNull String name) { final Cache cache = delegate.getCache(name); - if (cache == null) { - return null; + if (cache == null || cache instanceof SentryCacheWrapper) { + return cache; } return new SentryCacheWrapper(cache, scopes); } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt index ca56be7fbc9..dbc5992b7e0 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheManagerWrapperTest.kt @@ -4,6 +4,7 @@ import io.sentry.IScopes import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull +import kotlin.test.assertSame import kotlin.test.assertTrue import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -36,6 +37,18 @@ class SentryCacheManagerWrapperTest { assertNull(result) } + @Test + fun `getCache does not double-wrap SentryCacheWrapper`() { + val innerCache = mock() + val alreadyWrapped = SentryCacheWrapper(innerCache, scopes) + whenever(delegate.getCache("test")).thenReturn(alreadyWrapped) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.getCache("test") + + assertSame(alreadyWrapped, result) + } + @Test fun `getCacheNames delegates to underlying cache manager`() { whenever(delegate.cacheNames).thenReturn(listOf("cache1", "cache2")) From 76a2ecd02691e1559b88d9d2cc85ae55952e0162 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 14:41:53 +0100 Subject: [PATCH 22/48] feat(samples): Add cache tracing to all Spring Boot 4 samples Co-Authored-By: Claude Opus 4.6 --- .../build.gradle.kts | 4 ++ .../samples/spring/boot4/CacheController.java | 34 +++++++++++++ .../spring/boot4/SentryDemoApplication.java | 2 + .../samples/spring/boot4/TodoService.java | 29 +++++++++++ .../src/main/resources/application.properties | 3 ++ .../io/sentry/systemtest/CacheSystemTest.kt | 51 +++++++++++++++++++ .../build.gradle.kts | 4 ++ .../spring/boot4/otlp/CacheController.java | 34 +++++++++++++ .../boot4/otlp/SentryDemoApplication.java | 2 + .../spring/boot4/otlp/TodoService.java | 29 +++++++++++ .../src/main/resources/application.properties | 3 ++ .../io/sentry/systemtest/CacheSystemTest.kt | 51 +++++++++++++++++++ .../build.gradle.kts | 4 ++ .../samples/spring/boot4/CacheController.java | 34 +++++++++++++ .../spring/boot4/SentryDemoApplication.java | 2 + .../samples/spring/boot4/TodoService.java | 29 +++++++++++ .../src/main/resources/application.properties | 3 ++ .../io/sentry/systemtest/CacheSystemTest.kt | 51 +++++++++++++++++++ 18 files changed, 369 insertions(+) create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/CacheController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/TodoService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/CacheController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/TodoService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-otlp/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/CacheController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/TodoService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts index ef38162d6bf..c3e8ba06fae 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts @@ -59,6 +59,10 @@ dependencies { implementation(projects.sentryAsyncProfiler) implementation(libs.otel) + // cache tracing + implementation(libs.springboot4.starter.cache) + implementation(libs.caffeine) + // database query tracing implementation(projects.sentryJdbc) runtimeOnly(libs.hsqldb) diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/CacheController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/CacheController.java new file mode 100644 index 00000000000..3c2e66442de --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/CacheController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot4; + +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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java index aa5ebce68cd..b00609ad9ac 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java @@ -11,6 +11,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.restclient.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; @@ -21,6 +22,7 @@ import org.springframework.web.reactive.function.client.WebClient; @SpringBootApplication +@EnableCaching @EnableScheduling public class SentryDemoApplication { public static void main(String[] args) { diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/TodoService.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/TodoService.java new file mode 100644 index 00000000000..c837ab8398a --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/TodoService.java @@ -0,0 +1,29 @@ +package io.sentry.samples.spring.boot4; + +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 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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/application.properties index 6b57706019b..a0808e04fde 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/application.properties @@ -20,6 +20,9 @@ sentry.in-app-includes="io.sentry.samples" sentry.profile-session-sample-rate=1.0 sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces sentry.profile-lifecycle=TRACE +sentry.enable-cache-tracing=true +spring.cache.cache-names=todos +spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s # Uncomment and set to true to enable aot compatibility # This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt new file mode 100644 index 00000000000..7853750a8fb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -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") + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-otlp/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4-otlp/build.gradle.kts index 4f3d64524fd..01e07fc2526 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-otlp/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4-otlp/build.gradle.kts @@ -58,6 +58,10 @@ dependencies { implementation(projects.sentryAsyncProfiler) implementation(projects.sentryOpentelemetry.sentryOpentelemetryOtlpSpring) + // cache tracing + implementation(libs.springboot4.starter.cache) + implementation(libs.caffeine) + // database query tracing implementation(projects.sentryJdbc) runtimeOnly(libs.hsqldb) diff --git a/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/CacheController.java b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/CacheController.java new file mode 100644 index 00000000000..f453f81187c --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/CacheController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot4.otlp; + +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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/SentryDemoApplication.java index b4c58c4882e..1bf622154d0 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/SentryDemoApplication.java @@ -12,6 +12,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.restclient.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; @@ -22,6 +23,7 @@ import org.springframework.web.reactive.function.client.WebClient; @SpringBootApplication +@EnableCaching @EnableScheduling public class SentryDemoApplication { public static void main(String[] args) { diff --git a/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/TodoService.java b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/TodoService.java new file mode 100644 index 00000000000..1a748a165a4 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/java/io/sentry/samples/spring/boot4/otlp/TodoService.java @@ -0,0 +1,29 @@ +package io.sentry.samples.spring.boot4.otlp; + +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 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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/resources/application.properties index 43c0bd18c08..f9b35099062 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/main/resources/application.properties @@ -20,6 +20,9 @@ sentry.logs.enabled=true sentry.profile-session-sample-rate=1.0 sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces sentry.profile-lifecycle=TRACE +sentry.enable-cache-tracing=true +spring.cache.cache-names=todos +spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s # Uncomment and set to true to enable aot compatibility # This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) diff --git a/sentry-samples/sentry-samples-spring-boot-4-otlp/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt new file mode 100644 index 00000000000..7853750a8fb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -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") + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts index b9986a31d02..cdcf65711a8 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts @@ -32,6 +32,10 @@ dependencies { implementation(libs.springboot4.starter.webflux) implementation(libs.springboot4.starter.webclient) + // cache tracing + implementation(libs.springboot4.starter.cache) + implementation(libs.caffeine) + testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(projects.sentrySystemTestSupport) testImplementation(libs.apollo3.kotlin) diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/CacheController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/CacheController.java new file mode 100644 index 00000000000..3c2e66442de --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/CacheController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot4; + +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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java index 72980871730..0d37be7634c 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java @@ -2,10 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.web.reactive.function.client.WebClient; @SpringBootApplication +@EnableCaching public class SentryDemoApplication { public static void main(String[] args) { SpringApplication.run(SentryDemoApplication.class, args); diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/TodoService.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/TodoService.java new file mode 100644 index 00000000000..c837ab8398a --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/TodoService.java @@ -0,0 +1,29 @@ +package io.sentry.samples.spring.boot4; + +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 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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/application.properties index 9e9d6596e08..9fc969efd28 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/application.properties @@ -15,3 +15,6 @@ sentry.enable-spotlight=true sentry.profile-session-sample-rate=1.0 sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces sentry.profile-lifecycle=TRACE +sentry.enable-cache-tracing=true +spring.cache.cache-names=todos +spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt new file mode 100644 index 00000000000..7853750a8fb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -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") + } + } +} From d797a2dd21a34c0753538d69e4b1d70861ce1152 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 15:23:49 +0100 Subject: [PATCH 23/48] fix(test): Add SENTRY_ENABLE_CACHE_TRACING env var to system test runner Co-Authored-By: Claude Opus 4.6 --- test/system-test-runner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 55a1136fbe0..70489c580a5 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -61,7 +61,8 @@ "OTEL_TRACES_EXPORTER": "none", "OTEL_METRICS_EXPORTER": "none", "OTEL_LOGS_EXPORTER": "none", - "SENTRY_LOGS_ENABLED": "true" + "SENTRY_LOGS_ENABLED": "true", + "SENTRY_ENABLE_CACHE_TRACING": "true" } class ServerType(Enum): From dbb2998737a725ea11e86619de85d2a4ee3d4ea5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 15:32:48 +0100 Subject: [PATCH 24/48] feat(jcache): Add SentryJCacheWrapper ctor that uses ScopesAdapter Co-Authored-By: Claude Opus 4.6 --- sentry-jcache/api/sentry-jcache.api | 1 + .../src/main/java/io/sentry/jcache/SentryJCacheWrapper.java | 5 +++++ .../src/main/java/io/sentry/samples/console/Main.java | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/sentry-jcache/api/sentry-jcache.api b/sentry-jcache/api/sentry-jcache.api index 61f1031267f..b834ba41064 100644 --- a/sentry-jcache/api/sentry-jcache.api +++ b/sentry-jcache/api/sentry-jcache.api @@ -4,6 +4,7 @@ public final class io/sentry/jcache/BuildConfig { } public final class io/sentry/jcache/SentryJCacheWrapper : javax/cache/Cache { + public fun (Ljavax/cache/Cache;)V public fun (Ljavax/cache/Cache;Lio/sentry/IScopes;)V public fun clear ()V public fun close ()V diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 61c829726f6..d5022a5455f 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -2,6 +2,7 @@ import io.sentry.IScopes; import io.sentry.ISpan; +import io.sentry.ScopesAdapter; import io.sentry.SpanDataConvention; import io.sentry.SpanOptions; import io.sentry.SpanStatus; @@ -36,6 +37,10 @@ public final class SentryJCacheWrapper implements Cache { private final @NotNull Cache delegate; private final @NotNull IScopes scopes; + public SentryJCacheWrapper(final @NotNull Cache delegate) { + this(delegate, ScopesAdapter.getInstance()); + } + public SentryJCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes scopes) { this.delegate = delegate; this.scopes = scopes; diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index 15888cb5702..6f0a554821e 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -211,7 +211,7 @@ private static void demonstrateCacheTracing() { Cache rawCache = cacheManager.createCache("myCache", config); // Wrap with SentryJCacheWrapper to enable cache tracing - Cache cache = new SentryJCacheWrapper<>(rawCache, Sentry.getCurrentScopes()); + Cache cache = new SentryJCacheWrapper<>(rawCache); // All cache operations inside a transaction produce child spans ITransaction transaction = Sentry.startTransaction("cache-demo", "demo"); From 41872ad7571c7f3f4efc99b3b613fcd46b581ed7 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 10 Mar 2026 16:13:44 +0100 Subject: [PATCH 25/48] feat(spring7): Add retrieve() overrides to SentryCacheWrapper Adds support for Spring 6.1+ async cache operations (CompletableFuture and Mono/Flux). Without these overrides, @Cacheable on reactive return types crashes with UnsupportedOperationException. Co-Authored-By: Claude Opus 4.6 --- sentry-spring-7/api/sentry-spring-7.api | 2 + .../spring7/cache/SentryCacheWrapper.java | 72 ++++++++ .../spring7/cache/SentryCacheWrapperTest.kt | 168 ++++++++++++++++++ 3 files changed, 242 insertions(+) diff --git a/sentry-spring-7/api/sentry-spring-7.api b/sentry-spring-7/api/sentry-spring-7.api index b37747cf765..71a8a022bf6 100644 --- a/sentry-spring-7/api/sentry-spring-7.api +++ b/sentry-spring-7/api/sentry-spring-7.api @@ -129,6 +129,8 @@ public final class io/sentry/spring7/cache/SentryCacheWrapper : org/springframew 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 fun retrieve (Ljava/lang/Object;)Ljava/util/concurrent/CompletableFuture; + public fun retrieve (Ljava/lang/Object;Ljava/util/function/Supplier;)Ljava/util/concurrent/CompletableFuture; } public abstract interface annotation class io/sentry/spring7/checkin/SentryCheckIn : java/lang/annotation/Annotation { diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 63e4ef67745..181e9f398da 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -7,7 +7,9 @@ import io.sentry.SpanStatus; import java.util.Arrays; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -104,6 +106,76 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes } } + @Override + public @Nullable CompletableFuture retrieve(final @NotNull Object key) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.retrieve(key); + } + final CompletableFuture result; + try { + result = delegate.retrieve(key); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + span.finish(); + throw e; + } + if (result == null) { + span.setData(SpanDataConvention.CACHE_HIT_KEY, false); + span.setStatus(SpanStatus.OK); + span.finish(); + return null; + } + return result.whenComplete( + (value, throwable) -> { + if (throwable != null) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(throwable); + } else { + span.setData(SpanDataConvention.CACHE_HIT_KEY, value != null); + span.setStatus(SpanStatus.OK); + } + span.finish(); + }); + } + + @Override + public CompletableFuture retrieve( + final @NotNull Object key, final @NotNull Supplier> valueLoader) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.retrieve(key, valueLoader); + } + final AtomicBoolean loaderInvoked = new AtomicBoolean(false); + final CompletableFuture result; + try { + result = + delegate.retrieve( + key, + () -> { + loaderInvoked.set(true); + return valueLoader.get(); + }); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + span.finish(); + throw e; + } + return result.whenComplete( + (value, throwable) -> { + if (throwable != null) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(throwable); + } else { + span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setStatus(SpanStatus.OK); + } + span.finish(); + }); + } + @Override public void put(final @NotNull Object key, final @Nullable Object value) { final ISpan span = startSpan("cache.put", key); diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index dbd3d50c2af..9e20fe21554 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -7,6 +7,8 @@ import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TransactionContext import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture +import java.util.function.Supplier import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -137,6 +139,172 @@ class SentryCacheWrapperTest { assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + // -- retrieve(Object key) -- + + @Test + fun `retrieve creates span with cache hit true when future resolves with value`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.retrieve("myKey")).thenReturn(CompletableFuture.completedFuture("value")) + + val result = wrapper.retrieve("myKey") + + assertEquals("value", result!!.get()) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.get", span.operation) + assertEquals("myKey", span.description) + assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertTrue(span.isFinished) + } + + @Test + fun `retrieve creates span with cache hit false when future resolves with null`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.retrieve("myKey")).thenReturn(CompletableFuture.completedFuture(null)) + + val result = wrapper.retrieve("myKey") + + assertNull(result!!.get()) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertTrue(tx.spans.first().isFinished) + } + + @Test + fun `retrieve creates span with cache hit false when delegate returns null`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.retrieve("myKey")).thenReturn(null) + + val result = wrapper.retrieve("myKey") + + assertNull(result) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(false, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(SpanStatus.OK, span.status) + assertTrue(span.isFinished) + } + + @Test + fun `retrieve sets error status when future completes exceptionally`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("async cache error") + whenever(delegate.retrieve("myKey")) + .thenReturn(CompletableFuture().also { it.completeExceptionally(exception) }) + + val result = wrapper.retrieve("myKey") + + assertFailsWith { result!!.get() } + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + assertTrue(span.isFinished) + } + + @Test + fun `retrieve sets error status when delegate throws synchronously`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("sync error") + whenever(delegate.retrieve("myKey")).thenThrow(exception) + + assertFailsWith { wrapper.retrieve("myKey") } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + assertTrue(span.isFinished) + } + + @Test + fun `retrieve does not create span when tracing is disabled`() { + options.isEnableCacheTracing = false + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.retrieve("myKey")).thenReturn(CompletableFuture.completedFuture("value")) + + wrapper.retrieve("myKey") + + verify(delegate).retrieve("myKey") + assertEquals(0, tx.spans.size) + } + + // -- retrieve(Object key, Supplier>) -- + + @Test + fun `retrieve with loader creates span with cache hit true when loader not invoked`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + // Simulate cache hit: delegate returns value without invoking the loader + whenever(delegate.retrieve(eq("myKey"), any>>())) + .thenReturn(CompletableFuture.completedFuture("cached")) + + val result = wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") } + + assertEquals("cached", result.get()) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertTrue(tx.spans.first().isFinished) + } + + @Test + fun `retrieve with loader creates span with cache hit false when loader invoked`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + // Simulate cache miss: delegate invokes the loader supplier + whenever(delegate.retrieve(eq("myKey"), any>>())) + .thenAnswer { invocation -> + val loader = invocation.getArgument>>(1) + loader.get() + } + + val result = wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") } + + assertEquals("loaded", result.get()) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertTrue(tx.spans.first().isFinished) + } + + @Test + fun `retrieve with loader sets error status when future completes exceptionally`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("async loader error") + whenever(delegate.retrieve(eq("myKey"), any>>())) + .thenReturn(CompletableFuture().also { it.completeExceptionally(exception) }) + + val result = wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") } + + assertFailsWith { result.get() } + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + assertTrue(span.isFinished) + } + + @Test + fun `retrieve with loader does not create span when tracing is disabled`() { + options.isEnableCacheTracing = false + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.retrieve(eq("myKey"), any>>())) + .thenReturn(CompletableFuture.completedFuture("cached")) + + wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") } + + verify(delegate).retrieve(eq("myKey"), any>>()) + assertEquals(0, tx.spans.size) + } + // -- put -- @Test From 4d9f68376767b871f5a3b3f60914dab52b846f3a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 13 Mar 2026 07:03:39 +0100 Subject: [PATCH 26/48] feat(spring-jakarta): Add cache tracing for Spring Boot 3 / Spring 6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port cache tracing classes from sentry-spring-7 to sentry-spring-jakarta, covering Spring Boot 3 (Spring Framework 6.x) users. Includes SentryCacheWrapper, SentryCacheManagerWrapper, SentryCacheBeanPostProcessor, and auto-configuration in sentry-spring-boot-jakarta. The retrieve() overrides for CompletableFuture/reactive cache operations are included and safe on Spring 6.0 (where retrieve() doesn't exist on the Cache interface) — they're simply dead code, never called by the framework until Spring 6.1+. Co-Authored-By: Claude --- .../boot/jakarta/SentryAutoConfiguration.java | 15 + .../api/sentry-spring-jakarta.api | 29 ++ .../cache/SentryCacheBeanPostProcessor.java | 29 ++ .../cache/SentryCacheManagerWrapper.java | 37 ++ .../jakarta/cache/SentryCacheWrapper.java | 303 ++++++++++++ .../cache/SentryCacheBeanPostProcessorTest.kt | 44 ++ .../cache/SentryCacheManagerWrapperTest.kt | 61 +++ .../jakarta/cache/SentryCacheWrapperTest.kt | 463 ++++++++++++++++++ 8 files changed, 981 insertions(+) create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheBeanPostProcessor.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheManagerWrapper.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java create mode 100644 sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheBeanPostProcessorTest.kt create mode 100644 sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheManagerWrapperTest.kt create mode 100644 sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index 8663dac8c56..ef57868ad87 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -25,6 +25,7 @@ import io.sentry.spring.jakarta.SentryWebConfiguration; import io.sentry.spring.jakarta.SpringProfilesEventProcessor; import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider; +import io.sentry.spring.jakarta.cache.SentryCacheBeanPostProcessor; import io.sentry.spring.jakarta.checkin.SentryCheckInAdviceConfiguration; import io.sentry.spring.jakarta.checkin.SentryCheckInPointcutConfiguration; import io.sentry.spring.jakarta.checkin.SentryQuartzConfiguration; @@ -65,6 +66,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; @@ -231,6 +233,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-jakarta/api/sentry-spring-jakarta.api b/sentry-spring-jakarta/api/sentry-spring-jakarta.api index f28f4153b59..fe634da6f4c 100644 --- a/sentry-spring-jakarta/api/sentry-spring-jakarta.api +++ b/sentry-spring-jakarta/api/sentry-spring-jakarta.api @@ -104,6 +104,35 @@ public final class io/sentry/spring/jakarta/SpringSecuritySentryUserProvider : i public fun provideUser ()Lio/sentry/protocol/User; } +public final class io/sentry/spring/jakarta/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/spring/jakarta/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; + public fun getCacheNames ()Ljava/util/Collection; +} + +public final class io/sentry/spring/jakarta/cache/SentryCacheWrapper : org/springframework/cache/Cache { + public fun (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 fun retrieve (Ljava/lang/Object;)Ljava/util/concurrent/CompletableFuture; + public fun retrieve (Ljava/lang/Object;Ljava/util/function/Supplier;)Ljava/util/concurrent/CompletableFuture; +} + public abstract interface annotation class io/sentry/spring/jakarta/checkin/SentryCheckIn : java/lang/annotation/Annotation { public abstract fun heartbeat ()Z public abstract fun monitorSlug ()Ljava/lang/String; diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheBeanPostProcessor.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheBeanPostProcessor.java new file mode 100644 index 00000000000..ec9964f7abc --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheBeanPostProcessor.java @@ -0,0 +1,29 @@ +package io.sentry.spring.jakarta.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-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheManagerWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheManagerWrapper.java new file mode 100644 index 00000000000..ed243e973a2 --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheManagerWrapper.java @@ -0,0 +1,37 @@ +package io.sentry.spring.jakarta.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 getCacheNames() { + return delegate.getCacheNames(); + } +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java new file mode 100644 index 00000000000..6253ede412e --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -0,0 +1,303 @@ +package io.sentry.spring.jakarta.cache; + +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.SpanDataConvention; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.cache.Cache; + +/** Wraps a Spring {@link Cache} to create Sentry spans for cache operations. */ +@ApiStatus.Internal +public final class SentryCacheWrapper implements Cache { + + private static final String TRACE_ORIGIN = "auto.cache.spring"; + + private final @NotNull Cache delegate; + private final @NotNull IScopes scopes; + + public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes scopes) { + this.delegate = delegate; + this.scopes = scopes; + } + + @Override + public @NotNull String getName() { + return delegate.getName(); + } + + @Override + public @NotNull Object getNativeCache() { + return delegate.getNativeCache(); + } + + @Override + public @Nullable ValueWrapper get(final @NotNull Object key) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.get(key); + } + try { + final ValueWrapper result = delegate.get(key); + span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public @Nullable T get(final @NotNull Object key, final @Nullable Class type) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.get(key, type); + } + try { + final T result = delegate.get(key, type); + span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public @Nullable T get(final @NotNull Object key, final @NotNull Callable valueLoader) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.get(key, valueLoader); + } + try { + final AtomicBoolean loaderInvoked = new AtomicBoolean(false); + final T result = + delegate.get( + key, + () -> { + loaderInvoked.set(true); + return valueLoader.call(); + }); + span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public @Nullable CompletableFuture retrieve(final @NotNull Object key) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.retrieve(key); + } + final CompletableFuture result; + try { + result = delegate.retrieve(key); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + span.finish(); + throw e; + } + if (result == null) { + span.setData(SpanDataConvention.CACHE_HIT_KEY, false); + span.setStatus(SpanStatus.OK); + span.finish(); + return null; + } + return result.whenComplete( + (value, throwable) -> { + if (throwable != null) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(throwable); + } else { + span.setData(SpanDataConvention.CACHE_HIT_KEY, value != null); + span.setStatus(SpanStatus.OK); + } + span.finish(); + }); + } + + @Override + public CompletableFuture retrieve( + final @NotNull Object key, final @NotNull Supplier> valueLoader) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.retrieve(key, valueLoader); + } + final AtomicBoolean loaderInvoked = new AtomicBoolean(false); + final CompletableFuture result; + try { + result = + delegate.retrieve( + key, + () -> { + loaderInvoked.set(true); + return valueLoader.get(); + }); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + span.finish(); + throw e; + } + return result.whenComplete( + (value, throwable) -> { + if (throwable != null) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(throwable); + } else { + span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setStatus(SpanStatus.OK); + } + span.finish(); + }); + } + + @Override + public void put(final @NotNull Object key, final @Nullable Object value) { + final ISpan span = startSpan("cache.put", key); + if (span == null) { + delegate.put(key, value); + return; + } + try { + delegate.put(key, value); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + // putIfAbsent is not instrumented — we cannot know ahead of time whether the put + // will actually happen, and emitting a cache.put span for a no-op would be misleading. + // This matches sentry-python and sentry-javascript which also skip conditional puts. + // We must override to bypass the default implementation which calls this.get() + this.put(). + @Override + public @Nullable ValueWrapper putIfAbsent( + final @NotNull Object key, final @Nullable Object value) { + return delegate.putIfAbsent(key, value); + } + + @Override + public void evict(final @NotNull Object key) { + final ISpan span = startSpan("cache.remove", key); + if (span == null) { + delegate.evict(key); + return; + } + try { + delegate.evict(key); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public boolean evictIfPresent(final @NotNull Object key) { + final ISpan span = startSpan("cache.remove", key); + if (span == null) { + return delegate.evictIfPresent(key); + } + try { + final boolean result = delegate.evictIfPresent(key); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public void clear() { + final ISpan span = startSpan("cache.flush", null); + if (span == null) { + delegate.clear(); + return; + } + try { + delegate.clear(); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public boolean invalidate() { + final ISpan span = startSpan("cache.flush", null); + if (span == null) { + return delegate.invalidate(); + } + try { + final boolean result = delegate.invalidate(); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + private @Nullable ISpan startSpan(final @NotNull String operation, final @Nullable Object key) { + if (!scopes.getOptions().isEnableCacheTracing()) { + return null; + } + + final ISpan activeSpan = scopes.getSpan(); + if (activeSpan == null || activeSpan.isNoOp()) { + return null; + } + + final SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(TRACE_ORIGIN); + final String keyString = key != null ? String.valueOf(key) : null; + final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + if (keyString != null) { + span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); + } + return span; + } +} diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheBeanPostProcessorTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheBeanPostProcessorTest.kt new file mode 100644 index 00000000000..301678d35d9 --- /dev/null +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheBeanPostProcessorTest.kt @@ -0,0 +1,44 @@ +package io.sentry.spring.jakarta.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-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheManagerWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheManagerWrapperTest.kt new file mode 100644 index 00000000000..05daa207d37 --- /dev/null +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheManagerWrapperTest.kt @@ -0,0 +1,61 @@ +package io.sentry.spring.jakarta.cache + +import io.sentry.IScopes +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.cache.Cache +import org.springframework.cache.CacheManager + +class SentryCacheManagerWrapperTest { + + private val scopes: IScopes = mock() + private val delegate: CacheManager = mock() + + @Test + fun `getCache wraps returned cache in SentryCacheWrapper`() { + val cache = mock() + whenever(delegate.getCache("test")).thenReturn(cache) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.getCache("test") + + assertTrue(result is SentryCacheWrapper) + } + + @Test + fun `getCache returns null when delegate returns null`() { + whenever(delegate.getCache("missing")).thenReturn(null) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.getCache("missing") + + assertNull(result) + } + + @Test + fun `getCache does not double-wrap SentryCacheWrapper`() { + val innerCache = mock() + val alreadyWrapped = SentryCacheWrapper(innerCache, scopes) + whenever(delegate.getCache("test")).thenReturn(alreadyWrapped) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.getCache("test") + + assertSame(alreadyWrapped, result) + } + + @Test + fun `getCacheNames delegates to underlying cache manager`() { + whenever(delegate.cacheNames).thenReturn(listOf("cache1", "cache2")) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.cacheNames + + assertEquals(listOf("cache1", "cache2"), result) + } +} diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt new file mode 100644 index 00000000000..c618688ebb9 --- /dev/null +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -0,0 +1,463 @@ +package io.sentry.spring.jakarta.cache + +import io.sentry.IScopes +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanDataConvention +import io.sentry.SpanStatus +import io.sentry.TransactionContext +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture +import java.util.function.Supplier +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.cache.Cache + +class SentryCacheWrapperTest { + + private lateinit var scopes: IScopes + private lateinit var delegate: Cache + private lateinit var options: SentryOptions + + @BeforeTest + fun setup() { + scopes = mock() + delegate = mock() + options = SentryOptions().apply { isEnableCacheTracing = true } + whenever(scopes.options).thenReturn(options) + whenever(delegate.name).thenReturn("testCache") + } + + private fun createTransaction(): SentryTracer { + val tx = SentryTracer(TransactionContext("tx", "op"), scopes) + whenever(scopes.span).thenReturn(tx) + return tx + } + + // -- get(Object key) -- + + @Test + fun `get with ValueWrapper creates span with cache hit true on hit`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) + + val result = wrapper.get("myKey") + + assertEquals(valueWrapper, result) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.get", span.operation) + assertEquals("myKey", span.description) + assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("auto.cache.spring", span.spanContext.origin) + } + + @Test + fun `get with ValueWrapper creates span with cache hit false on miss`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + val result = wrapper.get("myKey") + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- get(Object key, Class) -- + + @Test + fun `get with type creates span with cache hit true on hit`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey", String::class.java)).thenReturn("value") + + val result = wrapper.get("myKey", String::class.java) + + assertEquals("value", result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + @Test + fun `get with type creates span with cache hit false on miss`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey", String::class.java)).thenReturn(null) + + val result = wrapper.get("myKey", String::class.java) + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- get(Object key, Callable) -- + + @Test + fun `get with callable creates span with cache hit true on hit`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + // Simulate cache hit: delegate returns value without invoking the loader + whenever(delegate.get(eq("myKey"), any>())).thenReturn("cached") + + val result = wrapper.get("myKey", Callable { "loaded" }) + + assertEquals("cached", result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + @Test + fun `get with callable creates span with cache hit false on miss`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + // Simulate cache miss: delegate invokes the loader callable + whenever(delegate.get(eq("myKey"), any>())).thenAnswer { invocation -> + val loader = invocation.getArgument>(1) + loader.call() + } + + val result = wrapper.get("myKey", Callable { "loaded" }) + + assertEquals("loaded", result) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- retrieve(Object key) -- + + @Test + fun `retrieve creates span with cache hit true when future resolves with value`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.retrieve("myKey")).thenReturn(CompletableFuture.completedFuture("value")) + + val result = wrapper.retrieve("myKey") + + assertEquals("value", result!!.get()) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.get", span.operation) + assertEquals("myKey", span.description) + assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertTrue(span.isFinished) + } + + @Test + fun `retrieve creates span with cache hit false when future resolves with null`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.retrieve("myKey")).thenReturn(CompletableFuture.completedFuture(null)) + + val result = wrapper.retrieve("myKey") + + assertNull(result!!.get()) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertTrue(tx.spans.first().isFinished) + } + + @Test + fun `retrieve creates span with cache hit false when delegate returns null`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.retrieve("myKey")).thenReturn(null) + + val result = wrapper.retrieve("myKey") + + assertNull(result) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(false, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(SpanStatus.OK, span.status) + assertTrue(span.isFinished) + } + + @Test + fun `retrieve sets error status when future completes exceptionally`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("async cache error") + whenever(delegate.retrieve("myKey")) + .thenReturn(CompletableFuture().also { it.completeExceptionally(exception) }) + + val result = wrapper.retrieve("myKey") + + assertFailsWith { result!!.get() } + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + assertTrue(span.isFinished) + } + + @Test + fun `retrieve sets error status when delegate throws synchronously`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("sync error") + whenever(delegate.retrieve("myKey")).thenThrow(exception) + + assertFailsWith { wrapper.retrieve("myKey") } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + assertTrue(span.isFinished) + } + + @Test + fun `retrieve does not create span when tracing is disabled`() { + options.isEnableCacheTracing = false + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.retrieve("myKey")).thenReturn(CompletableFuture.completedFuture("value")) + + wrapper.retrieve("myKey") + + verify(delegate).retrieve("myKey") + assertEquals(0, tx.spans.size) + } + + // -- retrieve(Object key, Supplier>) -- + + @Test + fun `retrieve with loader creates span with cache hit true when loader not invoked`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + // Simulate cache hit: delegate returns value without invoking the loader + whenever(delegate.retrieve(eq("myKey"), any>>())) + .thenReturn(CompletableFuture.completedFuture("cached")) + + val result = wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") } + + assertEquals("cached", result.get()) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertTrue(tx.spans.first().isFinished) + } + + @Test + fun `retrieve with loader creates span with cache hit false when loader invoked`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + // Simulate cache miss: delegate invokes the loader supplier + whenever(delegate.retrieve(eq("myKey"), any>>())) + .thenAnswer { invocation -> + val loader = invocation.getArgument>>(1) + loader.get() + } + + val result = wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") } + + assertEquals("loaded", result.get()) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertTrue(tx.spans.first().isFinished) + } + + @Test + fun `retrieve with loader sets error status when future completes exceptionally`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("async loader error") + whenever(delegate.retrieve(eq("myKey"), any>>())) + .thenReturn(CompletableFuture().also { it.completeExceptionally(exception) }) + + val result = wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") } + + assertFailsWith { result.get() } + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + assertTrue(span.isFinished) + } + + @Test + fun `retrieve with loader does not create span when tracing is disabled`() { + options.isEnableCacheTracing = false + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.retrieve(eq("myKey"), any>>())) + .thenReturn(CompletableFuture.completedFuture("cached")) + + wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") } + + verify(delegate).retrieve(eq("myKey"), any>>()) + assertEquals(0, tx.spans.size) + } + + // -- put -- + + @Test + fun `put creates cache put span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + + wrapper.put("myKey", "myValue") + + verify(delegate).put("myKey", "myValue") + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + } + + // -- putIfAbsent -- + + @Test + fun `putIfAbsent delegates without creating span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(null) + + wrapper.putIfAbsent("myKey", "myValue") + + verify(delegate).putIfAbsent("myKey", "myValue") + assertEquals(0, tx.spans.size) + } + + // -- evict -- + + @Test + fun `evict creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + + wrapper.evict("myKey") + + verify(delegate).evict("myKey") + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.remove", span.operation) + assertEquals(SpanStatus.OK, span.status) + } + + // -- evictIfPresent -- + + @Test + fun `evictIfPresent creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.evictIfPresent("myKey")).thenReturn(true) + + val result = wrapper.evictIfPresent("myKey") + + assertTrue(result) + assertEquals(1, tx.spans.size) + assertEquals("cache.remove", tx.spans.first().operation) + } + + // -- clear -- + + @Test + fun `clear creates cache flush span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + + wrapper.clear() + + verify(delegate).clear() + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.flush", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) + } + + // -- invalidate -- + + @Test + fun `invalidate creates cache flush span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.invalidate()).thenReturn(true) + + val result = wrapper.invalidate() + + assertTrue(result) + assertEquals(1, tx.spans.size) + assertEquals("cache.flush", tx.spans.first().operation) + } + + // -- no span when no active transaction -- + + @Test + fun `does not create span when there is no active transaction`() { + whenever(scopes.span).thenReturn(null) + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + wrapper.get("myKey") + + verify(delegate).get("myKey") + } + + // -- no span when option is disabled -- + + @Test + fun `does not create span when enableCacheTracing is false`() { + options.isEnableCacheTracing = false + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + wrapper.get("myKey") + + verify(delegate).get("myKey") + assertEquals(0, tx.spans.size) + } + + // -- error handling -- + + @Test + fun `sets error status and throwable on exception`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("cache error") + whenever(delegate.get("myKey")).thenThrow(exception) + + assertFailsWith { wrapper.get("myKey") } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + } + + // -- delegation -- + + @Test + fun `getName delegates to underlying cache`() { + val wrapper = SentryCacheWrapper(delegate, scopes) + assertEquals("testCache", wrapper.name) + } + + @Test + fun `getNativeCache delegates to underlying cache`() { + val nativeCache = Object() + whenever(delegate.nativeCache).thenReturn(nativeCache) + val wrapper = SentryCacheWrapper(delegate, scopes) + + assertEquals(nativeCache, wrapper.nativeCache) + } +} From 658e082fc372e2bb6ac0acaa676ceacba39743b9 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 13 Mar 2026 10:03:33 +0100 Subject: [PATCH 27/48] feat(samples): Add cache tracing to all Spring Boot 3 Jakarta samples Add CacheController, TodoService with @Cacheable/@CachePut/@CacheEvict, Caffeine cache config, and CacheSystemTest e2e tests to all four Jakarta sample modules: - sentry-samples-spring-boot-jakarta - sentry-samples-spring-boot-jakarta-opentelemetry - sentry-samples-spring-boot-jakarta-opentelemetry-noagent - sentry-samples-spring-boot-webflux-jakarta Co-Authored-By: Claude --- gradle/libs.versions.toml | 1 + .../build.gradle.kts | 4 ++ .../spring/boot/jakarta/CacheController.java | 34 +++++++++++++ .../boot/jakarta/SentryDemoApplication.java | 2 + .../spring/boot/jakarta/TodoService.java | 29 +++++++++++ .../src/main/resources/application.properties | 5 ++ .../io/sentry/systemtest/CacheSystemTest.kt | 51 +++++++++++++++++++ .../build.gradle.kts | 4 ++ .../spring/boot/jakarta/CacheController.java | 34 +++++++++++++ .../boot/jakarta/SentryDemoApplication.java | 2 + .../spring/boot/jakarta/TodoService.java | 29 +++++++++++ .../src/main/resources/application.properties | 5 ++ .../io/sentry/systemtest/CacheSystemTest.kt | 51 +++++++++++++++++++ .../build.gradle.kts | 4 ++ .../spring/boot/jakarta/CacheController.java | 34 +++++++++++++ .../boot/jakarta/SentryDemoApplication.java | 2 + .../spring/boot/jakarta/TodoService.java | 29 +++++++++++ .../src/main/resources/application.properties | 5 ++ .../io/sentry/systemtest/CacheSystemTest.kt | 51 +++++++++++++++++++ .../build.gradle.kts | 4 ++ .../spring/boot/jakarta/CacheController.java | 34 +++++++++++++ .../boot/jakarta/SentryDemoApplication.java | 2 + .../spring/boot/jakarta/TodoService.java | 29 +++++++++++ .../src/main/resources/application.properties | 5 ++ .../io/sentry/systemtest/CacheSystemTest.kt | 51 +++++++++++++++++++ 25 files changed, 501 insertions(+) create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 54731df1f24..368a87ac365 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -181,6 +181,7 @@ springboot3-starter-aop = { module = "org.springframework.boot:spring-boot-start springboot3-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "springboot3" } springboot3-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot3" } springboot3-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot3" } +springboot3-starter-cache = { module = "org.springframework.boot:spring-boot-starter-cache", version.ref = "springboot3" } springboot4-otel = { module = "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter", version.ref = "otelInstrumentation" } springboot4-resttestclient = { module = "org.springframework.boot:spring-boot-resttestclient", version.ref = "springboot4" } springboot4-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springboot4" } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/build.gradle.kts index b0fbae0ddc4..86914467a6d 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/build.gradle.kts @@ -52,6 +52,10 @@ dependencies { implementation(projects.sentryAsyncProfiler) implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentlessSpring) + // cache tracing + implementation(libs.springboot3.starter.cache) + implementation(libs.caffeine) + // database query tracing implementation(projects.sentryJdbc) runtimeOnly(libs.hsqldb) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java new file mode 100644 index 00000000000..1327d5e6e29 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot.jakarta; + +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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java index 7f412eaa0d6..8cbd7875b5f 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -11,6 +11,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; @@ -21,6 +22,7 @@ import org.springframework.web.reactive.function.client.WebClient; @SpringBootApplication +@EnableCaching @EnableScheduling public class SentryDemoApplication { public static void main(String[] args) { diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java new file mode 100644 index 00000000000..70a145558c2 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java @@ -0,0 +1,29 @@ +package io.sentry.samples.spring.boot.jakarta; + +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 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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/resources/application.properties index d19c33a3d1b..a3a59d290b1 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/resources/application.properties @@ -35,6 +35,11 @@ spring.graphql.graphiql.enabled=true spring.graphql.websocket.path=/graphql spring.quartz.job-store-type=memory +# Cache tracing +sentry.enable-cache-tracing=true +spring.cache.cache-names=todos +spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s + # OTEL configuration otel.propagators=tracecontext,baggage,sentry otel.logs.exporter=none diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt new file mode 100644 index 00000000000..7853750a8fb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -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") + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts index 0eeaf30d2bd..37d7a94eec0 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts @@ -56,6 +56,10 @@ dependencies { implementation(libs.otel) implementation(projects.sentryAsyncProfiler) + // cache tracing + implementation(libs.springboot3.starter.cache) + implementation(libs.caffeine) + // database query tracing implementation(projects.sentryJdbc) runtimeOnly(libs.hsqldb) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java new file mode 100644 index 00000000000..1327d5e6e29 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot.jakarta; + +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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java index a6eb46f4c74..cd550bfbadf 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -11,6 +11,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; @@ -21,6 +22,7 @@ import org.springframework.web.reactive.function.client.WebClient; @SpringBootApplication +@EnableCaching @EnableScheduling public class SentryDemoApplication { public static void main(String[] args) { diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java new file mode 100644 index 00000000000..70a145558c2 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java @@ -0,0 +1,29 @@ +package io.sentry.samples.spring.boot.jakarta; + +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 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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/resources/application.properties index 6b57706019b..12a9ca17269 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/resources/application.properties @@ -34,3 +34,8 @@ spring.datasource.password= spring.graphql.graphiql.enabled=true spring.graphql.websocket.path=/graphql spring.quartz.job-store-type=memory + +# Cache tracing +sentry.enable-cache-tracing=true +spring.cache.cache-names=todos +spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt new file mode 100644 index 00000000000..7853750a8fb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -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") + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts index 570d35b727b..a945b87109a 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts @@ -55,6 +55,10 @@ dependencies { implementation(projects.sentryAsyncProfiler) implementation(projects.sentryOpenfeature) + // cache tracing + implementation(libs.springboot3.starter.cache) + implementation(libs.caffeine) + // OpenFeature SDK implementation(libs.openfeature) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java new file mode 100644 index 00000000000..1327d5e6e29 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot.jakarta; + +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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java index 8050cb8e74c..e818cbe42ff 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -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; @@ -19,6 +20,7 @@ import org.springframework.web.reactive.function.client.WebClient; @SpringBootApplication +@EnableCaching @EnableScheduling public class SentryDemoApplication { public static void main(String[] args) { diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java new file mode 100644 index 00000000000..70a145558c2 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java @@ -0,0 +1,29 @@ +package io.sentry.samples.spring.boot.jakarta; + +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 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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties index 9830709c313..60b92d369d5 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties @@ -35,3 +35,8 @@ spring.graphql.graphiql.enabled=true spring.graphql.websocket.path=/graphql spring.quartz.job-store-type=memory +# Cache tracing +sentry.enable-cache-tracing=true +spring.cache.cache-names=todos +spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s + diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt new file mode 100644 index 00000000000..7853750a8fb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -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") + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/build.gradle.kts index 1cdff5cab38..a45249830f4 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/build.gradle.kts @@ -31,6 +31,10 @@ dependencies { implementation(libs.springboot3.starter.graphql) implementation(libs.springboot3.starter.webflux) + // cache tracing + implementation(libs.springboot3.starter.cache) + implementation(libs.caffeine) + testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(projects.sentrySystemTestSupport) testImplementation(libs.apollo3.kotlin) diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java new file mode 100644 index 00000000000..1327d5e6e29 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CacheController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot.jakarta; + +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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java index 926298bb97b..baa6d30e5c3 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -2,10 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.web.reactive.function.client.WebClient; @SpringBootApplication +@EnableCaching public class SentryDemoApplication { public static void main(String[] args) { SpringApplication.run(SentryDemoApplication.class, args); diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java new file mode 100644 index 00000000000..70a145558c2 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoService.java @@ -0,0 +1,29 @@ +package io.sentry.samples.spring.boot.jakarta; + +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 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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/resources/application.properties index 3bc4087b288..02eaf0c731c 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/resources/application.properties @@ -16,3 +16,8 @@ sentry.in-app-includes="io.sentry.samples" 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 diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt new file mode 100644 index 00000000000..7853750a8fb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -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") + } + } +} From 7e66a3df7b9c208b4b555ef5c13bbedb68bea6a5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 13 Mar 2026 11:00:37 +0100 Subject: [PATCH 28/48] 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 --- .../spring/boot/SentryAutoConfiguration.java | 15 + sentry-spring/api/sentry-spring.api | 27 ++ .../cache/SentryCacheBeanPostProcessor.java | 29 ++ .../cache/SentryCacheManagerWrapper.java | 37 +++ .../spring/cache/SentryCacheWrapper.java | 231 ++++++++++++++ .../cache/SentryCacheBeanPostProcessorTest.kt | 44 +++ .../cache/SentryCacheManagerWrapperTest.kt | 61 ++++ .../spring/cache/SentryCacheWrapperTest.kt | 295 ++++++++++++++++++ 8 files changed, 739 insertions(+) create mode 100644 sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheBeanPostProcessor.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheManagerWrapper.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java create mode 100644 sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheBeanPostProcessorTest.kt create mode 100644 sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheManagerWrapperTest.kt create mode 100644 sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index 76424b5c55f..99fd602f74b 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -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; @@ -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; @@ -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( diff --git a/sentry-spring/api/sentry-spring.api b/sentry-spring/api/sentry-spring.api index fb07af382ba..7148277e2ef 100644 --- a/sentry-spring/api/sentry-spring.api +++ b/sentry-spring/api/sentry-spring.api @@ -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 ()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 (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 (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; diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheBeanPostProcessor.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheBeanPostProcessor.java new file mode 100644 index 00000000000..7382f7500f2 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheBeanPostProcessor.java @@ -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; + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheManagerWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheManagerWrapper.java new file mode 100644 index 00000000000..a66517fd7fb --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheManagerWrapper.java @@ -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 getCacheNames() { + return delegate.getCacheNames(); + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java new file mode 100644 index 00000000000..629cceb7159 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -0,0 +1,231 @@ +package io.sentry.spring.cache; + +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.SpanDataConvention; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import java.util.Arrays; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.cache.Cache; + +/** Wraps a Spring {@link Cache} to create Sentry spans for cache operations. */ +@ApiStatus.Internal +public final class SentryCacheWrapper implements Cache { + + private static final String TRACE_ORIGIN = "auto.cache.spring"; + + private final @NotNull Cache delegate; + private final @NotNull IScopes scopes; + + public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes scopes) { + this.delegate = delegate; + this.scopes = scopes; + } + + @Override + public @NotNull String getName() { + return delegate.getName(); + } + + @Override + public @NotNull Object getNativeCache() { + return delegate.getNativeCache(); + } + + @Override + public @Nullable ValueWrapper get(final @NotNull Object key) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.get(key); + } + try { + final ValueWrapper result = delegate.get(key); + span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public @Nullable T get(final @NotNull Object key, final @Nullable Class type) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.get(key, type); + } + try { + final T result = delegate.get(key, type); + span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public @Nullable T get(final @NotNull Object key, final @NotNull Callable valueLoader) { + final ISpan span = startSpan("cache.get", key); + if (span == null) { + return delegate.get(key, valueLoader); + } + try { + final AtomicBoolean loaderInvoked = new AtomicBoolean(false); + final T result = + delegate.get( + key, + () -> { + loaderInvoked.set(true); + return valueLoader.call(); + }); + span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public void put(final @NotNull Object key, final @Nullable Object value) { + final ISpan span = startSpan("cache.put", key); + if (span == null) { + delegate.put(key, value); + return; + } + try { + delegate.put(key, value); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + // putIfAbsent is not instrumented — we cannot know ahead of time whether the put + // will actually happen, and emitting a cache.put span for a no-op would be misleading. + // This matches sentry-python and sentry-javascript which also skip conditional puts. + // We must override to bypass the default implementation which calls this.get() + this.put(). + @Override + public @Nullable ValueWrapper putIfAbsent( + final @NotNull Object key, final @Nullable Object value) { + return delegate.putIfAbsent(key, value); + } + + @Override + public void evict(final @NotNull Object key) { + final ISpan span = startSpan("cache.remove", key); + if (span == null) { + delegate.evict(key); + return; + } + try { + delegate.evict(key); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public boolean evictIfPresent(final @NotNull Object key) { + final ISpan span = startSpan("cache.remove", key); + if (span == null) { + return delegate.evictIfPresent(key); + } + try { + final boolean result = delegate.evictIfPresent(key); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public void clear() { + final ISpan span = startSpan("cache.flush", null); + if (span == null) { + delegate.clear(); + return; + } + try { + delegate.clear(); + span.setStatus(SpanStatus.OK); + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + @Override + public boolean invalidate() { + final ISpan span = startSpan("cache.flush", null); + if (span == null) { + return delegate.invalidate(); + } + try { + final boolean result = delegate.invalidate(); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + + private @Nullable ISpan startSpan(final @NotNull String operation, final @Nullable Object key) { + if (!scopes.getOptions().isEnableCacheTracing()) { + return null; + } + + final ISpan activeSpan = scopes.getSpan(); + if (activeSpan == null || activeSpan.isNoOp()) { + return null; + } + + final SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(TRACE_ORIGIN); + final String keyString = key != null ? String.valueOf(key) : null; + final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + if (keyString != null) { + span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); + } + return span; + } +} diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheBeanPostProcessorTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheBeanPostProcessorTest.kt new file mode 100644 index 00000000000..4392d6820e5 --- /dev/null +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheBeanPostProcessorTest.kt @@ -0,0 +1,44 @@ +package io.sentry.spring.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/src/test/kotlin/io/sentry/spring/cache/SentryCacheManagerWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheManagerWrapperTest.kt new file mode 100644 index 00000000000..e3d45038732 --- /dev/null +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheManagerWrapperTest.kt @@ -0,0 +1,61 @@ +package io.sentry.spring.cache + +import io.sentry.IScopes +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.cache.Cache +import org.springframework.cache.CacheManager + +class SentryCacheManagerWrapperTest { + + private val scopes: IScopes = mock() + private val delegate: CacheManager = mock() + + @Test + fun `getCache wraps returned cache in SentryCacheWrapper`() { + val cache = mock() + whenever(delegate.getCache("test")).thenReturn(cache) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.getCache("test") + + assertTrue(result is SentryCacheWrapper) + } + + @Test + fun `getCache returns null when delegate returns null`() { + whenever(delegate.getCache("missing")).thenReturn(null) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.getCache("missing") + + assertNull(result) + } + + @Test + fun `getCache does not double-wrap SentryCacheWrapper`() { + val innerCache = mock() + val alreadyWrapped = SentryCacheWrapper(innerCache, scopes) + whenever(delegate.getCache("test")).thenReturn(alreadyWrapped) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.getCache("test") + + assertSame(alreadyWrapped, result) + } + + @Test + fun `getCacheNames delegates to underlying cache manager`() { + whenever(delegate.cacheNames).thenReturn(listOf("cache1", "cache2")) + + val wrapper = SentryCacheManagerWrapper(delegate, scopes) + val result = wrapper.cacheNames + + assertEquals(listOf("cache1", "cache2"), result) + } +} diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt new file mode 100644 index 00000000000..c2511639470 --- /dev/null +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -0,0 +1,295 @@ +package io.sentry.spring.cache + +import io.sentry.IScopes +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanDataConvention +import io.sentry.SpanStatus +import io.sentry.TransactionContext +import java.util.concurrent.Callable +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.cache.Cache + +class SentryCacheWrapperTest { + + private lateinit var scopes: IScopes + private lateinit var delegate: Cache + private lateinit var options: SentryOptions + + @BeforeTest + fun setup() { + scopes = mock() + delegate = mock() + options = SentryOptions().apply { isEnableCacheTracing = true } + whenever(scopes.options).thenReturn(options) + whenever(delegate.name).thenReturn("testCache") + } + + private fun createTransaction(): SentryTracer { + val tx = SentryTracer(TransactionContext("tx", "op"), scopes) + whenever(scopes.span).thenReturn(tx) + return tx + } + + // -- get(Object key) -- + + @Test + fun `get with ValueWrapper creates span with cache hit true on hit`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) + + val result = wrapper.get("myKey") + + assertEquals(valueWrapper, result) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.get", span.operation) + assertEquals("myKey", span.description) + assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("auto.cache.spring", span.spanContext.origin) + } + + @Test + fun `get with ValueWrapper creates span with cache hit false on miss`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + val result = wrapper.get("myKey") + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- get(Object key, Class) -- + + @Test + fun `get with type creates span with cache hit true on hit`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey", String::class.java)).thenReturn("value") + + val result = wrapper.get("myKey", String::class.java) + + assertEquals("value", result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + @Test + fun `get with type creates span with cache hit false on miss`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey", String::class.java)).thenReturn(null) + + val result = wrapper.get("myKey", String::class.java) + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- get(Object key, Callable) -- + + @Test + fun `get with callable creates span with cache hit true on hit`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + // Simulate cache hit: delegate returns value without invoking the loader + whenever(delegate.get(eq("myKey"), any>())).thenReturn("cached") + + val result = wrapper.get("myKey", Callable { "loaded" }) + + assertEquals("cached", result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + @Test + fun `get with callable creates span with cache hit false on miss`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + // Simulate cache miss: delegate invokes the loader callable + whenever(delegate.get(eq("myKey"), any>())).thenAnswer { invocation -> + val loader = invocation.getArgument>(1) + loader.call() + } + + val result = wrapper.get("myKey", Callable { "loaded" }) + + assertEquals("loaded", result) + assertEquals(1, tx.spans.size) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + + // -- put -- + + @Test + fun `put creates cache put span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + + wrapper.put("myKey", "myValue") + + verify(delegate).put("myKey", "myValue") + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + } + + // -- putIfAbsent -- + + @Test + fun `putIfAbsent delegates without creating span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(null) + + wrapper.putIfAbsent("myKey", "myValue") + + verify(delegate).putIfAbsent("myKey", "myValue") + assertEquals(0, tx.spans.size) + } + + // -- evict -- + + @Test + fun `evict creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + + wrapper.evict("myKey") + + verify(delegate).evict("myKey") + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.remove", span.operation) + assertEquals(SpanStatus.OK, span.status) + } + + // -- evictIfPresent -- + + @Test + fun `evictIfPresent creates cache remove span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.evictIfPresent("myKey")).thenReturn(true) + + val result = wrapper.evictIfPresent("myKey") + + assertTrue(result) + assertEquals(1, tx.spans.size) + assertEquals("cache.remove", tx.spans.first().operation) + } + + // -- clear -- + + @Test + fun `clear creates cache flush span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + + wrapper.clear() + + verify(delegate).clear() + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.flush", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) + } + + // -- invalidate -- + + @Test + fun `invalidate creates cache flush span`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.invalidate()).thenReturn(true) + + val result = wrapper.invalidate() + + assertTrue(result) + assertEquals(1, tx.spans.size) + assertEquals("cache.flush", tx.spans.first().operation) + } + + // -- no span when no active transaction -- + + @Test + fun `does not create span when there is no active transaction`() { + whenever(scopes.span).thenReturn(null) + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + wrapper.get("myKey") + + verify(delegate).get("myKey") + } + + // -- no span when option is disabled -- + + @Test + fun `does not create span when enableCacheTracing is false`() { + options.isEnableCacheTracing = false + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) + + wrapper.get("myKey") + + verify(delegate).get("myKey") + assertEquals(0, tx.spans.size) + } + + // -- error handling -- + + @Test + fun `sets error status and throwable on exception`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("cache error") + whenever(delegate.get("myKey")).thenThrow(exception) + + assertFailsWith { wrapper.get("myKey") } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + } + + // -- delegation -- + + @Test + fun `getName delegates to underlying cache`() { + val wrapper = SentryCacheWrapper(delegate, scopes) + assertEquals("testCache", wrapper.name) + } + + @Test + fun `getNativeCache delegates to underlying cache`() { + val nativeCache = Object() + whenever(delegate.nativeCache).thenReturn(nativeCache) + val wrapper = SentryCacheWrapper(delegate, scopes) + + assertEquals(nativeCache, wrapper.nativeCache) + } +} From d5e58b0475cd8d8e70f5a871b9153e54c555f926 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 13 Mar 2026 11:10:42 +0100 Subject: [PATCH 29/48] feat(samples): Add cache tracing to Spring Boot 2 sample Add CacheController, TodoService with @Cacheable/@CachePut/@CacheEvict annotations, and CacheSystemTest e2e tests to the sentry-samples-spring-boot sample. Enables cache tracing with Caffeine as the cache provider. Co-Authored-By: Claude --- gradle/libs.versions.toml | 1 + .../build.gradle.kts | 2 + .../samples/spring/boot/CacheController.java | 34 +++++++++++++ .../spring/boot/SentryDemoApplication.java | 2 + .../samples/spring/boot/TodoService.java | 29 +++++++++++ .../src/main/resources/application.properties | 5 ++ .../io/sentry/systemtest/CacheSystemTest.kt | 51 +++++++++++++++++++ 7 files changed, 124 insertions(+) create mode 100644 sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CacheController.java create mode 100644 sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/TodoService.java create mode 100644 sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 368a87ac365..0451ba53986 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts index be2b4583fb6..b6fcd675cf3 100644 --- a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts @@ -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)) diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CacheController.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CacheController.java new file mode 100644 index 00000000000..e85f201139f --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CacheController.java @@ -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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java index b4f46260997..a08770b1029 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java @@ -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; @@ -18,6 +19,7 @@ import org.springframework.web.reactive.function.client.WebClient; @SpringBootApplication +@EnableCaching @EnableScheduling public class SentryDemoApplication { public static void main(String[] args) { diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/TodoService.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/TodoService.java new file mode 100644 index 00000000000..81aa944c8be --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/TodoService.java @@ -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 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); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties index d39f38d7182..4e97e7a1eb8 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties @@ -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 diff --git a/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt new file mode 100644 index 00000000000..7853750a8fb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -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") + } + } +} From 7fa53f7c5f851e1808a122b100bad68504983324 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 13 Mar 2026 11:30:27 +0100 Subject: [PATCH 30/48] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1237e6e07fc..62af42ab287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) From 5a729ca90a40bf4fb4506f9de3935d5133b828a5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 13 Mar 2026 11:51:27 +0100 Subject: [PATCH 31/48] fix(spring): Skip cache span data when child span is NoOp Add span.isNoOp() check after startChild() in all three Spring SentryCacheWrapper variants, matching the existing pattern in SentryJCacheWrapper. This avoids setting span data on noop spans when sampling drops the span. Co-Authored-By: Claude Opus 4.6 --- .../main/java/io/sentry/spring7/cache/SentryCacheWrapper.java | 3 +++ .../io/sentry/spring/jakarta/cache/SentryCacheWrapper.java | 3 +++ .../main/java/io/sentry/spring/cache/SentryCacheWrapper.java | 3 +++ 3 files changed, 9 insertions(+) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 181e9f398da..95d1b3c8025 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -295,6 +295,9 @@ public boolean invalidate() { spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + if (span.isNoOp()) { + return null; + } if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index 6253ede412e..f9e2d823798 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -295,6 +295,9 @@ public boolean invalidate() { spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + if (span.isNoOp()) { + return null; + } if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index 629cceb7159..1d106c9e7c9 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -223,6 +223,9 @@ public boolean invalidate() { spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + if (span.isNoOp()) { + return null; + } if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } From 5d8a4454f2238cedbc7baad0e8672485daa14cb0 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 17 Mar 2026 10:03:52 +0100 Subject: [PATCH 32/48] feat(spring): Add db.operation.name attribute to cache spans Co-Authored-By: Claude Opus 4.6 (1M context) --- .../io/sentry/jcache/SentryJCacheWrapper.java | 38 +++++++++++-------- .../sentry/jcache/SentryJCacheWrapperTest.kt | 13 +++++++ .../spring7/cache/SentryCacheWrapper.java | 27 +++++++------ .../spring7/cache/SentryCacheWrapperTest.kt | 7 ++++ .../jakarta/cache/SentryCacheWrapper.java | 27 +++++++------ .../jakarta/cache/SentryCacheWrapperTest.kt | 7 ++++ .../spring/cache/SentryCacheWrapper.java | 23 ++++++----- .../spring/cache/SentryCacheWrapperTest.kt | 6 +++ 8 files changed, 102 insertions(+), 46 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index d5022a5455f..816e10bfb1f 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -33,6 +33,7 @@ public final class SentryJCacheWrapper implements Cache { private static final String TRACE_ORIGIN = "auto.cache.jcache"; + private static final String OPERATION_ATTRIBUTE = "db.operation.name"; private final @NotNull Cache delegate; private final @NotNull IScopes scopes; @@ -50,7 +51,7 @@ public SentryJCacheWrapper(final @NotNull Cache delegate, final @NotNull I @Override public V get(final K key) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "get"); if (span == null) { return delegate.get(key); } @@ -70,7 +71,7 @@ public V get(final K key) { @Override public Map getAll(final Set keys) { - final ISpan span = startSpanForKeys("cache.get", keys); + final ISpan span = startSpanForKeys("cache.get", keys, "getAll"); if (span == null) { return delegate.getAll(keys); } @@ -97,7 +98,7 @@ public boolean containsKey(final K key) { @Override public void put(final K key, final V value) { - final ISpan span = startSpan("cache.put", key); + final ISpan span = startSpan("cache.put", key, "put"); if (span == null) { delegate.put(key, value); return; @@ -116,7 +117,7 @@ public void put(final K key, final V value) { @Override public V getAndPut(final K key, final V value) { - final ISpan span = startSpan("cache.put", key); + final ISpan span = startSpan("cache.put", key, "getAndPut"); if (span == null) { return delegate.getAndPut(key, value); } @@ -135,7 +136,7 @@ public V getAndPut(final K key, final V value) { @Override public void putAll(final Map map) { - final ISpan span = startSpanForKeys("cache.put", map.keySet()); + final ISpan span = startSpanForKeys("cache.put", map.keySet(), "putAll"); if (span == null) { delegate.putAll(map); return; @@ -181,7 +182,7 @@ public V getAndReplace(final K key, final V value) { @Override public boolean remove(final K key) { - final ISpan span = startSpan("cache.remove", key); + final ISpan span = startSpan("cache.remove", key, "remove"); if (span == null) { return delegate.remove(key); } @@ -200,7 +201,7 @@ public boolean remove(final K key) { @Override public boolean remove(final K key, final V oldValue) { - final ISpan span = startSpan("cache.remove", key); + final ISpan span = startSpan("cache.remove", key, "remove"); if (span == null) { return delegate.remove(key, oldValue); } @@ -219,7 +220,7 @@ public boolean remove(final K key, final V oldValue) { @Override public V getAndRemove(final K key) { - final ISpan span = startSpan("cache.remove", key); + final ISpan span = startSpan("cache.remove", key, "getAndRemove"); if (span == null) { return delegate.getAndRemove(key); } @@ -238,7 +239,7 @@ public V getAndRemove(final K key) { @Override public void removeAll(final Set keys) { - final ISpan span = startSpanForKeys("cache.remove", keys); + final ISpan span = startSpanForKeys("cache.remove", keys, "removeAll"); if (span == null) { delegate.removeAll(keys); return; @@ -257,7 +258,7 @@ public void removeAll(final Set keys) { @Override public void removeAll() { - final ISpan span = startSpan("cache.flush", null); + final ISpan span = startSpan("cache.flush", null, "removeAll"); if (span == null) { delegate.removeAll(); return; @@ -278,7 +279,7 @@ public void removeAll() { @Override public void clear() { - final ISpan span = startSpan("cache.flush", null); + final ISpan span = startSpan("cache.flush", null, "clear"); if (span == null) { delegate.clear(); return; @@ -306,7 +307,7 @@ public void close() { public T invoke( final K key, final EntryProcessor entryProcessor, final Object... arguments) throws EntryProcessorException { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "invoke"); if (span == null) { return delegate.invoke(key, entryProcessor, arguments); } @@ -328,7 +329,7 @@ public Map> invokeAll( final Set keys, final EntryProcessor entryProcessor, final Object... arguments) { - final ISpan span = startSpanForKeys("cache.get", keys); + final ISpan span = startSpanForKeys("cache.get", keys, "invokeAll"); if (span == null) { return delegate.invokeAll(keys, entryProcessor, arguments); } @@ -400,7 +401,10 @@ public Iterator> iterator() { // -- span helpers -- - private @Nullable ISpan startSpan(final @NotNull String operation, final @Nullable Object key) { + private @Nullable ISpan startSpan( + final @NotNull String operation, + final @Nullable Object key, + final @NotNull String operationName) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -420,11 +424,14 @@ public Iterator> iterator() { if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } + span.setData(OPERATION_ATTRIBUTE, operationName); return span; } private @Nullable ISpan startSpanForKeys( - final @NotNull String operation, final @NotNull Set keys) { + final @NotNull String operation, + final @NotNull Set keys, + final @NotNull String operationName) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -443,6 +450,7 @@ public Iterator> iterator() { span.setData( SpanDataConvention.CACHE_KEY_KEY, keys.stream().map(String::valueOf).collect(Collectors.toList())); + span.setData(OPERATION_ATTRIBUTE, operationName); return span; } } diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt index 375e0f0ca54..b0572878da7 100644 --- a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -64,6 +64,7 @@ class SentryJCacheWrapperTest { assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.jcache", span.spanContext.origin) + assertEquals("get", span.getData("db.operation.name")) } @Test @@ -98,6 +99,7 @@ class SentryJCacheWrapperTest { assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) + assertEquals("getAll", span.getData("db.operation.name")) } @Test @@ -127,6 +129,7 @@ class SentryJCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("put", span.getData("db.operation.name")) } // -- getAndPut -- @@ -142,6 +145,7 @@ class SentryJCacheWrapperTest { assertEquals("oldValue", result) assertEquals(1, tx.spans.size) assertEquals("cache.put", tx.spans.first().operation) + assertEquals("getAndPut", tx.spans.first().getData("db.operation.name")) } // -- putAll -- @@ -161,6 +165,7 @@ class SentryJCacheWrapperTest { assertEquals("testCache", span.description) val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) + assertEquals("putAll", span.getData("db.operation.name")) } // -- putIfAbsent -- @@ -236,6 +241,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.remove", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals("remove", span.getData("db.operation.name")) } // -- remove(K, V) -- @@ -251,6 +257,7 @@ class SentryJCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.remove", tx.spans.first().operation) + assertEquals("remove", tx.spans.first().getData("db.operation.name")) } // -- getAndRemove -- @@ -266,6 +273,7 @@ class SentryJCacheWrapperTest { assertEquals("value", result) assertEquals(1, tx.spans.size) assertEquals("cache.remove", tx.spans.first().operation) + assertEquals("getAndRemove", tx.spans.first().getData("db.operation.name")) } // -- removeAll(Set) -- @@ -283,6 +291,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.remove", span.operation) assertEquals("testCache", span.description) + assertEquals("removeAll", span.getData("db.operation.name")) } // -- removeAll() -- @@ -297,6 +306,7 @@ class SentryJCacheWrapperTest { verify(delegate).removeAll() assertEquals(1, tx.spans.size) assertEquals("cache.flush", tx.spans.first().operation) + assertEquals("removeAll", tx.spans.first().getData("db.operation.name")) } // -- clear -- @@ -314,6 +324,7 @@ class SentryJCacheWrapperTest { assertEquals("cache.flush", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("clear", span.getData("db.operation.name")) } // -- invoke -- @@ -330,6 +341,7 @@ class SentryJCacheWrapperTest { assertEquals("result", result) assertEquals(1, tx.spans.size) assertEquals("cache.get", tx.spans.first().operation) + assertEquals("invoke", tx.spans.first().getData("db.operation.name")) } // -- invokeAll -- @@ -348,6 +360,7 @@ class SentryJCacheWrapperTest { assertEquals(resultMap, result) assertEquals(1, tx.spans.size) assertEquals("cache.get", tx.spans.first().operation) + assertEquals("invokeAll", tx.spans.first().getData("db.operation.name")) } // -- passthrough operations -- diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 95d1b3c8025..1b8b0912177 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -20,6 +20,7 @@ public final class SentryCacheWrapper implements Cache { private static final String TRACE_ORIGIN = "auto.cache.spring"; + private static final String OPERATION_ATTRIBUTE = "db.operation.name"; private final @NotNull Cache delegate; private final @NotNull IScopes scopes; @@ -41,7 +42,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable ValueWrapper get(final @NotNull Object key) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "get"); if (span == null) { return delegate.get(key); } @@ -61,7 +62,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @Nullable Class type) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "get"); if (span == null) { return delegate.get(key, type); } @@ -81,7 +82,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @NotNull Callable valueLoader) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "get"); if (span == null) { return delegate.get(key, valueLoader); } @@ -108,7 +109,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable CompletableFuture retrieve(final @NotNull Object key) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "retrieve"); if (span == null) { return delegate.retrieve(key); } @@ -143,7 +144,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public CompletableFuture retrieve( final @NotNull Object key, final @NotNull Supplier> valueLoader) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "retrieve"); if (span == null) { return delegate.retrieve(key, valueLoader); } @@ -178,7 +179,7 @@ public CompletableFuture retrieve( @Override public void put(final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.put", key); + final ISpan span = startSpan("cache.put", key, "put"); if (span == null) { delegate.put(key, value); return; @@ -207,7 +208,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public void evict(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key); + final ISpan span = startSpan("cache.remove", key, "evict"); if (span == null) { delegate.evict(key); return; @@ -226,7 +227,7 @@ public void evict(final @NotNull Object key) { @Override public boolean evictIfPresent(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key); + final ISpan span = startSpan("cache.remove", key, "evictIfPresent"); if (span == null) { return delegate.evictIfPresent(key); } @@ -245,7 +246,7 @@ public boolean evictIfPresent(final @NotNull Object key) { @Override public void clear() { - final ISpan span = startSpan("cache.flush", null); + final ISpan span = startSpan("cache.flush", null, "clear"); if (span == null) { delegate.clear(); return; @@ -264,7 +265,7 @@ public void clear() { @Override public boolean invalidate() { - final ISpan span = startSpan("cache.flush", null); + final ISpan span = startSpan("cache.flush", null, "invalidate"); if (span == null) { return delegate.invalidate(); } @@ -281,7 +282,10 @@ public boolean invalidate() { } } - private @Nullable ISpan startSpan(final @NotNull String operation, final @Nullable Object key) { + private @Nullable ISpan startSpan( + final @NotNull String operation, + final @Nullable Object key, + final @NotNull String operationName) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -301,6 +305,7 @@ public boolean invalidate() { if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } + span.setData(OPERATION_ATTRIBUTE, operationName); return span; } } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index 9e20fe21554..2a90f45516d 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -63,6 +63,7 @@ class SentryCacheWrapperTest { assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) + assertEquals("get", span.getData("db.operation.name")) } @Test @@ -156,6 +157,7 @@ class SentryCacheWrapperTest { assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals("retrieve", span.getData("db.operation.name")) assertTrue(span.isFinished) } @@ -320,6 +322,7 @@ class SentryCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("put", span.getData("db.operation.name")) } // -- putIfAbsent -- @@ -350,6 +353,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.remove", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals("evict", span.getData("db.operation.name")) } // -- evictIfPresent -- @@ -365,6 +369,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.remove", tx.spans.first().operation) + assertEquals("evictIfPresent", tx.spans.first().getData("db.operation.name")) } // -- clear -- @@ -382,6 +387,7 @@ class SentryCacheWrapperTest { assertEquals("cache.flush", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("clear", span.getData("db.operation.name")) } // -- invalidate -- @@ -397,6 +403,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.flush", tx.spans.first().operation) + assertEquals("invalidate", tx.spans.first().getData("db.operation.name")) } // -- no span when no active transaction -- diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index f9e2d823798..f4f170cbfda 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -20,6 +20,7 @@ public final class SentryCacheWrapper implements Cache { private static final String TRACE_ORIGIN = "auto.cache.spring"; + private static final String OPERATION_ATTRIBUTE = "db.operation.name"; private final @NotNull Cache delegate; private final @NotNull IScopes scopes; @@ -41,7 +42,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable ValueWrapper get(final @NotNull Object key) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "get"); if (span == null) { return delegate.get(key); } @@ -61,7 +62,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @Nullable Class type) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "get"); if (span == null) { return delegate.get(key, type); } @@ -81,7 +82,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @NotNull Callable valueLoader) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "get"); if (span == null) { return delegate.get(key, valueLoader); } @@ -108,7 +109,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable CompletableFuture retrieve(final @NotNull Object key) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "retrieve"); if (span == null) { return delegate.retrieve(key); } @@ -143,7 +144,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public CompletableFuture retrieve( final @NotNull Object key, final @NotNull Supplier> valueLoader) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "retrieve"); if (span == null) { return delegate.retrieve(key, valueLoader); } @@ -178,7 +179,7 @@ public CompletableFuture retrieve( @Override public void put(final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.put", key); + final ISpan span = startSpan("cache.put", key, "put"); if (span == null) { delegate.put(key, value); return; @@ -207,7 +208,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public void evict(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key); + final ISpan span = startSpan("cache.remove", key, "evict"); if (span == null) { delegate.evict(key); return; @@ -226,7 +227,7 @@ public void evict(final @NotNull Object key) { @Override public boolean evictIfPresent(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key); + final ISpan span = startSpan("cache.remove", key, "evictIfPresent"); if (span == null) { return delegate.evictIfPresent(key); } @@ -245,7 +246,7 @@ public boolean evictIfPresent(final @NotNull Object key) { @Override public void clear() { - final ISpan span = startSpan("cache.flush", null); + final ISpan span = startSpan("cache.flush", null, "clear"); if (span == null) { delegate.clear(); return; @@ -264,7 +265,7 @@ public void clear() { @Override public boolean invalidate() { - final ISpan span = startSpan("cache.flush", null); + final ISpan span = startSpan("cache.flush", null, "invalidate"); if (span == null) { return delegate.invalidate(); } @@ -281,7 +282,10 @@ public boolean invalidate() { } } - private @Nullable ISpan startSpan(final @NotNull String operation, final @Nullable Object key) { + private @Nullable ISpan startSpan( + final @NotNull String operation, + final @Nullable Object key, + final @NotNull String operationName) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -301,6 +305,7 @@ public boolean invalidate() { if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } + span.setData(OPERATION_ATTRIBUTE, operationName); return span; } } diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt index c618688ebb9..090bae83909 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -63,6 +63,7 @@ class SentryCacheWrapperTest { assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) + assertEquals("get", span.getData("db.operation.name")) } @Test @@ -156,6 +157,7 @@ class SentryCacheWrapperTest { assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals("retrieve", span.getData("db.operation.name")) assertTrue(span.isFinished) } @@ -320,6 +322,7 @@ class SentryCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("put", span.getData("db.operation.name")) } // -- putIfAbsent -- @@ -350,6 +353,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.remove", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals("evict", span.getData("db.operation.name")) } // -- evictIfPresent -- @@ -365,6 +369,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.remove", tx.spans.first().operation) + assertEquals("evictIfPresent", tx.spans.first().getData("db.operation.name")) } // -- clear -- @@ -382,6 +387,7 @@ class SentryCacheWrapperTest { assertEquals("cache.flush", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("clear", span.getData("db.operation.name")) } // -- invalidate -- @@ -397,6 +403,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.flush", tx.spans.first().operation) + assertEquals("invalidate", tx.spans.first().getData("db.operation.name")) } // -- no span when no active transaction -- diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index 1d106c9e7c9..c765b1e143d 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -18,6 +18,7 @@ public final class SentryCacheWrapper implements Cache { private static final String TRACE_ORIGIN = "auto.cache.spring"; + private static final String OPERATION_ATTRIBUTE = "db.operation.name"; private final @NotNull Cache delegate; private final @NotNull IScopes scopes; @@ -39,7 +40,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable ValueWrapper get(final @NotNull Object key) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "get"); if (span == null) { return delegate.get(key); } @@ -59,7 +60,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @Nullable Class type) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "get"); if (span == null) { return delegate.get(key, type); } @@ -79,7 +80,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @NotNull Callable valueLoader) { - final ISpan span = startSpan("cache.get", key); + final ISpan span = startSpan("cache.get", key, "get"); if (span == null) { return delegate.get(key, valueLoader); } @@ -106,7 +107,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public void put(final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.put", key); + final ISpan span = startSpan("cache.put", key, "put"); if (span == null) { delegate.put(key, value); return; @@ -135,7 +136,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public void evict(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key); + final ISpan span = startSpan("cache.remove", key, "evict"); if (span == null) { delegate.evict(key); return; @@ -154,7 +155,7 @@ public void evict(final @NotNull Object key) { @Override public boolean evictIfPresent(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key); + final ISpan span = startSpan("cache.remove", key, "evictIfPresent"); if (span == null) { return delegate.evictIfPresent(key); } @@ -173,7 +174,7 @@ public boolean evictIfPresent(final @NotNull Object key) { @Override public void clear() { - final ISpan span = startSpan("cache.flush", null); + final ISpan span = startSpan("cache.flush", null, "clear"); if (span == null) { delegate.clear(); return; @@ -192,7 +193,7 @@ public void clear() { @Override public boolean invalidate() { - final ISpan span = startSpan("cache.flush", null); + final ISpan span = startSpan("cache.flush", null, "invalidate"); if (span == null) { return delegate.invalidate(); } @@ -209,7 +210,10 @@ public boolean invalidate() { } } - private @Nullable ISpan startSpan(final @NotNull String operation, final @Nullable Object key) { + private @Nullable ISpan startSpan( + final @NotNull String operation, + final @Nullable Object key, + final @NotNull String operationName) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -229,6 +233,7 @@ public boolean invalidate() { if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } + span.setData(OPERATION_ATTRIBUTE, operationName); return span; } } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt index c2511639470..007f5374115 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -61,6 +61,7 @@ class SentryCacheWrapperTest { assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) + assertEquals("get", span.getData("db.operation.name")) } @Test @@ -152,6 +153,7 @@ class SentryCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("put", span.getData("db.operation.name")) } // -- putIfAbsent -- @@ -182,6 +184,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.remove", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals("evict", span.getData("db.operation.name")) } // -- evictIfPresent -- @@ -197,6 +200,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.remove", tx.spans.first().operation) + assertEquals("evictIfPresent", tx.spans.first().getData("db.operation.name")) } // -- clear -- @@ -214,6 +218,7 @@ class SentryCacheWrapperTest { assertEquals("cache.flush", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("clear", span.getData("db.operation.name")) } // -- invalidate -- @@ -229,6 +234,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.flush", tx.spans.first().operation) + assertEquals("invalidate", tx.spans.first().getData("db.operation.name")) } // -- no span when no active transaction -- From b8911d9aef8cfc3282463067fb9464ea39bb5094 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 17 Mar 2026 10:34:51 +0100 Subject: [PATCH 33/48] feat(spring): Instrument putIfAbsent, replace, and getAndReplace cache operations Co-Authored-By: Claude Opus 4.6 (1M context) --- .../io/sentry/jcache/SentryJCacheWrapper.java | 69 ++++++++++++++++--- .../sentry/jcache/SentryJCacheWrapperTest.kt | 37 +++++++--- .../spring7/cache/SentryCacheWrapper.java | 20 ++++-- .../spring7/cache/SentryCacheWrapperTest.kt | 12 +++- .../jakarta/cache/SentryCacheWrapper.java | 20 ++++-- .../jakarta/cache/SentryCacheWrapperTest.kt | 12 +++- .../spring/cache/SentryCacheWrapper.java | 20 ++++-- .../spring/cache/SentryCacheWrapperTest.kt | 12 +++- 8 files changed, 159 insertions(+), 43 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 816e10bfb1f..81f84f4d7dd 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -153,29 +153,80 @@ public void putAll(final Map map) { } } - // putIfAbsent is not instrumented — we cannot know ahead of time whether the put - // will actually happen, and emitting a cache.put span for a no-op would be misleading. @Override public boolean putIfAbsent(final K key, final V value) { - return delegate.putIfAbsent(key, value); + final ISpan span = startSpan("cache.put", key, "putIfAbsent"); + if (span == null) { + return delegate.putIfAbsent(key, value); + } + try { + final boolean result = delegate.putIfAbsent(key, value); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } } - // replace and getAndReplace are not instrumented — like putIfAbsent, they are conditional - // writes (only happen if the key exists / value matches). Emitting a cache.put span for a - // potential no-op would be misleading. @Override public boolean replace(final K key, final V oldValue, final V newValue) { - return delegate.replace(key, oldValue, newValue); + final ISpan span = startSpan("cache.put", key, "replace"); + if (span == null) { + return delegate.replace(key, oldValue, newValue); + } + try { + final boolean result = delegate.replace(key, oldValue, newValue); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } } @Override public boolean replace(final K key, final V value) { - return delegate.replace(key, value); + final ISpan span = startSpan("cache.put", key, "replace"); + if (span == null) { + return delegate.replace(key, value); + } + try { + final boolean result = delegate.replace(key, value); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } } @Override public V getAndReplace(final K key, final V value) { - return delegate.getAndReplace(key, value); + final ISpan span = startSpan("cache.put", key, "getAndReplace"); + if (span == null) { + return delegate.getAndReplace(key, value); + } + try { + final V result = delegate.getAndReplace(key, value); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } } // -- remove operations -- diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt index b0572878da7..a0d6548121a 100644 --- a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -171,7 +171,7 @@ class SentryJCacheWrapperTest { // -- putIfAbsent -- @Test - fun `putIfAbsent delegates without creating span`() { + fun `putIfAbsent creates cache put span`() { val tx = createTransaction() val wrapper = SentryJCacheWrapper(delegate, scopes) whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(true) @@ -180,13 +180,18 @@ class SentryJCacheWrapperTest { assertTrue(result) verify(delegate).putIfAbsent("myKey", "myValue") - assertEquals(0, tx.spans.size) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("putIfAbsent", span.getData("db.operation.name")) } - // -- replace (passthrough, no span — conditional write) -- + // -- replace -- @Test - fun `replace with old value delegates without creating span`() { + fun `replace with old value creates cache put span`() { val tx = createTransaction() val wrapper = SentryJCacheWrapper(delegate, scopes) whenever(delegate.replace("myKey", "old", "new")).thenReturn(true) @@ -195,11 +200,15 @@ class SentryJCacheWrapperTest { assertTrue(result) verify(delegate).replace("myKey", "old", "new") - assertEquals(0, tx.spans.size) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals("replace", span.getData("db.operation.name")) } @Test - fun `replace delegates without creating span`() { + fun `replace creates cache put span`() { val tx = createTransaction() val wrapper = SentryJCacheWrapper(delegate, scopes) whenever(delegate.replace("myKey", "value")).thenReturn(true) @@ -208,13 +217,17 @@ class SentryJCacheWrapperTest { assertTrue(result) verify(delegate).replace("myKey", "value") - assertEquals(0, tx.spans.size) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals("replace", span.getData("db.operation.name")) } - // -- getAndReplace (passthrough, no span — conditional write) -- + // -- getAndReplace -- @Test - fun `getAndReplace delegates without creating span`() { + fun `getAndReplace creates cache put span`() { val tx = createTransaction() val wrapper = SentryJCacheWrapper(delegate, scopes) whenever(delegate.getAndReplace("myKey", "newValue")).thenReturn("oldValue") @@ -223,7 +236,11 @@ class SentryJCacheWrapperTest { assertEquals("oldValue", result) verify(delegate).getAndReplace("myKey", "newValue") - assertEquals(0, tx.spans.size) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals("getAndReplace", span.getData("db.operation.name")) } // -- remove(K) -- diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 1b8b0912177..bd04285362c 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -196,14 +196,24 @@ public void put(final @NotNull Object key, final @Nullable Object value) { } } - // putIfAbsent is not instrumented — we cannot know ahead of time whether the put - // will actually happen, and emitting a cache.put span for a no-op would be misleading. - // This matches sentry-python and sentry-javascript which also skip conditional puts. - // We must override to bypass the default implementation which calls this.get() + this.put(). @Override public @Nullable ValueWrapper putIfAbsent( final @NotNull Object key, final @Nullable Object value) { - return delegate.putIfAbsent(key, value); + final ISpan span = startSpan("cache.put", key, "putIfAbsent"); + if (span == null) { + return delegate.putIfAbsent(key, value); + } + try { + final ValueWrapper result = delegate.putIfAbsent(key, value); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } } @Override diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index 2a90f45516d..71ce645b376 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -328,15 +328,21 @@ class SentryCacheWrapperTest { // -- putIfAbsent -- @Test - fun `putIfAbsent delegates without creating span`() { + fun `putIfAbsent creates cache put span`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(null) - wrapper.putIfAbsent("myKey", "myValue") + val result = wrapper.putIfAbsent("myKey", "myValue") + assertNull(result) verify(delegate).putIfAbsent("myKey", "myValue") - assertEquals(0, tx.spans.size) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("putIfAbsent", span.getData("db.operation.name")) } // -- evict -- diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index f4f170cbfda..061669f6786 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -196,14 +196,24 @@ public void put(final @NotNull Object key, final @Nullable Object value) { } } - // putIfAbsent is not instrumented — we cannot know ahead of time whether the put - // will actually happen, and emitting a cache.put span for a no-op would be misleading. - // This matches sentry-python and sentry-javascript which also skip conditional puts. - // We must override to bypass the default implementation which calls this.get() + this.put(). @Override public @Nullable ValueWrapper putIfAbsent( final @NotNull Object key, final @Nullable Object value) { - return delegate.putIfAbsent(key, value); + final ISpan span = startSpan("cache.put", key, "putIfAbsent"); + if (span == null) { + return delegate.putIfAbsent(key, value); + } + try { + final ValueWrapper result = delegate.putIfAbsent(key, value); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } } @Override diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt index 090bae83909..bddde75969a 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -328,15 +328,21 @@ class SentryCacheWrapperTest { // -- putIfAbsent -- @Test - fun `putIfAbsent delegates without creating span`() { + fun `putIfAbsent creates cache put span`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(null) - wrapper.putIfAbsent("myKey", "myValue") + val result = wrapper.putIfAbsent("myKey", "myValue") + assertNull(result) verify(delegate).putIfAbsent("myKey", "myValue") - assertEquals(0, tx.spans.size) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("putIfAbsent", span.getData("db.operation.name")) } // -- evict -- diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index c765b1e143d..dab20fd4d52 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -124,14 +124,24 @@ public void put(final @NotNull Object key, final @Nullable Object value) { } } - // putIfAbsent is not instrumented — we cannot know ahead of time whether the put - // will actually happen, and emitting a cache.put span for a no-op would be misleading. - // This matches sentry-python and sentry-javascript which also skip conditional puts. - // We must override to bypass the default implementation which calls this.get() + this.put(). @Override public @Nullable ValueWrapper putIfAbsent( final @NotNull Object key, final @Nullable Object value) { - return delegate.putIfAbsent(key, value); + final ISpan span = startSpan("cache.put", key, "putIfAbsent"); + if (span == null) { + return delegate.putIfAbsent(key, value); + } + try { + final ValueWrapper result = delegate.putIfAbsent(key, value); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } } @Override diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt index 007f5374115..163d0935fc0 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -159,15 +159,21 @@ class SentryCacheWrapperTest { // -- putIfAbsent -- @Test - fun `putIfAbsent delegates without creating span`() { + fun `putIfAbsent creates cache put span`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(null) - wrapper.putIfAbsent("myKey", "myValue") + val result = wrapper.putIfAbsent("myKey", "myValue") + assertNull(result) verify(delegate).putIfAbsent("myKey", "myValue") - assertEquals(0, tx.spans.size) + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals("cache.put", span.operation) + assertEquals(SpanStatus.OK, span.status) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals("putIfAbsent", span.getData("db.operation.name")) } // -- evict -- From 282054940e6972cdcb5fd41233a6270be45f514b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 05:35:50 +0100 Subject: [PATCH 34/48] fix(spring): Use ValueWrapper to determine cache hit in typed get The get(key, type) method incorrectly used result != null to detect cache hits, failing to distinguish a miss from a cached null value. Now uses delegate.get(key) to check the ValueWrapper first. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../spring7/cache/SentryCacheWrapper.java | 3 +- .../spring7/cache/SentryCacheWrapperTest.kt | 34 +++++++++++++++++++ .../jakarta/cache/SentryCacheWrapper.java | 3 +- .../jakarta/cache/SentryCacheWrapperTest.kt | 34 +++++++++++++++++++ .../spring/cache/SentryCacheWrapper.java | 3 +- .../spring/cache/SentryCacheWrapperTest.kt | 34 +++++++++++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index bd04285362c..58b803d40e2 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -67,8 +67,9 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return delegate.get(key, type); } try { + final ValueWrapper wrapper = delegate.get(key); + span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null); final T result = delegate.get(key, type); - span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index 71ce645b376..d42fcabaeb4 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -85,6 +85,8 @@ class SentryCacheWrapperTest { fun `get with type creates span with cache hit true on hit`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) whenever(delegate.get("myKey", String::class.java)).thenReturn("value") val result = wrapper.get("myKey", String::class.java) @@ -94,10 +96,26 @@ class SentryCacheWrapperTest { assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type creates span with cache hit true when cached value is null`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) + whenever(delegate.get("myKey", String::class.java)).thenReturn(null) + + val result = wrapper.get("myKey", String::class.java) + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + @Test fun `get with type creates span with cache hit false on miss`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) whenever(delegate.get("myKey", String::class.java)).thenReturn(null) val result = wrapper.get("myKey", String::class.java) @@ -107,6 +125,22 @@ class SentryCacheWrapperTest { assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type sets error status and throwable on exception`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("cache error") + whenever(delegate.get("myKey")).thenReturn(mock()) + whenever(delegate.get("myKey", String::class.java)).thenThrow(exception) + + assertFailsWith { wrapper.get("myKey", String::class.java) } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + } + // -- get(Object key, Callable) -- @Test diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index 061669f6786..79ea227dbea 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -67,8 +67,9 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return delegate.get(key, type); } try { + final ValueWrapper wrapper = delegate.get(key); + span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null); final T result = delegate.get(key, type); - span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt index bddde75969a..62cb26a5af1 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -85,6 +85,8 @@ class SentryCacheWrapperTest { fun `get with type creates span with cache hit true on hit`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) whenever(delegate.get("myKey", String::class.java)).thenReturn("value") val result = wrapper.get("myKey", String::class.java) @@ -94,10 +96,26 @@ class SentryCacheWrapperTest { assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type creates span with cache hit true when cached value is null`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) + whenever(delegate.get("myKey", String::class.java)).thenReturn(null) + + val result = wrapper.get("myKey", String::class.java) + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + @Test fun `get with type creates span with cache hit false on miss`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) whenever(delegate.get("myKey", String::class.java)).thenReturn(null) val result = wrapper.get("myKey", String::class.java) @@ -107,6 +125,22 @@ class SentryCacheWrapperTest { assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type sets error status and throwable on exception`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("cache error") + whenever(delegate.get("myKey")).thenReturn(mock()) + whenever(delegate.get("myKey", String::class.java)).thenThrow(exception) + + assertFailsWith { wrapper.get("myKey", String::class.java) } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + } + // -- get(Object key, Callable) -- @Test diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index dab20fd4d52..1b895deea56 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -65,8 +65,9 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return delegate.get(key, type); } try { + final ValueWrapper wrapper = delegate.get(key); + span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null); final T result = delegate.get(key, type); - span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt index 163d0935fc0..74af2721b7e 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -83,6 +83,8 @@ class SentryCacheWrapperTest { fun `get with type creates span with cache hit true on hit`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) whenever(delegate.get("myKey", String::class.java)).thenReturn("value") val result = wrapper.get("myKey", String::class.java) @@ -92,10 +94,26 @@ class SentryCacheWrapperTest { assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type creates span with cache hit true when cached value is null`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val valueWrapper = mock() + whenever(delegate.get("myKey")).thenReturn(valueWrapper) + whenever(delegate.get("myKey", String::class.java)).thenReturn(null) + + val result = wrapper.get("myKey", String::class.java) + + assertNull(result) + assertEquals(1, tx.spans.size) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + } + @Test fun `get with type creates span with cache hit false on miss`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) + whenever(delegate.get("myKey")).thenReturn(null) whenever(delegate.get("myKey", String::class.java)).thenReturn(null) val result = wrapper.get("myKey", String::class.java) @@ -105,6 +123,22 @@ class SentryCacheWrapperTest { assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) } + @Test + fun `get with type sets error status and throwable on exception`() { + val tx = createTransaction() + val wrapper = SentryCacheWrapper(delegate, scopes) + val exception = RuntimeException("cache error") + whenever(delegate.get("myKey")).thenReturn(mock()) + whenever(delegate.get("myKey", String::class.java)).thenThrow(exception) + + assertFailsWith { wrapper.get("myKey", String::class.java) } + + assertEquals(1, tx.spans.size) + val span = tx.spans.first() + assertEquals(SpanStatus.INTERNAL_ERROR, span.status) + assertEquals(exception, span.throwable) + } + // -- get(Object key, Callable) -- @Test From 4b60cec958afdfc31cac356cde73b70d6d224e59 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 05:35:53 +0100 Subject: [PATCH 35/48] docs(jcache): Fix docs link in README Co-Authored-By: Claude Opus 4.6 (1M context) --- sentry-jcache/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-jcache/README.md b/sentry-jcache/README.md index 071950a7a2c..e4f4d8e49a4 100644 --- a/sentry-jcache/README.md +++ b/sentry-jcache/README.md @@ -10,4 +10,4 @@ JCache is a standard API — you need a provider implementation at runtime. Comm - [Apache Ignite](https://ignite.apache.org/) - [Infinispan](https://infinispan.org/) -Please consult the documentation on how to install and use this integration in the Sentry Docs for [Java](https://docs.sentry.io/platforms/java/tracing/instrumentation/jcache/). +Please consult the documentation on how to install and use this integration in the Sentry Docs for [Java](https://docs.sentry.io/platforms/java/integrations/jcache/). From 4e794eff965a12f975727696f59bcb5890bb7d83 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 05:50:37 +0100 Subject: [PATCH 36/48] ref(spring): Use method-specific span operations for cache spans Instead of the 4 generic categories (cache.get, cache.put, cache.remove, cache.flush), use the actual method name as the span operation (e.g. cache.evict, cache.putIfAbsent, cache.retrieve). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../io/sentry/jcache/SentryJCacheWrapper.java | 26 +++++++++---------- .../sentry/jcache/SentryJCacheWrapperTest.kt | 26 +++++++++---------- .../spring7/cache/SentryCacheWrapper.java | 14 +++++----- .../spring7/cache/SentryCacheWrapperTest.kt | 12 ++++----- .../jakarta/cache/SentryCacheWrapper.java | 14 +++++----- .../jakarta/cache/SentryCacheWrapperTest.kt | 12 ++++----- .../spring/cache/SentryCacheWrapper.java | 10 +++---- .../spring/cache/SentryCacheWrapperTest.kt | 10 +++---- 8 files changed, 62 insertions(+), 62 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 81f84f4d7dd..065038aef5f 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -71,7 +71,7 @@ public V get(final K key) { @Override public Map getAll(final Set keys) { - final ISpan span = startSpanForKeys("cache.get", keys, "getAll"); + final ISpan span = startSpanForKeys("cache.getAll", keys, "getAll"); if (span == null) { return delegate.getAll(keys); } @@ -117,7 +117,7 @@ public void put(final K key, final V value) { @Override public V getAndPut(final K key, final V value) { - final ISpan span = startSpan("cache.put", key, "getAndPut"); + final ISpan span = startSpan("cache.getAndPut", key, "getAndPut"); if (span == null) { return delegate.getAndPut(key, value); } @@ -136,7 +136,7 @@ public V getAndPut(final K key, final V value) { @Override public void putAll(final Map map) { - final ISpan span = startSpanForKeys("cache.put", map.keySet(), "putAll"); + final ISpan span = startSpanForKeys("cache.putAll", map.keySet(), "putAll"); if (span == null) { delegate.putAll(map); return; @@ -155,7 +155,7 @@ public void putAll(final Map map) { @Override public boolean putIfAbsent(final K key, final V value) { - final ISpan span = startSpan("cache.put", key, "putIfAbsent"); + final ISpan span = startSpan("cache.putIfAbsent", key, "putIfAbsent"); if (span == null) { return delegate.putIfAbsent(key, value); } @@ -174,7 +174,7 @@ public boolean putIfAbsent(final K key, final V value) { @Override public boolean replace(final K key, final V oldValue, final V newValue) { - final ISpan span = startSpan("cache.put", key, "replace"); + final ISpan span = startSpan("cache.replace", key, "replace"); if (span == null) { return delegate.replace(key, oldValue, newValue); } @@ -193,7 +193,7 @@ public boolean replace(final K key, final V oldValue, final V newValue) { @Override public boolean replace(final K key, final V value) { - final ISpan span = startSpan("cache.put", key, "replace"); + final ISpan span = startSpan("cache.replace", key, "replace"); if (span == null) { return delegate.replace(key, value); } @@ -212,7 +212,7 @@ public boolean replace(final K key, final V value) { @Override public V getAndReplace(final K key, final V value) { - final ISpan span = startSpan("cache.put", key, "getAndReplace"); + final ISpan span = startSpan("cache.getAndReplace", key, "getAndReplace"); if (span == null) { return delegate.getAndReplace(key, value); } @@ -271,7 +271,7 @@ public boolean remove(final K key, final V oldValue) { @Override public V getAndRemove(final K key) { - final ISpan span = startSpan("cache.remove", key, "getAndRemove"); + final ISpan span = startSpan("cache.getAndRemove", key, "getAndRemove"); if (span == null) { return delegate.getAndRemove(key); } @@ -290,7 +290,7 @@ public V getAndRemove(final K key) { @Override public void removeAll(final Set keys) { - final ISpan span = startSpanForKeys("cache.remove", keys, "removeAll"); + final ISpan span = startSpanForKeys("cache.removeAll", keys, "removeAll"); if (span == null) { delegate.removeAll(keys); return; @@ -309,7 +309,7 @@ public void removeAll(final Set keys) { @Override public void removeAll() { - final ISpan span = startSpan("cache.flush", null, "removeAll"); + final ISpan span = startSpan("cache.removeAll", null, "removeAll"); if (span == null) { delegate.removeAll(); return; @@ -330,7 +330,7 @@ public void removeAll() { @Override public void clear() { - final ISpan span = startSpan("cache.flush", null, "clear"); + final ISpan span = startSpan("cache.clear", null, "clear"); if (span == null) { delegate.clear(); return; @@ -358,7 +358,7 @@ public void close() { public T invoke( final K key, final EntryProcessor entryProcessor, final Object... arguments) throws EntryProcessorException { - final ISpan span = startSpan("cache.get", key, "invoke"); + final ISpan span = startSpan("cache.invoke", key, "invoke"); if (span == null) { return delegate.invoke(key, entryProcessor, arguments); } @@ -380,7 +380,7 @@ public Map> invokeAll( final Set keys, final EntryProcessor entryProcessor, final Object... arguments) { - final ISpan span = startSpanForKeys("cache.get", keys, "invokeAll"); + final ISpan span = startSpanForKeys("cache.invokeAll", keys, "invokeAll"); if (span == null) { return delegate.invokeAll(keys, entryProcessor, arguments); } diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt index a0d6548121a..7b3692bbdde 100644 --- a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -94,7 +94,7 @@ class SentryJCacheWrapperTest { assertEquals(mapOf("k1" to "v1"), result) assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.get", span.operation) + assertEquals("cache.getAll", span.operation) assertEquals("testCache", span.description) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> @@ -144,7 +144,7 @@ class SentryJCacheWrapperTest { assertEquals("oldValue", result) assertEquals(1, tx.spans.size) - assertEquals("cache.put", tx.spans.first().operation) + assertEquals("cache.getAndPut", tx.spans.first().operation) assertEquals("getAndPut", tx.spans.first().getData("db.operation.name")) } @@ -161,7 +161,7 @@ class SentryJCacheWrapperTest { verify(delegate).putAll(entries) assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.put", span.operation) + assertEquals("cache.putAll", span.operation) assertEquals("testCache", span.description) val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) @@ -182,7 +182,7 @@ class SentryJCacheWrapperTest { verify(delegate).putIfAbsent("myKey", "myValue") assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.put", span.operation) + assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("putIfAbsent", span.getData("db.operation.name")) @@ -202,7 +202,7 @@ class SentryJCacheWrapperTest { verify(delegate).replace("myKey", "old", "new") assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.put", span.operation) + assertEquals("cache.replace", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals("replace", span.getData("db.operation.name")) } @@ -219,7 +219,7 @@ class SentryJCacheWrapperTest { verify(delegate).replace("myKey", "value") assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.put", span.operation) + assertEquals("cache.replace", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals("replace", span.getData("db.operation.name")) } @@ -238,7 +238,7 @@ class SentryJCacheWrapperTest { verify(delegate).getAndReplace("myKey", "newValue") assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.put", span.operation) + assertEquals("cache.getAndReplace", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals("getAndReplace", span.getData("db.operation.name")) } @@ -289,7 +289,7 @@ class SentryJCacheWrapperTest { assertEquals("value", result) assertEquals(1, tx.spans.size) - assertEquals("cache.remove", tx.spans.first().operation) + assertEquals("cache.getAndRemove", tx.spans.first().operation) assertEquals("getAndRemove", tx.spans.first().getData("db.operation.name")) } @@ -306,7 +306,7 @@ class SentryJCacheWrapperTest { verify(delegate).removeAll(keys) assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.remove", span.operation) + assertEquals("cache.removeAll", span.operation) assertEquals("testCache", span.description) assertEquals("removeAll", span.getData("db.operation.name")) } @@ -322,7 +322,7 @@ class SentryJCacheWrapperTest { verify(delegate).removeAll() assertEquals(1, tx.spans.size) - assertEquals("cache.flush", tx.spans.first().operation) + assertEquals("cache.removeAll", tx.spans.first().operation) assertEquals("removeAll", tx.spans.first().getData("db.operation.name")) } @@ -338,7 +338,7 @@ class SentryJCacheWrapperTest { verify(delegate).clear() assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.flush", span.operation) + assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("clear", span.getData("db.operation.name")) @@ -357,7 +357,7 @@ class SentryJCacheWrapperTest { assertEquals("result", result) assertEquals(1, tx.spans.size) - assertEquals("cache.get", tx.spans.first().operation) + assertEquals("cache.invoke", tx.spans.first().operation) assertEquals("invoke", tx.spans.first().getData("db.operation.name")) } @@ -376,7 +376,7 @@ class SentryJCacheWrapperTest { assertEquals(resultMap, result) assertEquals(1, tx.spans.size) - assertEquals("cache.get", tx.spans.first().operation) + assertEquals("cache.invokeAll", tx.spans.first().operation) assertEquals("invokeAll", tx.spans.first().getData("db.operation.name")) } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 58b803d40e2..1d4d56b9654 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -110,7 +110,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable CompletableFuture retrieve(final @NotNull Object key) { - final ISpan span = startSpan("cache.get", key, "retrieve"); + final ISpan span = startSpan("cache.retrieve", key, "retrieve"); if (span == null) { return delegate.retrieve(key); } @@ -145,7 +145,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public CompletableFuture retrieve( final @NotNull Object key, final @NotNull Supplier> valueLoader) { - final ISpan span = startSpan("cache.get", key, "retrieve"); + final ISpan span = startSpan("cache.retrieve", key, "retrieve"); if (span == null) { return delegate.retrieve(key, valueLoader); } @@ -200,7 +200,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public @Nullable ValueWrapper putIfAbsent( final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.put", key, "putIfAbsent"); + final ISpan span = startSpan("cache.putIfAbsent", key, "putIfAbsent"); if (span == null) { return delegate.putIfAbsent(key, value); } @@ -219,7 +219,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public void evict(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key, "evict"); + final ISpan span = startSpan("cache.evict", key, "evict"); if (span == null) { delegate.evict(key); return; @@ -238,7 +238,7 @@ public void evict(final @NotNull Object key) { @Override public boolean evictIfPresent(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key, "evictIfPresent"); + final ISpan span = startSpan("cache.evictIfPresent", key, "evictIfPresent"); if (span == null) { return delegate.evictIfPresent(key); } @@ -257,7 +257,7 @@ public boolean evictIfPresent(final @NotNull Object key) { @Override public void clear() { - final ISpan span = startSpan("cache.flush", null, "clear"); + final ISpan span = startSpan("cache.clear", null, "clear"); if (span == null) { delegate.clear(); return; @@ -276,7 +276,7 @@ public void clear() { @Override public boolean invalidate() { - final ISpan span = startSpan("cache.flush", null, "invalidate"); + final ISpan span = startSpan("cache.invalidate", null, "invalidate"); if (span == null) { return delegate.invalidate(); } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index d42fcabaeb4..f6e4d75a0af 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -187,7 +187,7 @@ class SentryCacheWrapperTest { assertEquals("value", result!!.get()) assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.get", span.operation) + assertEquals("cache.retrieve", span.operation) assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) @@ -373,7 +373,7 @@ class SentryCacheWrapperTest { verify(delegate).putIfAbsent("myKey", "myValue") assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.put", span.operation) + assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("putIfAbsent", span.getData("db.operation.name")) @@ -391,7 +391,7 @@ class SentryCacheWrapperTest { verify(delegate).evict("myKey") assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.remove", span.operation) + assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals("evict", span.getData("db.operation.name")) } @@ -408,7 +408,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) - assertEquals("cache.remove", tx.spans.first().operation) + assertEquals("cache.evictIfPresent", tx.spans.first().operation) assertEquals("evictIfPresent", tx.spans.first().getData("db.operation.name")) } @@ -424,7 +424,7 @@ class SentryCacheWrapperTest { verify(delegate).clear() assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.flush", span.operation) + assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("clear", span.getData("db.operation.name")) @@ -442,7 +442,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) - assertEquals("cache.flush", tx.spans.first().operation) + assertEquals("cache.invalidate", tx.spans.first().operation) assertEquals("invalidate", tx.spans.first().getData("db.operation.name")) } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index 79ea227dbea..ff7fcc961b6 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -110,7 +110,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable CompletableFuture retrieve(final @NotNull Object key) { - final ISpan span = startSpan("cache.get", key, "retrieve"); + final ISpan span = startSpan("cache.retrieve", key, "retrieve"); if (span == null) { return delegate.retrieve(key); } @@ -145,7 +145,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public CompletableFuture retrieve( final @NotNull Object key, final @NotNull Supplier> valueLoader) { - final ISpan span = startSpan("cache.get", key, "retrieve"); + final ISpan span = startSpan("cache.retrieve", key, "retrieve"); if (span == null) { return delegate.retrieve(key, valueLoader); } @@ -200,7 +200,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public @Nullable ValueWrapper putIfAbsent( final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.put", key, "putIfAbsent"); + final ISpan span = startSpan("cache.putIfAbsent", key, "putIfAbsent"); if (span == null) { return delegate.putIfAbsent(key, value); } @@ -219,7 +219,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public void evict(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key, "evict"); + final ISpan span = startSpan("cache.evict", key, "evict"); if (span == null) { delegate.evict(key); return; @@ -238,7 +238,7 @@ public void evict(final @NotNull Object key) { @Override public boolean evictIfPresent(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key, "evictIfPresent"); + final ISpan span = startSpan("cache.evictIfPresent", key, "evictIfPresent"); if (span == null) { return delegate.evictIfPresent(key); } @@ -257,7 +257,7 @@ public boolean evictIfPresent(final @NotNull Object key) { @Override public void clear() { - final ISpan span = startSpan("cache.flush", null, "clear"); + final ISpan span = startSpan("cache.clear", null, "clear"); if (span == null) { delegate.clear(); return; @@ -276,7 +276,7 @@ public void clear() { @Override public boolean invalidate() { - final ISpan span = startSpan("cache.flush", null, "invalidate"); + final ISpan span = startSpan("cache.invalidate", null, "invalidate"); if (span == null) { return delegate.invalidate(); } diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt index 62cb26a5af1..8bbcf0186b3 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -187,7 +187,7 @@ class SentryCacheWrapperTest { assertEquals("value", result!!.get()) assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.get", span.operation) + assertEquals("cache.retrieve", span.operation) assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) @@ -373,7 +373,7 @@ class SentryCacheWrapperTest { verify(delegate).putIfAbsent("myKey", "myValue") assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.put", span.operation) + assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("putIfAbsent", span.getData("db.operation.name")) @@ -391,7 +391,7 @@ class SentryCacheWrapperTest { verify(delegate).evict("myKey") assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.remove", span.operation) + assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals("evict", span.getData("db.operation.name")) } @@ -408,7 +408,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) - assertEquals("cache.remove", tx.spans.first().operation) + assertEquals("cache.evictIfPresent", tx.spans.first().operation) assertEquals("evictIfPresent", tx.spans.first().getData("db.operation.name")) } @@ -424,7 +424,7 @@ class SentryCacheWrapperTest { verify(delegate).clear() assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.flush", span.operation) + assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("clear", span.getData("db.operation.name")) @@ -442,7 +442,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) - assertEquals("cache.flush", tx.spans.first().operation) + assertEquals("cache.invalidate", tx.spans.first().operation) assertEquals("invalidate", tx.spans.first().getData("db.operation.name")) } diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index 1b895deea56..0be535d7e90 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -128,7 +128,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public @Nullable ValueWrapper putIfAbsent( final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.put", key, "putIfAbsent"); + final ISpan span = startSpan("cache.putIfAbsent", key, "putIfAbsent"); if (span == null) { return delegate.putIfAbsent(key, value); } @@ -147,7 +147,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public void evict(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key, "evict"); + final ISpan span = startSpan("cache.evict", key, "evict"); if (span == null) { delegate.evict(key); return; @@ -166,7 +166,7 @@ public void evict(final @NotNull Object key) { @Override public boolean evictIfPresent(final @NotNull Object key) { - final ISpan span = startSpan("cache.remove", key, "evictIfPresent"); + final ISpan span = startSpan("cache.evictIfPresent", key, "evictIfPresent"); if (span == null) { return delegate.evictIfPresent(key); } @@ -185,7 +185,7 @@ public boolean evictIfPresent(final @NotNull Object key) { @Override public void clear() { - final ISpan span = startSpan("cache.flush", null, "clear"); + final ISpan span = startSpan("cache.clear", null, "clear"); if (span == null) { delegate.clear(); return; @@ -204,7 +204,7 @@ public void clear() { @Override public boolean invalidate() { - final ISpan span = startSpan("cache.flush", null, "invalidate"); + final ISpan span = startSpan("cache.invalidate", null, "invalidate"); if (span == null) { return delegate.invalidate(); } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt index 74af2721b7e..35bf2b30d04 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -204,7 +204,7 @@ class SentryCacheWrapperTest { verify(delegate).putIfAbsent("myKey", "myValue") assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.put", span.operation) + assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("putIfAbsent", span.getData("db.operation.name")) @@ -222,7 +222,7 @@ class SentryCacheWrapperTest { verify(delegate).evict("myKey") assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.remove", span.operation) + assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals("evict", span.getData("db.operation.name")) } @@ -239,7 +239,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) - assertEquals("cache.remove", tx.spans.first().operation) + assertEquals("cache.evictIfPresent", tx.spans.first().operation) assertEquals("evictIfPresent", tx.spans.first().getData("db.operation.name")) } @@ -255,7 +255,7 @@ class SentryCacheWrapperTest { verify(delegate).clear() assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals("cache.flush", span.operation) + assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("clear", span.getData("db.operation.name")) @@ -273,7 +273,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) - assertEquals("cache.flush", tx.spans.first().operation) + assertEquals("cache.invalidate", tx.spans.first().operation) assertEquals("invalidate", tx.spans.first().getData("db.operation.name")) } From 0a326149a0b8657d2bb5d388e06613f6663b86f6 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 06:27:33 +0100 Subject: [PATCH 37/48] ref(spring): Derive span operation from operationName in startSpan Remove redundant first parameter since it was always "cache." + operationName. The prefix is now applied inside the helper method. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../io/sentry/jcache/SentryJCacheWrapper.java | 48 +++++++++---------- .../spring7/cache/SentryCacheWrapper.java | 29 ++++++----- .../jakarta/cache/SentryCacheWrapper.java | 29 ++++++----- .../spring/cache/SentryCacheWrapper.java | 25 +++++----- 4 files changed, 63 insertions(+), 68 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 065038aef5f..0fa7d0844c9 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -51,7 +51,7 @@ public SentryJCacheWrapper(final @NotNull Cache delegate, final @NotNull I @Override public V get(final K key) { - final ISpan span = startSpan("cache.get", key, "get"); + final ISpan span = startSpan(key, "get"); if (span == null) { return delegate.get(key); } @@ -71,7 +71,7 @@ public V get(final K key) { @Override public Map getAll(final Set keys) { - final ISpan span = startSpanForKeys("cache.getAll", keys, "getAll"); + final ISpan span = startSpanForKeys(keys, "getAll"); if (span == null) { return delegate.getAll(keys); } @@ -98,7 +98,7 @@ public boolean containsKey(final K key) { @Override public void put(final K key, final V value) { - final ISpan span = startSpan("cache.put", key, "put"); + final ISpan span = startSpan(key, "put"); if (span == null) { delegate.put(key, value); return; @@ -117,7 +117,7 @@ public void put(final K key, final V value) { @Override public V getAndPut(final K key, final V value) { - final ISpan span = startSpan("cache.getAndPut", key, "getAndPut"); + final ISpan span = startSpan(key, "getAndPut"); if (span == null) { return delegate.getAndPut(key, value); } @@ -136,7 +136,7 @@ public V getAndPut(final K key, final V value) { @Override public void putAll(final Map map) { - final ISpan span = startSpanForKeys("cache.putAll", map.keySet(), "putAll"); + final ISpan span = startSpanForKeys(map.keySet(), "putAll"); if (span == null) { delegate.putAll(map); return; @@ -155,7 +155,7 @@ public void putAll(final Map map) { @Override public boolean putIfAbsent(final K key, final V value) { - final ISpan span = startSpan("cache.putIfAbsent", key, "putIfAbsent"); + final ISpan span = startSpan(key, "putIfAbsent"); if (span == null) { return delegate.putIfAbsent(key, value); } @@ -174,7 +174,7 @@ public boolean putIfAbsent(final K key, final V value) { @Override public boolean replace(final K key, final V oldValue, final V newValue) { - final ISpan span = startSpan("cache.replace", key, "replace"); + final ISpan span = startSpan(key, "replace"); if (span == null) { return delegate.replace(key, oldValue, newValue); } @@ -193,7 +193,7 @@ public boolean replace(final K key, final V oldValue, final V newValue) { @Override public boolean replace(final K key, final V value) { - final ISpan span = startSpan("cache.replace", key, "replace"); + final ISpan span = startSpan(key, "replace"); if (span == null) { return delegate.replace(key, value); } @@ -212,7 +212,7 @@ public boolean replace(final K key, final V value) { @Override public V getAndReplace(final K key, final V value) { - final ISpan span = startSpan("cache.getAndReplace", key, "getAndReplace"); + final ISpan span = startSpan(key, "getAndReplace"); if (span == null) { return delegate.getAndReplace(key, value); } @@ -233,7 +233,7 @@ public V getAndReplace(final K key, final V value) { @Override public boolean remove(final K key) { - final ISpan span = startSpan("cache.remove", key, "remove"); + final ISpan span = startSpan(key, "remove"); if (span == null) { return delegate.remove(key); } @@ -252,7 +252,7 @@ public boolean remove(final K key) { @Override public boolean remove(final K key, final V oldValue) { - final ISpan span = startSpan("cache.remove", key, "remove"); + final ISpan span = startSpan(key, "remove"); if (span == null) { return delegate.remove(key, oldValue); } @@ -271,7 +271,7 @@ public boolean remove(final K key, final V oldValue) { @Override public V getAndRemove(final K key) { - final ISpan span = startSpan("cache.getAndRemove", key, "getAndRemove"); + final ISpan span = startSpan(key, "getAndRemove"); if (span == null) { return delegate.getAndRemove(key); } @@ -290,7 +290,7 @@ public V getAndRemove(final K key) { @Override public void removeAll(final Set keys) { - final ISpan span = startSpanForKeys("cache.removeAll", keys, "removeAll"); + final ISpan span = startSpanForKeys(keys, "removeAll"); if (span == null) { delegate.removeAll(keys); return; @@ -309,7 +309,7 @@ public void removeAll(final Set keys) { @Override public void removeAll() { - final ISpan span = startSpan("cache.removeAll", null, "removeAll"); + final ISpan span = startSpan(null, "removeAll"); if (span == null) { delegate.removeAll(); return; @@ -330,7 +330,7 @@ public void removeAll() { @Override public void clear() { - final ISpan span = startSpan("cache.clear", null, "clear"); + final ISpan span = startSpan(null, "clear"); if (span == null) { delegate.clear(); return; @@ -358,7 +358,7 @@ public void close() { public T invoke( final K key, final EntryProcessor entryProcessor, final Object... arguments) throws EntryProcessorException { - final ISpan span = startSpan("cache.invoke", key, "invoke"); + final ISpan span = startSpan(key, "invoke"); if (span == null) { return delegate.invoke(key, entryProcessor, arguments); } @@ -380,7 +380,7 @@ public Map> invokeAll( final Set keys, final EntryProcessor entryProcessor, final Object... arguments) { - final ISpan span = startSpanForKeys("cache.invokeAll", keys, "invokeAll"); + final ISpan span = startSpanForKeys(keys, "invokeAll"); if (span == null) { return delegate.invokeAll(keys, entryProcessor, arguments); } @@ -453,9 +453,7 @@ public Iterator> iterator() { // -- span helpers -- private @Nullable ISpan startSpan( - final @NotNull String operation, - final @Nullable Object key, - final @NotNull String operationName) { + final @Nullable Object key, final @NotNull String operationName) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -468,7 +466,8 @@ public Iterator> iterator() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + final ISpan span = + activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } @@ -480,9 +479,7 @@ public Iterator> iterator() { } private @Nullable ISpan startSpanForKeys( - final @NotNull String operation, - final @NotNull Set keys, - final @NotNull String operationName) { + final @NotNull Set keys, final @NotNull String operationName) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -494,7 +491,8 @@ public Iterator> iterator() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); - final ISpan span = activeSpan.startChild(operation, delegate.getName(), spanOptions); + final ISpan span = + activeSpan.startChild("cache." + operationName, delegate.getName(), spanOptions); if (span.isNoOp()) { return null; } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 1d4d56b9654..8953ed3e4b4 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -42,7 +42,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable ValueWrapper get(final @NotNull Object key) { - final ISpan span = startSpan("cache.get", key, "get"); + final ISpan span = startSpan(key, "get"); if (span == null) { return delegate.get(key); } @@ -62,7 +62,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @Nullable Class type) { - final ISpan span = startSpan("cache.get", key, "get"); + final ISpan span = startSpan(key, "get"); if (span == null) { return delegate.get(key, type); } @@ -83,7 +83,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @NotNull Callable valueLoader) { - final ISpan span = startSpan("cache.get", key, "get"); + final ISpan span = startSpan(key, "get"); if (span == null) { return delegate.get(key, valueLoader); } @@ -110,7 +110,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable CompletableFuture retrieve(final @NotNull Object key) { - final ISpan span = startSpan("cache.retrieve", key, "retrieve"); + final ISpan span = startSpan(key, "retrieve"); if (span == null) { return delegate.retrieve(key); } @@ -145,7 +145,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public CompletableFuture retrieve( final @NotNull Object key, final @NotNull Supplier> valueLoader) { - final ISpan span = startSpan("cache.retrieve", key, "retrieve"); + final ISpan span = startSpan(key, "retrieve"); if (span == null) { return delegate.retrieve(key, valueLoader); } @@ -180,7 +180,7 @@ public CompletableFuture retrieve( @Override public void put(final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.put", key, "put"); + final ISpan span = startSpan(key, "put"); if (span == null) { delegate.put(key, value); return; @@ -200,7 +200,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public @Nullable ValueWrapper putIfAbsent( final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.putIfAbsent", key, "putIfAbsent"); + final ISpan span = startSpan(key, "putIfAbsent"); if (span == null) { return delegate.putIfAbsent(key, value); } @@ -219,7 +219,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public void evict(final @NotNull Object key) { - final ISpan span = startSpan("cache.evict", key, "evict"); + final ISpan span = startSpan(key, "evict"); if (span == null) { delegate.evict(key); return; @@ -238,7 +238,7 @@ public void evict(final @NotNull Object key) { @Override public boolean evictIfPresent(final @NotNull Object key) { - final ISpan span = startSpan("cache.evictIfPresent", key, "evictIfPresent"); + final ISpan span = startSpan(key, "evictIfPresent"); if (span == null) { return delegate.evictIfPresent(key); } @@ -257,7 +257,7 @@ public boolean evictIfPresent(final @NotNull Object key) { @Override public void clear() { - final ISpan span = startSpan("cache.clear", null, "clear"); + final ISpan span = startSpan(null, "clear"); if (span == null) { delegate.clear(); return; @@ -276,7 +276,7 @@ public void clear() { @Override public boolean invalidate() { - final ISpan span = startSpan("cache.invalidate", null, "invalidate"); + final ISpan span = startSpan(null, "invalidate"); if (span == null) { return delegate.invalidate(); } @@ -294,9 +294,7 @@ public boolean invalidate() { } private @Nullable ISpan startSpan( - final @NotNull String operation, - final @Nullable Object key, - final @NotNull String operationName) { + final @Nullable Object key, final @NotNull String operationName) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -309,7 +307,8 @@ public boolean invalidate() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + final ISpan span = + activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index ff7fcc961b6..95602bd1359 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -42,7 +42,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable ValueWrapper get(final @NotNull Object key) { - final ISpan span = startSpan("cache.get", key, "get"); + final ISpan span = startSpan(key, "get"); if (span == null) { return delegate.get(key); } @@ -62,7 +62,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @Nullable Class type) { - final ISpan span = startSpan("cache.get", key, "get"); + final ISpan span = startSpan(key, "get"); if (span == null) { return delegate.get(key, type); } @@ -83,7 +83,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @NotNull Callable valueLoader) { - final ISpan span = startSpan("cache.get", key, "get"); + final ISpan span = startSpan(key, "get"); if (span == null) { return delegate.get(key, valueLoader); } @@ -110,7 +110,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable CompletableFuture retrieve(final @NotNull Object key) { - final ISpan span = startSpan("cache.retrieve", key, "retrieve"); + final ISpan span = startSpan(key, "retrieve"); if (span == null) { return delegate.retrieve(key); } @@ -145,7 +145,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public CompletableFuture retrieve( final @NotNull Object key, final @NotNull Supplier> valueLoader) { - final ISpan span = startSpan("cache.retrieve", key, "retrieve"); + final ISpan span = startSpan(key, "retrieve"); if (span == null) { return delegate.retrieve(key, valueLoader); } @@ -180,7 +180,7 @@ public CompletableFuture retrieve( @Override public void put(final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.put", key, "put"); + final ISpan span = startSpan(key, "put"); if (span == null) { delegate.put(key, value); return; @@ -200,7 +200,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public @Nullable ValueWrapper putIfAbsent( final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.putIfAbsent", key, "putIfAbsent"); + final ISpan span = startSpan(key, "putIfAbsent"); if (span == null) { return delegate.putIfAbsent(key, value); } @@ -219,7 +219,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public void evict(final @NotNull Object key) { - final ISpan span = startSpan("cache.evict", key, "evict"); + final ISpan span = startSpan(key, "evict"); if (span == null) { delegate.evict(key); return; @@ -238,7 +238,7 @@ public void evict(final @NotNull Object key) { @Override public boolean evictIfPresent(final @NotNull Object key) { - final ISpan span = startSpan("cache.evictIfPresent", key, "evictIfPresent"); + final ISpan span = startSpan(key, "evictIfPresent"); if (span == null) { return delegate.evictIfPresent(key); } @@ -257,7 +257,7 @@ public boolean evictIfPresent(final @NotNull Object key) { @Override public void clear() { - final ISpan span = startSpan("cache.clear", null, "clear"); + final ISpan span = startSpan(null, "clear"); if (span == null) { delegate.clear(); return; @@ -276,7 +276,7 @@ public void clear() { @Override public boolean invalidate() { - final ISpan span = startSpan("cache.invalidate", null, "invalidate"); + final ISpan span = startSpan(null, "invalidate"); if (span == null) { return delegate.invalidate(); } @@ -294,9 +294,7 @@ public boolean invalidate() { } private @Nullable ISpan startSpan( - final @NotNull String operation, - final @Nullable Object key, - final @NotNull String operationName) { + final @Nullable Object key, final @NotNull String operationName) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -309,7 +307,8 @@ public boolean invalidate() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + final ISpan span = + activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index 0be535d7e90..757ff222564 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -40,7 +40,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable ValueWrapper get(final @NotNull Object key) { - final ISpan span = startSpan("cache.get", key, "get"); + final ISpan span = startSpan(key, "get"); if (span == null) { return delegate.get(key); } @@ -60,7 +60,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @Nullable Class type) { - final ISpan span = startSpan("cache.get", key, "get"); + final ISpan span = startSpan(key, "get"); if (span == null) { return delegate.get(key, type); } @@ -81,7 +81,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public @Nullable T get(final @NotNull Object key, final @NotNull Callable valueLoader) { - final ISpan span = startSpan("cache.get", key, "get"); + final ISpan span = startSpan(key, "get"); if (span == null) { return delegate.get(key, valueLoader); } @@ -108,7 +108,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes @Override public void put(final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.put", key, "put"); + final ISpan span = startSpan(key, "put"); if (span == null) { delegate.put(key, value); return; @@ -128,7 +128,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public @Nullable ValueWrapper putIfAbsent( final @NotNull Object key, final @Nullable Object value) { - final ISpan span = startSpan("cache.putIfAbsent", key, "putIfAbsent"); + final ISpan span = startSpan(key, "putIfAbsent"); if (span == null) { return delegate.putIfAbsent(key, value); } @@ -147,7 +147,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { @Override public void evict(final @NotNull Object key) { - final ISpan span = startSpan("cache.evict", key, "evict"); + final ISpan span = startSpan(key, "evict"); if (span == null) { delegate.evict(key); return; @@ -166,7 +166,7 @@ public void evict(final @NotNull Object key) { @Override public boolean evictIfPresent(final @NotNull Object key) { - final ISpan span = startSpan("cache.evictIfPresent", key, "evictIfPresent"); + final ISpan span = startSpan(key, "evictIfPresent"); if (span == null) { return delegate.evictIfPresent(key); } @@ -185,7 +185,7 @@ public boolean evictIfPresent(final @NotNull Object key) { @Override public void clear() { - final ISpan span = startSpan("cache.clear", null, "clear"); + final ISpan span = startSpan(null, "clear"); if (span == null) { delegate.clear(); return; @@ -204,7 +204,7 @@ public void clear() { @Override public boolean invalidate() { - final ISpan span = startSpan("cache.invalidate", null, "invalidate"); + final ISpan span = startSpan(null, "invalidate"); if (span == null) { return delegate.invalidate(); } @@ -222,9 +222,7 @@ public boolean invalidate() { } private @Nullable ISpan startSpan( - final @NotNull String operation, - final @Nullable Object key, - final @NotNull String operationName) { + final @Nullable Object key, final @NotNull String operationName) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -237,7 +235,8 @@ public boolean invalidate() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = activeSpan.startChild(operation, keyString, spanOptions); + final ISpan span = + activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } From 82bc35ed220f5c9f5f9b62f6e31c48c8aefdf374 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 06:34:10 +0100 Subject: [PATCH 38/48] ref(jcache): Merge startSpanForKeys into startSpan overload Replace the separate startSpanForKeys helper with a startSpan(Set, String) overload, unifying the two span creation methods under the same name. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../io/sentry/jcache/SentryJCacheWrapper.java | 42 +++++++------------ .../spring7/cache/SentryCacheWrapper.java | 3 +- .../jakarta/cache/SentryCacheWrapper.java | 3 +- .../spring/cache/SentryCacheWrapper.java | 3 +- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 0fa7d0844c9..2c0eb44e6bc 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -8,6 +8,7 @@ import io.sentry.SpanStatus; import java.util.Arrays; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -454,32 +455,22 @@ public Iterator> iterator() { private @Nullable ISpan startSpan( final @Nullable Object key, final @NotNull String operationName) { - if (!scopes.getOptions().isEnableCacheTracing()) { - return null; - } - - final ISpan activeSpan = scopes.getSpan(); - if (activeSpan == null || activeSpan.isNoOp()) { - return null; - } - - final SpanOptions spanOptions = new SpanOptions(); - spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = - activeSpan.startChild("cache." + operationName, keyString, spanOptions); - if (span.isNoOp()) { - return null; - } - if (keyString != null) { - span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); - } - span.setData(OPERATION_ATTRIBUTE, operationName); - return span; + return startSpan(operationName, keyString, keyString != null ? Arrays.asList(keyString) : null); } private @Nullable ISpan startSpanForKeys( final @NotNull Set keys, final @NotNull String operationName) { + return startSpan( + operationName, + delegate.getName(), + keys.stream().map(String::valueOf).collect(Collectors.toList())); + } + + private @Nullable ISpan startSpan( + final @NotNull String operationName, + final @Nullable String description, + final @Nullable List cacheKeys) { if (!scopes.getOptions().isEnableCacheTracing()) { return null; } @@ -491,14 +482,13 @@ public Iterator> iterator() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); - final ISpan span = - activeSpan.startChild("cache." + operationName, delegate.getName(), spanOptions); + final ISpan span = activeSpan.startChild("cache." + operationName, description, spanOptions); if (span.isNoOp()) { return null; } - span.setData( - SpanDataConvention.CACHE_KEY_KEY, - keys.stream().map(String::valueOf).collect(Collectors.toList())); + if (cacheKeys != null) { + span.setData(SpanDataConvention.CACHE_KEY_KEY, cacheKeys); + } span.setData(OPERATION_ATTRIBUTE, operationName); return span; } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 8953ed3e4b4..114eb979797 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -307,8 +307,7 @@ public boolean invalidate() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = - activeSpan.startChild("cache." + operationName, keyString, spanOptions); + final ISpan span = activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index 95602bd1359..cb6d4b50394 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -307,8 +307,7 @@ public boolean invalidate() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = - activeSpan.startChild("cache." + operationName, keyString, spanOptions); + final ISpan span = activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index 757ff222564..d0cac9304f4 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -235,8 +235,7 @@ public boolean invalidate() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = - activeSpan.startChild("cache." + operationName, keyString, spanOptions); + final ISpan span = activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } From f1dd736403f58ae9414c764d3d67fdb255d339aa Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 18 Mar 2026 09:07:25 +0000 Subject: [PATCH 39/48] Format code --- .../src/main/java/io/sentry/jcache/SentryJCacheWrapper.java | 3 +-- .../main/java/io/sentry/spring7/cache/SentryCacheWrapper.java | 3 +-- .../io/sentry/spring/jakarta/cache/SentryCacheWrapper.java | 3 +-- .../main/java/io/sentry/spring/cache/SentryCacheWrapper.java | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 0fa7d0844c9..831394e75f1 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -466,8 +466,7 @@ public Iterator> iterator() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = - activeSpan.startChild("cache." + operationName, keyString, spanOptions); + final ISpan span = activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 8953ed3e4b4..114eb979797 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -307,8 +307,7 @@ public boolean invalidate() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = - activeSpan.startChild("cache." + operationName, keyString, spanOptions); + final ISpan span = activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index 95602bd1359..cb6d4b50394 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -307,8 +307,7 @@ public boolean invalidate() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = - activeSpan.startChild("cache." + operationName, keyString, spanOptions); + final ISpan span = activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index 757ff222564..d0cac9304f4 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -235,8 +235,7 @@ public boolean invalidate() { final SpanOptions spanOptions = new SpanOptions(); spanOptions.setOrigin(TRACE_ORIGIN); final String keyString = key != null ? String.valueOf(key) : null; - final ISpan span = - activeSpan.startChild("cache." + operationName, keyString, spanOptions); + final ISpan span = activeSpan.startChild("cache." + operationName, keyString, spanOptions); if (span.isNoOp()) { return null; } From bc934356a1a640db76d33bebb150642728cee5cb Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 10:17:48 +0100 Subject: [PATCH 40/48] ref(cache): Move operation attribute to SpanDataConvention as CACHE_OPERATION_KEY Replace local OPERATION_ATTRIBUTE constants in all cache wrappers with a shared CACHE_OPERATION_KEY constant in SpanDataConvention. Also changes the attribute key from "db.operation.name" to "cache.operation". --- .../io/sentry/jcache/SentryJCacheWrapper.java | 3 +- .../sentry/jcache/SentryJCacheWrapperTest.kt | 34 +++++++++---------- .../spring7/cache/SentryCacheWrapper.java | 3 +- .../spring7/cache/SentryCacheWrapperTest.kt | 16 ++++----- .../jakarta/cache/SentryCacheWrapper.java | 3 +- .../jakarta/cache/SentryCacheWrapperTest.kt | 16 ++++----- .../spring/cache/SentryCacheWrapper.java | 3 +- .../spring/cache/SentryCacheWrapperTest.kt | 14 ++++---- sentry/api/sentry.api | 1 + .../java/io/sentry/SpanDataConvention.java | 1 + 10 files changed, 46 insertions(+), 48 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 2c0eb44e6bc..e8377a2aae0 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -34,7 +34,6 @@ public final class SentryJCacheWrapper implements Cache { private static final String TRACE_ORIGIN = "auto.cache.jcache"; - private static final String OPERATION_ATTRIBUTE = "db.operation.name"; private final @NotNull Cache delegate; private final @NotNull IScopes scopes; @@ -489,7 +488,7 @@ public Iterator> iterator() { if (cacheKeys != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, cacheKeys); } - span.setData(OPERATION_ATTRIBUTE, operationName); + span.setData(SpanDataConvention.CACHE_OPERATION_KEY, operationName); return span; } } diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt index 7b3692bbdde..3021b04af04 100644 --- a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -64,7 +64,7 @@ class SentryJCacheWrapperTest { assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.jcache", span.spanContext.origin) - assertEquals("get", span.getData("db.operation.name")) + assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @Test @@ -99,7 +99,7 @@ class SentryJCacheWrapperTest { assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) - assertEquals("getAll", span.getData("db.operation.name")) + assertEquals("getAll", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @Test @@ -129,7 +129,7 @@ class SentryJCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("put", span.getData("db.operation.name")) + assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- getAndPut -- @@ -145,7 +145,7 @@ class SentryJCacheWrapperTest { assertEquals("oldValue", result) assertEquals(1, tx.spans.size) assertEquals("cache.getAndPut", tx.spans.first().operation) - assertEquals("getAndPut", tx.spans.first().getData("db.operation.name")) + assertEquals("getAndPut", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- putAll -- @@ -165,7 +165,7 @@ class SentryJCacheWrapperTest { assertEquals("testCache", span.description) val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) - assertEquals("putAll", span.getData("db.operation.name")) + assertEquals("putAll", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- putIfAbsent -- @@ -185,7 +185,7 @@ class SentryJCacheWrapperTest { assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("putIfAbsent", span.getData("db.operation.name")) + assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- replace -- @@ -204,7 +204,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.replace", span.operation) assertEquals(SpanStatus.OK, span.status) - assertEquals("replace", span.getData("db.operation.name")) + assertEquals("replace", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @Test @@ -221,7 +221,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.replace", span.operation) assertEquals(SpanStatus.OK, span.status) - assertEquals("replace", span.getData("db.operation.name")) + assertEquals("replace", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- getAndReplace -- @@ -240,7 +240,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.getAndReplace", span.operation) assertEquals(SpanStatus.OK, span.status) - assertEquals("getAndReplace", span.getData("db.operation.name")) + assertEquals("getAndReplace", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- remove(K) -- @@ -258,7 +258,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.remove", span.operation) assertEquals(SpanStatus.OK, span.status) - assertEquals("remove", span.getData("db.operation.name")) + assertEquals("remove", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- remove(K, V) -- @@ -274,7 +274,7 @@ class SentryJCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.remove", tx.spans.first().operation) - assertEquals("remove", tx.spans.first().getData("db.operation.name")) + assertEquals("remove", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- getAndRemove -- @@ -290,7 +290,7 @@ class SentryJCacheWrapperTest { assertEquals("value", result) assertEquals(1, tx.spans.size) assertEquals("cache.getAndRemove", tx.spans.first().operation) - assertEquals("getAndRemove", tx.spans.first().getData("db.operation.name")) + assertEquals("getAndRemove", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- removeAll(Set) -- @@ -308,7 +308,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.removeAll", span.operation) assertEquals("testCache", span.description) - assertEquals("removeAll", span.getData("db.operation.name")) + assertEquals("removeAll", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- removeAll() -- @@ -323,7 +323,7 @@ class SentryJCacheWrapperTest { verify(delegate).removeAll() assertEquals(1, tx.spans.size) assertEquals("cache.removeAll", tx.spans.first().operation) - assertEquals("removeAll", tx.spans.first().getData("db.operation.name")) + assertEquals("removeAll", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- clear -- @@ -341,7 +341,7 @@ class SentryJCacheWrapperTest { assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("clear", span.getData("db.operation.name")) + assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- invoke -- @@ -358,7 +358,7 @@ class SentryJCacheWrapperTest { assertEquals("result", result) assertEquals(1, tx.spans.size) assertEquals("cache.invoke", tx.spans.first().operation) - assertEquals("invoke", tx.spans.first().getData("db.operation.name")) + assertEquals("invoke", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- invokeAll -- @@ -377,7 +377,7 @@ class SentryJCacheWrapperTest { assertEquals(resultMap, result) assertEquals(1, tx.spans.size) assertEquals("cache.invokeAll", tx.spans.first().operation) - assertEquals("invokeAll", tx.spans.first().getData("db.operation.name")) + assertEquals("invokeAll", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- passthrough operations -- diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 114eb979797..646e8a09cf0 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -20,7 +20,6 @@ public final class SentryCacheWrapper implements Cache { private static final String TRACE_ORIGIN = "auto.cache.spring"; - private static final String OPERATION_ATTRIBUTE = "db.operation.name"; private final @NotNull Cache delegate; private final @NotNull IScopes scopes; @@ -314,7 +313,7 @@ public boolean invalidate() { if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } - span.setData(OPERATION_ATTRIBUTE, operationName); + span.setData(SpanDataConvention.CACHE_OPERATION_KEY, operationName); return span; } } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index f6e4d75a0af..c42abfcb7ac 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -63,7 +63,7 @@ class SentryCacheWrapperTest { assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) - assertEquals("get", span.getData("db.operation.name")) + assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @Test @@ -191,7 +191,7 @@ class SentryCacheWrapperTest { assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) - assertEquals("retrieve", span.getData("db.operation.name")) + assertEquals("retrieve", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) assertTrue(span.isFinished) } @@ -356,7 +356,7 @@ class SentryCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("put", span.getData("db.operation.name")) + assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- putIfAbsent -- @@ -376,7 +376,7 @@ class SentryCacheWrapperTest { assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("putIfAbsent", span.getData("db.operation.name")) + assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- evict -- @@ -393,7 +393,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) - assertEquals("evict", span.getData("db.operation.name")) + assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- evictIfPresent -- @@ -409,7 +409,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.evictIfPresent", tx.spans.first().operation) - assertEquals("evictIfPresent", tx.spans.first().getData("db.operation.name")) + assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- clear -- @@ -427,7 +427,7 @@ class SentryCacheWrapperTest { assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("clear", span.getData("db.operation.name")) + assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- invalidate -- @@ -443,7 +443,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.invalidate", tx.spans.first().operation) - assertEquals("invalidate", tx.spans.first().getData("db.operation.name")) + assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- no span when no active transaction -- diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index cb6d4b50394..a994bce0b69 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -20,7 +20,6 @@ public final class SentryCacheWrapper implements Cache { private static final String TRACE_ORIGIN = "auto.cache.spring"; - private static final String OPERATION_ATTRIBUTE = "db.operation.name"; private final @NotNull Cache delegate; private final @NotNull IScopes scopes; @@ -314,7 +313,7 @@ public boolean invalidate() { if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } - span.setData(OPERATION_ATTRIBUTE, operationName); + span.setData(SpanDataConvention.CACHE_OPERATION_KEY, operationName); return span; } } diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt index 8bbcf0186b3..1739003fa3c 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -63,7 +63,7 @@ class SentryCacheWrapperTest { assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) - assertEquals("get", span.getData("db.operation.name")) + assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @Test @@ -191,7 +191,7 @@ class SentryCacheWrapperTest { assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) - assertEquals("retrieve", span.getData("db.operation.name")) + assertEquals("retrieve", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) assertTrue(span.isFinished) } @@ -356,7 +356,7 @@ class SentryCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("put", span.getData("db.operation.name")) + assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- putIfAbsent -- @@ -376,7 +376,7 @@ class SentryCacheWrapperTest { assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("putIfAbsent", span.getData("db.operation.name")) + assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- evict -- @@ -393,7 +393,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) - assertEquals("evict", span.getData("db.operation.name")) + assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- evictIfPresent -- @@ -409,7 +409,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.evictIfPresent", tx.spans.first().operation) - assertEquals("evictIfPresent", tx.spans.first().getData("db.operation.name")) + assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- clear -- @@ -427,7 +427,7 @@ class SentryCacheWrapperTest { assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("clear", span.getData("db.operation.name")) + assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- invalidate -- @@ -443,7 +443,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.invalidate", tx.spans.first().operation) - assertEquals("invalidate", tx.spans.first().getData("db.operation.name")) + assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- no span when no active transaction -- diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index d0cac9304f4..eaa9028b47f 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -18,7 +18,6 @@ public final class SentryCacheWrapper implements Cache { private static final String TRACE_ORIGIN = "auto.cache.spring"; - private static final String OPERATION_ATTRIBUTE = "db.operation.name"; private final @NotNull Cache delegate; private final @NotNull IScopes scopes; @@ -242,7 +241,7 @@ public boolean invalidate() { if (keyString != null) { span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); } - span.setData(OPERATION_ATTRIBUTE, operationName); + span.setData(SpanDataConvention.CACHE_OPERATION_KEY, operationName); return span; } } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt index 35bf2b30d04..7eed20b0d48 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -61,7 +61,7 @@ class SentryCacheWrapperTest { assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) - assertEquals("get", span.getData("db.operation.name")) + assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @Test @@ -187,7 +187,7 @@ class SentryCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("put", span.getData("db.operation.name")) + assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- putIfAbsent -- @@ -207,7 +207,7 @@ class SentryCacheWrapperTest { assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("putIfAbsent", span.getData("db.operation.name")) + assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- evict -- @@ -224,7 +224,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) - assertEquals("evict", span.getData("db.operation.name")) + assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- evictIfPresent -- @@ -240,7 +240,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.evictIfPresent", tx.spans.first().operation) - assertEquals("evictIfPresent", tx.spans.first().getData("db.operation.name")) + assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- clear -- @@ -258,7 +258,7 @@ class SentryCacheWrapperTest { assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("clear", span.getData("db.operation.name")) + assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- invalidate -- @@ -274,7 +274,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.invalidate", tx.spans.first().operation) - assertEquals("invalidate", tx.spans.first().getData("db.operation.name")) + assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } // -- no span when no active transaction -- diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 6718fecac1b..c66106ca47a 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4344,6 +4344,7 @@ public abstract interface class io/sentry/SpanDataConvention { public static final field BLOCKED_MAIN_THREAD_KEY Ljava/lang/String; public static final field CACHE_HIT_KEY Ljava/lang/String; public static final field CACHE_KEY_KEY Ljava/lang/String; + public static final field CACHE_OPERATION_KEY Ljava/lang/String; public static final field CALL_STACK_KEY Ljava/lang/String; public static final field CONTRIBUTES_TTFD Ljava/lang/String; public static final field CONTRIBUTES_TTID Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/SpanDataConvention.java b/sentry/src/main/java/io/sentry/SpanDataConvention.java index 8a31a7c70ff..75d688e2dd9 100644 --- a/sentry/src/main/java/io/sentry/SpanDataConvention.java +++ b/sentry/src/main/java/io/sentry/SpanDataConvention.java @@ -28,4 +28,5 @@ public interface SpanDataConvention { String PROFILER_ID = "profiler_id"; String CACHE_HIT_KEY = "cache.hit"; String CACHE_KEY_KEY = "cache.key"; + String CACHE_OPERATION_KEY = "cache.operation"; } From 7426ac713e6042ea577f248c1d48b80ae06bba93 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 11:30:49 +0100 Subject: [PATCH 41/48] feat(cache): Add cache.write boolean span attribute Set cache.write on spans across all four cache wrapper implementations to indicate whether an operation actually modified the cache. This complements the existing cache.hit attribute for read operations. Co-Authored-By: Claude --- .../java/io/sentry/jcache/SentryJCacheWrapper.java | 13 +++++++++++++ .../io/sentry/jcache/SentryJCacheWrapperTest.kt | 14 ++++++++++++++ .../sentry/spring7/cache/SentryCacheWrapper.java | 8 ++++++++ .../sentry/spring7/cache/SentryCacheWrapperTest.kt | 12 ++++++++++++ .../spring/jakarta/cache/SentryCacheWrapper.java | 8 ++++++++ .../spring/jakarta/cache/SentryCacheWrapperTest.kt | 12 ++++++++++++ .../io/sentry/spring/cache/SentryCacheWrapper.java | 7 +++++++ .../sentry/spring/cache/SentryCacheWrapperTest.kt | 9 +++++++++ sentry/api/sentry.api | 1 + .../main/java/io/sentry/SpanDataConvention.java | 1 + 10 files changed, 85 insertions(+) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index e8377a2aae0..6e97c2baa8a 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -105,6 +105,7 @@ public void put(final K key, final V value) { } try { delegate.put(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -123,6 +124,7 @@ public V getAndPut(final K key, final V value) { } try { final V result = delegate.getAndPut(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -143,6 +145,7 @@ public void putAll(final Map map) { } try { delegate.putAll(map); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -161,6 +164,7 @@ public boolean putIfAbsent(final K key, final V value) { } try { final boolean result = delegate.putIfAbsent(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, result); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -180,6 +184,7 @@ public boolean replace(final K key, final V oldValue, final V newValue) { } try { final boolean result = delegate.replace(key, oldValue, newValue); + span.setData(SpanDataConvention.CACHE_WRITE, result); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -199,6 +204,7 @@ public boolean replace(final K key, final V value) { } try { final boolean result = delegate.replace(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, result); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -218,6 +224,7 @@ public V getAndReplace(final K key, final V value) { } try { final V result = delegate.getAndReplace(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -239,6 +246,7 @@ public boolean remove(final K key) { } try { final boolean result = delegate.remove(key); + span.setData(SpanDataConvention.CACHE_WRITE, result); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -258,6 +266,7 @@ public boolean remove(final K key, final V oldValue) { } try { final boolean result = delegate.remove(key, oldValue); + span.setData(SpanDataConvention.CACHE_WRITE, result); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -277,6 +286,7 @@ public V getAndRemove(final K key) { } try { final V result = delegate.getAndRemove(key); + span.setData(SpanDataConvention.CACHE_WRITE, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -297,6 +307,7 @@ public void removeAll(final Set keys) { } try { delegate.removeAll(keys); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -316,6 +327,7 @@ public void removeAll() { } try { delegate.removeAll(); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -337,6 +349,7 @@ public void clear() { } try { delegate.clear(); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt index 3021b04af04..dd3f9f59bf7 100644 --- a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -62,6 +62,7 @@ class SentryJCacheWrapperTest { assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.jcache", span.spanContext.origin) assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) @@ -128,6 +129,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -145,6 +147,7 @@ class SentryJCacheWrapperTest { assertEquals("oldValue", result) assertEquals(1, tx.spans.size) assertEquals("cache.getAndPut", tx.spans.first().operation) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("getAndPut", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -163,6 +166,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.putAll", span.operation) assertEquals("testCache", span.description) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) assertEquals("putAll", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) @@ -184,6 +188,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -204,6 +209,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.replace", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("replace", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -221,6 +227,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.replace", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("replace", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -240,6 +247,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.getAndReplace", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("getAndReplace", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -258,6 +266,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.remove", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("remove", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -274,6 +283,7 @@ class SentryJCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.remove", tx.spans.first().operation) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("remove", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -290,6 +300,7 @@ class SentryJCacheWrapperTest { assertEquals("value", result) assertEquals(1, tx.spans.size) assertEquals("cache.getAndRemove", tx.spans.first().operation) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("getAndRemove", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -308,6 +319,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.removeAll", span.operation) assertEquals("testCache", span.description) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("removeAll", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -323,6 +335,7 @@ class SentryJCacheWrapperTest { verify(delegate).removeAll() assertEquals(1, tx.spans.size) assertEquals("cache.removeAll", tx.spans.first().operation) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("removeAll", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -340,6 +353,7 @@ class SentryJCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 646e8a09cf0..38ff9b7d4d8 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -96,6 +96,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return valueLoader.call(); }); span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setData(SpanDataConvention.CACHE_WRITE, loaderInvoked.get()); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -171,6 +172,7 @@ public CompletableFuture retrieve( span.setThrowable(throwable); } else { span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setData(SpanDataConvention.CACHE_WRITE, loaderInvoked.get()); span.setStatus(SpanStatus.OK); } span.finish(); @@ -186,6 +188,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { } try { delegate.put(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -205,6 +208,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { } try { final ValueWrapper result = delegate.putIfAbsent(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, result == null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -225,6 +229,7 @@ public void evict(final @NotNull Object key) { } try { delegate.evict(key); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -243,6 +248,7 @@ public boolean evictIfPresent(final @NotNull Object key) { } try { final boolean result = delegate.evictIfPresent(key); + span.setData(SpanDataConvention.CACHE_WRITE, result); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -263,6 +269,7 @@ public void clear() { } try { delegate.clear(); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -281,6 +288,7 @@ public boolean invalidate() { } try { final boolean result = delegate.invalidate(); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index c42abfcb7ac..8f3867dd500 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -61,6 +61,7 @@ class SentryCacheWrapperTest { assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) @@ -155,6 +156,7 @@ class SentryCacheWrapperTest { assertEquals("cached", result) assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } @Test @@ -172,6 +174,7 @@ class SentryCacheWrapperTest { assertEquals("loaded", result) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } // -- retrieve(Object key) -- @@ -191,6 +194,7 @@ class SentryCacheWrapperTest { assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("retrieve", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) assertTrue(span.isFinished) } @@ -287,6 +291,7 @@ class SentryCacheWrapperTest { assertEquals("cached", result.get()) assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertTrue(tx.spans.first().isFinished) } @@ -306,6 +311,7 @@ class SentryCacheWrapperTest { assertEquals("loaded", result.get()) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertTrue(tx.spans.first().isFinished) } @@ -355,6 +361,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -375,6 +382,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -393,6 +401,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -409,6 +418,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.evictIfPresent", tx.spans.first().operation) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -426,6 +436,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -443,6 +454,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.invalidate", tx.spans.first().operation) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index a994bce0b69..61405c02768 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -96,6 +96,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return valueLoader.call(); }); span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setData(SpanDataConvention.CACHE_WRITE, loaderInvoked.get()); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -171,6 +172,7 @@ public CompletableFuture retrieve( span.setThrowable(throwable); } else { span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setData(SpanDataConvention.CACHE_WRITE, loaderInvoked.get()); span.setStatus(SpanStatus.OK); } span.finish(); @@ -186,6 +188,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { } try { delegate.put(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -205,6 +208,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { } try { final ValueWrapper result = delegate.putIfAbsent(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, result == null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -225,6 +229,7 @@ public void evict(final @NotNull Object key) { } try { delegate.evict(key); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -243,6 +248,7 @@ public boolean evictIfPresent(final @NotNull Object key) { } try { final boolean result = delegate.evictIfPresent(key); + span.setData(SpanDataConvention.CACHE_WRITE, result); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -263,6 +269,7 @@ public void clear() { } try { delegate.clear(); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -281,6 +288,7 @@ public boolean invalidate() { } try { final boolean result = delegate.invalidate(); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt index 1739003fa3c..d366e7574fc 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -61,6 +61,7 @@ class SentryCacheWrapperTest { assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) @@ -155,6 +156,7 @@ class SentryCacheWrapperTest { assertEquals("cached", result) assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } @Test @@ -172,6 +174,7 @@ class SentryCacheWrapperTest { assertEquals("loaded", result) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } // -- retrieve(Object key) -- @@ -191,6 +194,7 @@ class SentryCacheWrapperTest { assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("retrieve", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) assertTrue(span.isFinished) } @@ -287,6 +291,7 @@ class SentryCacheWrapperTest { assertEquals("cached", result.get()) assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertTrue(tx.spans.first().isFinished) } @@ -306,6 +311,7 @@ class SentryCacheWrapperTest { assertEquals("loaded", result.get()) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertTrue(tx.spans.first().isFinished) } @@ -355,6 +361,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -375,6 +382,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -393,6 +401,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -409,6 +418,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.evictIfPresent", tx.spans.first().operation) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -426,6 +436,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -443,6 +454,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.invalidate", tx.spans.first().operation) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index eaa9028b47f..e4deceae291 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -94,6 +94,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return valueLoader.call(); }); span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setData(SpanDataConvention.CACHE_WRITE, loaderInvoked.get()); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -114,6 +115,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { } try { delegate.put(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -133,6 +135,7 @@ public void put(final @NotNull Object key, final @Nullable Object value) { } try { final ValueWrapper result = delegate.putIfAbsent(key, value); + span.setData(SpanDataConvention.CACHE_WRITE, result == null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -153,6 +156,7 @@ public void evict(final @NotNull Object key) { } try { delegate.evict(key); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -171,6 +175,7 @@ public boolean evictIfPresent(final @NotNull Object key) { } try { final boolean result = delegate.evictIfPresent(key); + span.setData(SpanDataConvention.CACHE_WRITE, result); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -191,6 +196,7 @@ public void clear() { } try { delegate.clear(); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); } catch (Throwable e) { span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -209,6 +215,7 @@ public boolean invalidate() { } try { final boolean result = delegate.invalidate(); + span.setData(SpanDataConvention.CACHE_WRITE, true); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt index 7eed20b0d48..11e078e86f2 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -59,6 +59,7 @@ class SentryCacheWrapperTest { assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) @@ -153,6 +154,7 @@ class SentryCacheWrapperTest { assertEquals("cached", result) assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } @Test @@ -170,6 +172,7 @@ class SentryCacheWrapperTest { assertEquals("loaded", result) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } // -- put -- @@ -186,6 +189,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -206,6 +210,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -224,6 +229,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -240,6 +246,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.evictIfPresent", tx.spans.first().operation) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -257,6 +264,7 @@ class SentryCacheWrapperTest { val span = tx.spans.first() assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) + assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } @@ -274,6 +282,7 @@ class SentryCacheWrapperTest { assertTrue(result) assertEquals(1, tx.spans.size) assertEquals("cache.invalidate", tx.spans.first().operation) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index c66106ca47a..c9d3da2f443 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4345,6 +4345,7 @@ public abstract interface class io/sentry/SpanDataConvention { public static final field CACHE_HIT_KEY Ljava/lang/String; public static final field CACHE_KEY_KEY Ljava/lang/String; public static final field CACHE_OPERATION_KEY Ljava/lang/String; + public static final field CACHE_WRITE Ljava/lang/String; public static final field CALL_STACK_KEY Ljava/lang/String; public static final field CONTRIBUTES_TTFD Ljava/lang/String; public static final field CONTRIBUTES_TTID Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/SpanDataConvention.java b/sentry/src/main/java/io/sentry/SpanDataConvention.java index 75d688e2dd9..cbb6d0ceb7d 100644 --- a/sentry/src/main/java/io/sentry/SpanDataConvention.java +++ b/sentry/src/main/java/io/sentry/SpanDataConvention.java @@ -29,4 +29,5 @@ public interface SpanDataConvention { String CACHE_HIT_KEY = "cache.hit"; String CACHE_KEY_KEY = "cache.key"; String CACHE_OPERATION_KEY = "cache.operation"; + String CACHE_WRITE = "cache.write"; } From 867c67e2c94e395e602757f0e9d48d66321c2bfe Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 12:26:29 +0100 Subject: [PATCH 42/48] fix(jcache): Use comma-joined keys as span description for bulk operations Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/java/io/sentry/jcache/SentryJCacheWrapper.java | 6 ++---- .../kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt | 9 ++++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 6e97c2baa8a..5977dc10adc 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -473,10 +473,8 @@ public Iterator> iterator() { private @Nullable ISpan startSpanForKeys( final @NotNull Set keys, final @NotNull String operationName) { - return startSpan( - operationName, - delegate.getName(), - keys.stream().map(String::valueOf).collect(Collectors.toList())); + final List keyStrings = keys.stream().map(String::valueOf).collect(Collectors.toList()); + return startSpan(operationName, String.join(", ", keyStrings), keyStrings); } private @Nullable ISpan startSpan( diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt index dd3f9f59bf7..220e75e4131 100644 --- a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -96,7 +96,8 @@ class SentryJCacheWrapperTest { assertEquals(1, tx.spans.size) val span = tx.spans.first() assertEquals("cache.getAll", span.operation) - assertEquals("testCache", span.description) + assertTrue(span.description!!.contains("k1")) + assertTrue(span.description!!.contains("k2")) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) @@ -165,7 +166,8 @@ class SentryJCacheWrapperTest { assertEquals(1, tx.spans.size) val span = tx.spans.first() assertEquals("cache.putAll", span.operation) - assertEquals("testCache", span.description) + assertTrue(span.description!!.contains("k1")) + assertTrue(span.description!!.contains("k2")) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) @@ -318,7 +320,8 @@ class SentryJCacheWrapperTest { assertEquals(1, tx.spans.size) val span = tx.spans.first() assertEquals("cache.removeAll", span.operation) - assertEquals("testCache", span.description) + assertTrue(span.description!!.contains("k1")) + assertTrue(span.description!!.contains("k2")) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("removeAll", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) } From 518e465aa79138b08b39fb84070e64cb9be7be77 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 12:37:31 +0100 Subject: [PATCH 43/48] ref(cache): Remove _KEY suffix from cache SpanDataConvention constants Rename CACHE_HIT_KEY, CACHE_KEY_KEY, and CACHE_OPERATION_KEY to CACHE_HIT, CACHE_KEY, and CACHE_OPERATION to match the newer naming convention used by CACHE_WRITE, THREAD_ID, FRAMES_TOTAL, etc. Co-Authored-By: Claude --- .../io/sentry/jcache/SentryJCacheWrapper.java | 8 +-- .../sentry/jcache/SentryJCacheWrapperTest.kt | 54 +++++++++---------- .../spring7/cache/SentryCacheWrapper.java | 16 +++--- .../spring7/cache/SentryCacheWrapperTest.kt | 48 ++++++++--------- .../jakarta/cache/SentryCacheWrapper.java | 16 +++--- .../jakarta/cache/SentryCacheWrapperTest.kt | 48 ++++++++--------- .../spring/cache/SentryCacheWrapper.java | 10 ++-- .../spring/cache/SentryCacheWrapperTest.kt | 36 ++++++------- sentry/api/sentry.api | 6 +-- .../java/io/sentry/SpanDataConvention.java | 6 +-- 10 files changed, 124 insertions(+), 124 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 5977dc10adc..3b3d1c4c2be 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -57,7 +57,7 @@ public V get(final K key) { } try { final V result = delegate.get(key); - span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setData(SpanDataConvention.CACHE_HIT, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -77,7 +77,7 @@ public Map getAll(final Set keys) { } try { final Map result = delegate.getAll(keys); - span.setData(SpanDataConvention.CACHE_HIT_KEY, !result.isEmpty()); + span.setData(SpanDataConvention.CACHE_HIT, !result.isEmpty()); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -497,9 +497,9 @@ public Iterator> iterator() { return null; } if (cacheKeys != null) { - span.setData(SpanDataConvention.CACHE_KEY_KEY, cacheKeys); + span.setData(SpanDataConvention.CACHE_KEY, cacheKeys); } - span.setData(SpanDataConvention.CACHE_OPERATION_KEY, operationName); + span.setData(SpanDataConvention.CACHE_OPERATION, operationName); return span; } } diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt index 220e75e4131..8dfbada0a27 100644 --- a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -61,11 +61,11 @@ class SentryJCacheWrapperTest { assertEquals("cache.get", span.operation) assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) - assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT)) assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) assertEquals("auto.cache.jcache", span.spanContext.origin) - assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION)) } @Test @@ -78,7 +78,7 @@ class SentryJCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } // -- getAll -- @@ -98,10 +98,10 @@ class SentryJCacheWrapperTest { assertEquals("cache.getAll", span.operation) assertTrue(span.description!!.contains("k1")) assertTrue(span.description!!.contains("k2")) - assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) - val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT)) + val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY) as List<*> assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) - assertEquals("getAll", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("getAll", span.getData(SpanDataConvention.CACHE_OPERATION)) } @Test @@ -113,7 +113,7 @@ class SentryJCacheWrapperTest { wrapper.getAll(keys) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } // -- put -- @@ -131,8 +131,8 @@ class SentryJCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- getAndPut -- @@ -149,7 +149,7 @@ class SentryJCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals("cache.getAndPut", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("getAndPut", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("getAndPut", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- putAll -- @@ -169,9 +169,9 @@ class SentryJCacheWrapperTest { assertTrue(span.description!!.contains("k1")) assertTrue(span.description!!.contains("k2")) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY_KEY) as List<*> + val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY) as List<*> assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) - assertEquals("putAll", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("putAll", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- putIfAbsent -- @@ -191,8 +191,8 @@ class SentryJCacheWrapperTest { assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- replace -- @@ -212,7 +212,7 @@ class SentryJCacheWrapperTest { assertEquals("cache.replace", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("replace", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("replace", span.getData(SpanDataConvention.CACHE_OPERATION)) } @Test @@ -230,7 +230,7 @@ class SentryJCacheWrapperTest { assertEquals("cache.replace", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("replace", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("replace", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- getAndReplace -- @@ -250,7 +250,7 @@ class SentryJCacheWrapperTest { assertEquals("cache.getAndReplace", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("getAndReplace", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("getAndReplace", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- remove(K) -- @@ -269,7 +269,7 @@ class SentryJCacheWrapperTest { assertEquals("cache.remove", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("remove", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("remove", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- remove(K, V) -- @@ -286,7 +286,7 @@ class SentryJCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals("cache.remove", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("remove", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("remove", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- getAndRemove -- @@ -303,7 +303,7 @@ class SentryJCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals("cache.getAndRemove", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("getAndRemove", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("getAndRemove", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- removeAll(Set) -- @@ -323,7 +323,7 @@ class SentryJCacheWrapperTest { assertTrue(span.description!!.contains("k1")) assertTrue(span.description!!.contains("k2")) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("removeAll", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("removeAll", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- removeAll() -- @@ -339,7 +339,7 @@ class SentryJCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals("cache.removeAll", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("removeAll", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("removeAll", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- clear -- @@ -357,8 +357,8 @@ class SentryJCacheWrapperTest { assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertNull(span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- invoke -- @@ -375,7 +375,7 @@ class SentryJCacheWrapperTest { assertEquals("result", result) assertEquals(1, tx.spans.size) assertEquals("cache.invoke", tx.spans.first().operation) - assertEquals("invoke", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("invoke", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- invokeAll -- @@ -394,7 +394,7 @@ class SentryJCacheWrapperTest { assertEquals(resultMap, result) assertEquals(1, tx.spans.size) assertEquals("cache.invokeAll", tx.spans.first().operation) - assertEquals("invokeAll", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("invokeAll", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- passthrough operations -- diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 38ff9b7d4d8..1d881afaf7a 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -47,7 +47,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes } try { final ValueWrapper result = delegate.get(key); - span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setData(SpanDataConvention.CACHE_HIT, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -67,7 +67,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes } try { final ValueWrapper wrapper = delegate.get(key); - span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null); + span.setData(SpanDataConvention.CACHE_HIT, wrapper != null); final T result = delegate.get(key, type); span.setStatus(SpanStatus.OK); return result; @@ -95,7 +95,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes loaderInvoked.set(true); return valueLoader.call(); }); - span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setData(SpanDataConvention.CACHE_HIT, !loaderInvoked.get()); span.setData(SpanDataConvention.CACHE_WRITE, loaderInvoked.get()); span.setStatus(SpanStatus.OK); return result; @@ -124,7 +124,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes throw e; } if (result == null) { - span.setData(SpanDataConvention.CACHE_HIT_KEY, false); + span.setData(SpanDataConvention.CACHE_HIT, false); span.setStatus(SpanStatus.OK); span.finish(); return null; @@ -135,7 +135,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes span.setStatus(SpanStatus.INTERNAL_ERROR); span.setThrowable(throwable); } else { - span.setData(SpanDataConvention.CACHE_HIT_KEY, value != null); + span.setData(SpanDataConvention.CACHE_HIT, value != null); span.setStatus(SpanStatus.OK); } span.finish(); @@ -171,7 +171,7 @@ public CompletableFuture retrieve( span.setStatus(SpanStatus.INTERNAL_ERROR); span.setThrowable(throwable); } else { - span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setData(SpanDataConvention.CACHE_HIT, !loaderInvoked.get()); span.setData(SpanDataConvention.CACHE_WRITE, loaderInvoked.get()); span.setStatus(SpanStatus.OK); } @@ -319,9 +319,9 @@ public boolean invalidate() { return null; } if (keyString != null) { - span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); + span.setData(SpanDataConvention.CACHE_KEY, Arrays.asList(keyString)); } - span.setData(SpanDataConvention.CACHE_OPERATION_KEY, operationName); + span.setData(SpanDataConvention.CACHE_OPERATION, operationName); return span; } } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index 8f3867dd500..8d38e93c4e3 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -60,11 +60,11 @@ class SentryCacheWrapperTest { assertEquals("cache.get", span.operation) assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) - assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT)) assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) - assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION)) } @Test @@ -77,7 +77,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } // -- get(Object key, Class) -- @@ -94,7 +94,7 @@ class SentryCacheWrapperTest { assertEquals("value", result) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } @Test @@ -109,7 +109,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } @Test @@ -123,7 +123,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } @Test @@ -155,7 +155,7 @@ class SentryCacheWrapperTest { assertEquals("cached", result) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } @@ -173,7 +173,7 @@ class SentryCacheWrapperTest { assertEquals("loaded", result) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } @@ -193,9 +193,9 @@ class SentryCacheWrapperTest { assertEquals("cache.retrieve", span.operation) assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) - assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT)) assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("retrieve", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("retrieve", span.getData(SpanDataConvention.CACHE_OPERATION)) assertTrue(span.isFinished) } @@ -209,7 +209,7 @@ class SentryCacheWrapperTest { assertNull(result!!.get()) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertTrue(tx.spans.first().isFinished) } @@ -224,7 +224,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals(false, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, span.getData(SpanDataConvention.CACHE_HIT)) assertEquals(SpanStatus.OK, span.status) assertTrue(span.isFinished) } @@ -290,7 +290,7 @@ class SentryCacheWrapperTest { assertEquals("cached", result.get()) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertTrue(tx.spans.first().isFinished) } @@ -310,7 +310,7 @@ class SentryCacheWrapperTest { assertEquals("loaded", result.get()) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertTrue(tx.spans.first().isFinished) } @@ -362,8 +362,8 @@ class SentryCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- putIfAbsent -- @@ -383,8 +383,8 @@ class SentryCacheWrapperTest { assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- evict -- @@ -402,7 +402,7 @@ class SentryCacheWrapperTest { assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- evictIfPresent -- @@ -419,7 +419,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals("cache.evictIfPresent", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- clear -- @@ -437,8 +437,8 @@ class SentryCacheWrapperTest { assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertNull(span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- invalidate -- @@ -455,7 +455,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals("cache.invalidate", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- no span when no active transaction -- diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index 61405c02768..961d08f042b 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -47,7 +47,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes } try { final ValueWrapper result = delegate.get(key); - span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setData(SpanDataConvention.CACHE_HIT, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -67,7 +67,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes } try { final ValueWrapper wrapper = delegate.get(key); - span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null); + span.setData(SpanDataConvention.CACHE_HIT, wrapper != null); final T result = delegate.get(key, type); span.setStatus(SpanStatus.OK); return result; @@ -95,7 +95,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes loaderInvoked.set(true); return valueLoader.call(); }); - span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setData(SpanDataConvention.CACHE_HIT, !loaderInvoked.get()); span.setData(SpanDataConvention.CACHE_WRITE, loaderInvoked.get()); span.setStatus(SpanStatus.OK); return result; @@ -124,7 +124,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes throw e; } if (result == null) { - span.setData(SpanDataConvention.CACHE_HIT_KEY, false); + span.setData(SpanDataConvention.CACHE_HIT, false); span.setStatus(SpanStatus.OK); span.finish(); return null; @@ -135,7 +135,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes span.setStatus(SpanStatus.INTERNAL_ERROR); span.setThrowable(throwable); } else { - span.setData(SpanDataConvention.CACHE_HIT_KEY, value != null); + span.setData(SpanDataConvention.CACHE_HIT, value != null); span.setStatus(SpanStatus.OK); } span.finish(); @@ -171,7 +171,7 @@ public CompletableFuture retrieve( span.setStatus(SpanStatus.INTERNAL_ERROR); span.setThrowable(throwable); } else { - span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setData(SpanDataConvention.CACHE_HIT, !loaderInvoked.get()); span.setData(SpanDataConvention.CACHE_WRITE, loaderInvoked.get()); span.setStatus(SpanStatus.OK); } @@ -319,9 +319,9 @@ public boolean invalidate() { return null; } if (keyString != null) { - span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); + span.setData(SpanDataConvention.CACHE_KEY, Arrays.asList(keyString)); } - span.setData(SpanDataConvention.CACHE_OPERATION_KEY, operationName); + span.setData(SpanDataConvention.CACHE_OPERATION, operationName); return span; } } diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt index d366e7574fc..fae2e9dce5c 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -60,11 +60,11 @@ class SentryCacheWrapperTest { assertEquals("cache.get", span.operation) assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) - assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT)) assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) - assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION)) } @Test @@ -77,7 +77,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } // -- get(Object key, Class) -- @@ -94,7 +94,7 @@ class SentryCacheWrapperTest { assertEquals("value", result) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } @Test @@ -109,7 +109,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } @Test @@ -123,7 +123,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } @Test @@ -155,7 +155,7 @@ class SentryCacheWrapperTest { assertEquals("cached", result) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } @@ -173,7 +173,7 @@ class SentryCacheWrapperTest { assertEquals("loaded", result) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } @@ -193,9 +193,9 @@ class SentryCacheWrapperTest { assertEquals("cache.retrieve", span.operation) assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) - assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT)) assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("retrieve", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("retrieve", span.getData(SpanDataConvention.CACHE_OPERATION)) assertTrue(span.isFinished) } @@ -209,7 +209,7 @@ class SentryCacheWrapperTest { assertNull(result!!.get()) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertTrue(tx.spans.first().isFinished) } @@ -224,7 +224,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) val span = tx.spans.first() - assertEquals(false, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, span.getData(SpanDataConvention.CACHE_HIT)) assertEquals(SpanStatus.OK, span.status) assertTrue(span.isFinished) } @@ -290,7 +290,7 @@ class SentryCacheWrapperTest { assertEquals("cached", result.get()) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertTrue(tx.spans.first().isFinished) } @@ -310,7 +310,7 @@ class SentryCacheWrapperTest { assertEquals("loaded", result.get()) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertTrue(tx.spans.first().isFinished) } @@ -362,8 +362,8 @@ class SentryCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- putIfAbsent -- @@ -383,8 +383,8 @@ class SentryCacheWrapperTest { assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- evict -- @@ -402,7 +402,7 @@ class SentryCacheWrapperTest { assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- evictIfPresent -- @@ -419,7 +419,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals("cache.evictIfPresent", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- clear -- @@ -437,8 +437,8 @@ class SentryCacheWrapperTest { assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertNull(span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- invalidate -- @@ -455,7 +455,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals("cache.invalidate", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- no span when no active transaction -- diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index e4deceae291..ab8692afc32 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -45,7 +45,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes } try { final ValueWrapper result = delegate.get(key); - span.setData(SpanDataConvention.CACHE_HIT_KEY, result != null); + span.setData(SpanDataConvention.CACHE_HIT, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { @@ -65,7 +65,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes } try { final ValueWrapper wrapper = delegate.get(key); - span.setData(SpanDataConvention.CACHE_HIT_KEY, wrapper != null); + span.setData(SpanDataConvention.CACHE_HIT, wrapper != null); final T result = delegate.get(key, type); span.setStatus(SpanStatus.OK); return result; @@ -93,7 +93,7 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes loaderInvoked.set(true); return valueLoader.call(); }); - span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get()); + span.setData(SpanDataConvention.CACHE_HIT, !loaderInvoked.get()); span.setData(SpanDataConvention.CACHE_WRITE, loaderInvoked.get()); span.setStatus(SpanStatus.OK); return result; @@ -246,9 +246,9 @@ public boolean invalidate() { return null; } if (keyString != null) { - span.setData(SpanDataConvention.CACHE_KEY_KEY, Arrays.asList(keyString)); + span.setData(SpanDataConvention.CACHE_KEY, Arrays.asList(keyString)); } - span.setData(SpanDataConvention.CACHE_OPERATION_KEY, operationName); + span.setData(SpanDataConvention.CACHE_OPERATION, operationName); return span; } } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt index 11e078e86f2..4f40886c41e 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -58,11 +58,11 @@ class SentryCacheWrapperTest { assertEquals("cache.get", span.operation) assertEquals("myKey", span.description) assertEquals(SpanStatus.OK, span.status) - assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT)) assertNull(span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) assertEquals("auto.cache.spring", span.spanContext.origin) - assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("get", span.getData(SpanDataConvention.CACHE_OPERATION)) } @Test @@ -75,7 +75,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } // -- get(Object key, Class) -- @@ -92,7 +92,7 @@ class SentryCacheWrapperTest { assertEquals("value", result) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } @Test @@ -107,7 +107,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } @Test @@ -121,7 +121,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } @Test @@ -153,7 +153,7 @@ class SentryCacheWrapperTest { assertEquals("cached", result) assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } @@ -171,7 +171,7 @@ class SentryCacheWrapperTest { assertEquals("loaded", result) assertEquals(1, tx.spans.size) - assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY)) + assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) } @@ -190,8 +190,8 @@ class SentryCacheWrapperTest { assertEquals("cache.put", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("put", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- putIfAbsent -- @@ -211,8 +211,8 @@ class SentryCacheWrapperTest { assertEquals("cache.putIfAbsent", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("putIfAbsent", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- evict -- @@ -230,7 +230,7 @@ class SentryCacheWrapperTest { assertEquals("cache.evict", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- evictIfPresent -- @@ -247,7 +247,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals("cache.evictIfPresent", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- clear -- @@ -265,8 +265,8 @@ class SentryCacheWrapperTest { assertEquals("cache.clear", span.operation) assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - assertNull(span.getData(SpanDataConvention.CACHE_KEY_KEY)) - assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertNull(span.getData(SpanDataConvention.CACHE_KEY)) + assertEquals("clear", span.getData(SpanDataConvention.CACHE_OPERATION)) } // -- invalidate -- @@ -283,7 +283,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals("cache.invalidate", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) - assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION_KEY)) + assertEquals("invalidate", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) } // -- no span when no active transaction -- diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index c9d3da2f443..6fa2cb29df4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4342,9 +4342,9 @@ public final class io/sentry/SpanContext$JsonKeys { public abstract interface class io/sentry/SpanDataConvention { public static final field BLOCKED_MAIN_THREAD_KEY Ljava/lang/String; - public static final field CACHE_HIT_KEY Ljava/lang/String; - public static final field CACHE_KEY_KEY Ljava/lang/String; - public static final field CACHE_OPERATION_KEY Ljava/lang/String; + public static final field CACHE_HIT Ljava/lang/String; + public static final field CACHE_KEY Ljava/lang/String; + public static final field CACHE_OPERATION Ljava/lang/String; public static final field CACHE_WRITE Ljava/lang/String; public static final field CALL_STACK_KEY Ljava/lang/String; public static final field CONTRIBUTES_TTFD Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/SpanDataConvention.java b/sentry/src/main/java/io/sentry/SpanDataConvention.java index cbb6d0ceb7d..647c0dacddf 100644 --- a/sentry/src/main/java/io/sentry/SpanDataConvention.java +++ b/sentry/src/main/java/io/sentry/SpanDataConvention.java @@ -26,8 +26,8 @@ public interface SpanDataConvention { String HTTP_START_TIMESTAMP = "http.start_timestamp"; String HTTP_END_TIMESTAMP = "http.end_timestamp"; String PROFILER_ID = "profiler_id"; - String CACHE_HIT_KEY = "cache.hit"; - String CACHE_KEY_KEY = "cache.key"; - String CACHE_OPERATION_KEY = "cache.operation"; + String CACHE_HIT = "cache.hit"; + String CACHE_KEY = "cache.key"; + String CACHE_OPERATION = "cache.operation"; String CACHE_WRITE = "cache.write"; } From 83dfbfac8d2d5dc79c60ca55528e4ef16d1ec018 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 13:24:09 +0100 Subject: [PATCH 44/48] fix(spring): Fix get(key, type) double-call in SentryCacheWrapper Use a single delegate.get(key, type) call instead of calling delegate.get(key) for hit detection and delegate.get(key, type) for the actual value. This eliminates doubled cache round trips (e.g. Redis network calls) and a TOCTOU race where the entry could expire between the two calls. The trade-off is that cached null values are now indistinguishable from cache misses, which is acceptable for observability purposes. --- .../spring7/cache/SentryCacheWrapper.java | 3 +-- .../spring7/cache/SentryCacheWrapperTest.kt | 19 ------------------- .../jakarta/cache/SentryCacheWrapper.java | 3 +-- .../jakarta/cache/SentryCacheWrapperTest.kt | 19 ------------------- .../spring/cache/SentryCacheWrapper.java | 3 +-- .../spring/cache/SentryCacheWrapperTest.kt | 19 ------------------- 6 files changed, 3 insertions(+), 63 deletions(-) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 1d881afaf7a..07b57e24925 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -66,9 +66,8 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return delegate.get(key, type); } try { - final ValueWrapper wrapper = delegate.get(key); - span.setData(SpanDataConvention.CACHE_HIT, wrapper != null); final T result = delegate.get(key, type); + span.setData(SpanDataConvention.CACHE_HIT, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index 8d38e93c4e3..33b44970958 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -86,8 +86,6 @@ class SentryCacheWrapperTest { fun `get with type creates span with cache hit true on hit`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) - val valueWrapper = mock() - whenever(delegate.get("myKey")).thenReturn(valueWrapper) whenever(delegate.get("myKey", String::class.java)).thenReturn("value") val result = wrapper.get("myKey", String::class.java) @@ -97,26 +95,10 @@ class SentryCacheWrapperTest { assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } - @Test - fun `get with type creates span with cache hit true when cached value is null`() { - val tx = createTransaction() - val wrapper = SentryCacheWrapper(delegate, scopes) - val valueWrapper = mock() - whenever(delegate.get("myKey")).thenReturn(valueWrapper) - whenever(delegate.get("myKey", String::class.java)).thenReturn(null) - - val result = wrapper.get("myKey", String::class.java) - - assertNull(result) - assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) - } - @Test fun `get with type creates span with cache hit false on miss`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) - whenever(delegate.get("myKey")).thenReturn(null) whenever(delegate.get("myKey", String::class.java)).thenReturn(null) val result = wrapper.get("myKey", String::class.java) @@ -131,7 +113,6 @@ class SentryCacheWrapperTest { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) val exception = RuntimeException("cache error") - whenever(delegate.get("myKey")).thenReturn(mock()) whenever(delegate.get("myKey", String::class.java)).thenThrow(exception) assertFailsWith { wrapper.get("myKey", String::class.java) } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index 961d08f042b..47b60c3ff69 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -66,9 +66,8 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return delegate.get(key, type); } try { - final ValueWrapper wrapper = delegate.get(key); - span.setData(SpanDataConvention.CACHE_HIT, wrapper != null); final T result = delegate.get(key, type); + span.setData(SpanDataConvention.CACHE_HIT, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt index fae2e9dce5c..869308ec659 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -86,8 +86,6 @@ class SentryCacheWrapperTest { fun `get with type creates span with cache hit true on hit`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) - val valueWrapper = mock() - whenever(delegate.get("myKey")).thenReturn(valueWrapper) whenever(delegate.get("myKey", String::class.java)).thenReturn("value") val result = wrapper.get("myKey", String::class.java) @@ -97,26 +95,10 @@ class SentryCacheWrapperTest { assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } - @Test - fun `get with type creates span with cache hit true when cached value is null`() { - val tx = createTransaction() - val wrapper = SentryCacheWrapper(delegate, scopes) - val valueWrapper = mock() - whenever(delegate.get("myKey")).thenReturn(valueWrapper) - whenever(delegate.get("myKey", String::class.java)).thenReturn(null) - - val result = wrapper.get("myKey", String::class.java) - - assertNull(result) - assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) - } - @Test fun `get with type creates span with cache hit false on miss`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) - whenever(delegate.get("myKey")).thenReturn(null) whenever(delegate.get("myKey", String::class.java)).thenReturn(null) val result = wrapper.get("myKey", String::class.java) @@ -131,7 +113,6 @@ class SentryCacheWrapperTest { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) val exception = RuntimeException("cache error") - whenever(delegate.get("myKey")).thenReturn(mock()) whenever(delegate.get("myKey", String::class.java)).thenThrow(exception) assertFailsWith { wrapper.get("myKey", String::class.java) } diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index ab8692afc32..8860d205068 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -64,9 +64,8 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes return delegate.get(key, type); } try { - final ValueWrapper wrapper = delegate.get(key); - span.setData(SpanDataConvention.CACHE_HIT, wrapper != null); final T result = delegate.get(key, type); + span.setData(SpanDataConvention.CACHE_HIT, result != null); span.setStatus(SpanStatus.OK); return result; } catch (Throwable e) { diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt index 4f40886c41e..405f266cdd4 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -84,8 +84,6 @@ class SentryCacheWrapperTest { fun `get with type creates span with cache hit true on hit`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) - val valueWrapper = mock() - whenever(delegate.get("myKey")).thenReturn(valueWrapper) whenever(delegate.get("myKey", String::class.java)).thenReturn("value") val result = wrapper.get("myKey", String::class.java) @@ -95,26 +93,10 @@ class SentryCacheWrapperTest { assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) } - @Test - fun `get with type creates span with cache hit true when cached value is null`() { - val tx = createTransaction() - val wrapper = SentryCacheWrapper(delegate, scopes) - val valueWrapper = mock() - whenever(delegate.get("myKey")).thenReturn(valueWrapper) - whenever(delegate.get("myKey", String::class.java)).thenReturn(null) - - val result = wrapper.get("myKey", String::class.java) - - assertNull(result) - assertEquals(1, tx.spans.size) - assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) - } - @Test fun `get with type creates span with cache hit false on miss`() { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) - whenever(delegate.get("myKey")).thenReturn(null) whenever(delegate.get("myKey", String::class.java)).thenReturn(null) val result = wrapper.get("myKey", String::class.java) @@ -129,7 +111,6 @@ class SentryCacheWrapperTest { val tx = createTransaction() val wrapper = SentryCacheWrapper(delegate, scopes) val exception = RuntimeException("cache error") - whenever(delegate.get("myKey")).thenReturn(mock()) whenever(delegate.get("myKey", String::class.java)).thenThrow(exception) assertFailsWith { wrapper.get("myKey", String::class.java) } From f9831a9d94b3c97f57a23cd4d514b3f23a97b4e5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 18 Mar 2026 14:18:57 +0100 Subject: [PATCH 45/48] fix(samples): Fix cache evict system test to match actual span op --- .../src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt | 2 +- .../src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt | 2 +- .../src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt | 2 +- .../src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt | 2 +- .../src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt | 2 +- .../src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt | 2 +- .../src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt | 2 +- .../src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt | 2 +- .../src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt index 7853750a8fb..b45e9c10853 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -45,7 +45,7 @@ class CacheSystemTest { restClient.deleteCachedTodo(1L) testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.remove") + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.evict") } } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-otlp/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt index 7853750a8fb..b45e9c10853 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-otlp/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-otlp/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -45,7 +45,7 @@ class CacheSystemTest { restClient.deleteCachedTodo(1L) testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.remove") + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.evict") } } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt index 7853750a8fb..b45e9c10853 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -45,7 +45,7 @@ class CacheSystemTest { restClient.deleteCachedTodo(1L) testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.remove") + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.evict") } } } diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt index 7853750a8fb..b45e9c10853 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -45,7 +45,7 @@ class CacheSystemTest { restClient.deleteCachedTodo(1L) testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.remove") + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.evict") } } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt index 7853750a8fb..b45e9c10853 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -45,7 +45,7 @@ class CacheSystemTest { restClient.deleteCachedTodo(1L) testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.remove") + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.evict") } } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt index 7853750a8fb..b45e9c10853 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -45,7 +45,7 @@ class CacheSystemTest { restClient.deleteCachedTodo(1L) testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.remove") + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.evict") } } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt index 7853750a8fb..b45e9c10853 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -45,7 +45,7 @@ class CacheSystemTest { restClient.deleteCachedTodo(1L) testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.remove") + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.evict") } } } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt index 7853750a8fb..b45e9c10853 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -45,7 +45,7 @@ class CacheSystemTest { restClient.deleteCachedTodo(1L) testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.remove") + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.evict") } } } diff --git a/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt index 7853750a8fb..b45e9c10853 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt @@ -45,7 +45,7 @@ class CacheSystemTest { restClient.deleteCachedTodo(1L) testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.remove") + testHelper.doesTransactionContainSpanWithOp(transaction, "cache.evict") } } } From 9bc1f3a52080fa99a61c0fe283b73721442d5ef3 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 23 Mar 2026 14:29:44 +0100 Subject: [PATCH 46/48] assert multiple keys in single assertions --- .../io/sentry/jcache/SentryJCacheWrapper.java | 4 ++-- .../io/sentry/jcache/SentryJCacheWrapperTest.kt | 16 ++++++---------- .../sentry/spring7/cache/SentryCacheWrapper.java | 4 ++-- .../spring7/cache/SentryCacheWrapperTest.kt | 11 +++++++++++ .../spring/jakarta/cache/SentryCacheWrapper.java | 4 ++-- .../jakarta/cache/SentryCacheWrapperTest.kt | 11 +++++++++++ .../sentry/spring/cache/SentryCacheWrapper.java | 4 ++-- .../spring/cache/SentryCacheWrapperTest.kt | 7 +++++++ 8 files changed, 43 insertions(+), 18 deletions(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 3b3d1c4c2be..37e3d970d3e 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -6,7 +6,7 @@ import io.sentry.SpanDataConvention; import io.sentry.SpanOptions; import io.sentry.SpanStatus; -import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -468,7 +468,7 @@ public Iterator> iterator() { private @Nullable ISpan startSpan( final @Nullable Object key, final @NotNull String operationName) { final String keyString = key != null ? String.valueOf(key) : null; - return startSpan(operationName, keyString, keyString != null ? Arrays.asList(keyString) : null); + return startSpan(operationName, keyString, keyString != null ? Collections.singletonList(keyString) : null); } private @Nullable ISpan startSpanForKeys( diff --git a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt index 8dfbada0a27..9e523d3df21 100644 --- a/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt +++ b/sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt @@ -96,11 +96,9 @@ class SentryJCacheWrapperTest { assertEquals(1, tx.spans.size) val span = tx.spans.first() assertEquals("cache.getAll", span.operation) - assertTrue(span.description!!.contains("k1")) - assertTrue(span.description!!.contains("k2")) + assertEquals("k1, k2", span.description) assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT)) - val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY) as List<*> - assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) + assertEquals(listOf("k1", "k2"), span.getData(SpanDataConvention.CACHE_KEY)) assertEquals("getAll", span.getData(SpanDataConvention.CACHE_OPERATION)) } @@ -166,11 +164,9 @@ class SentryJCacheWrapperTest { assertEquals(1, tx.spans.size) val span = tx.spans.first() assertEquals("cache.putAll", span.operation) - assertTrue(span.description!!.contains("k1")) - assertTrue(span.description!!.contains("k2")) + assertEquals("k1, k2", span.description) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) - val cacheKeys = span.getData(SpanDataConvention.CACHE_KEY) as List<*> - assertTrue(cacheKeys.containsAll(listOf("k1", "k2"))) + assertEquals(listOf("k1", "k2"), span.getData(SpanDataConvention.CACHE_KEY)) assertEquals("putAll", span.getData(SpanDataConvention.CACHE_OPERATION)) } @@ -320,9 +316,9 @@ class SentryJCacheWrapperTest { assertEquals(1, tx.spans.size) val span = tx.spans.first() assertEquals("cache.removeAll", span.operation) - assertTrue(span.description!!.contains("k1")) - assertTrue(span.description!!.contains("k2")) + assertEquals("k1, k2", span.description) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("k1", "k2"), span.getData(SpanDataConvention.CACHE_KEY)) assertEquals("removeAll", span.getData(SpanDataConvention.CACHE_OPERATION)) } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java index 07b57e24925..f53f7a3ce70 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java @@ -5,7 +5,7 @@ import io.sentry.SpanDataConvention; import io.sentry.SpanOptions; import io.sentry.SpanStatus; -import java.util.Arrays; +import java.util.Collections; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; @@ -318,7 +318,7 @@ public boolean invalidate() { return null; } if (keyString != null) { - span.setData(SpanDataConvention.CACHE_KEY, Arrays.asList(keyString)); + span.setData(SpanDataConvention.CACHE_KEY, Collections.singletonList(keyString)); } span.setData(SpanDataConvention.CACHE_OPERATION, operationName); return span; diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt index 33b44970958..ced7f491032 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt @@ -78,6 +78,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } // -- get(Object key, Class) -- @@ -93,6 +94,7 @@ class SentryCacheWrapperTest { assertEquals("value", result) assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } @Test @@ -106,6 +108,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } @Test @@ -138,6 +141,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } @Test @@ -156,6 +160,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } // -- retrieve(Object key) -- @@ -191,6 +196,7 @@ class SentryCacheWrapperTest { assertNull(result!!.get()) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) assertTrue(tx.spans.first().isFinished) } @@ -206,6 +212,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) val span = tx.spans.first() assertEquals(false, span.getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) assertEquals(SpanStatus.OK, span.status) assertTrue(span.isFinished) } @@ -273,6 +280,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) assertTrue(tx.spans.first().isFinished) } @@ -293,6 +301,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) assertTrue(tx.spans.first().isFinished) } @@ -384,6 +393,7 @@ class SentryCacheWrapperTest { assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) } // -- evictIfPresent -- @@ -401,6 +411,7 @@ class SentryCacheWrapperTest { assertEquals("cache.evictIfPresent", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } // -- clear -- diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java index 47b60c3ff69..c09661b304e 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java @@ -5,7 +5,7 @@ import io.sentry.SpanDataConvention; import io.sentry.SpanOptions; import io.sentry.SpanStatus; -import java.util.Arrays; +import java.util.Collections; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; @@ -318,7 +318,7 @@ public boolean invalidate() { return null; } if (keyString != null) { - span.setData(SpanDataConvention.CACHE_KEY, Arrays.asList(keyString)); + span.setData(SpanDataConvention.CACHE_KEY, Collections.singletonList(keyString)); } span.setData(SpanDataConvention.CACHE_OPERATION, operationName); return span; diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt index 869308ec659..7aa0791cf08 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt @@ -78,6 +78,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } // -- get(Object key, Class) -- @@ -93,6 +94,7 @@ class SentryCacheWrapperTest { assertEquals("value", result) assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } @Test @@ -106,6 +108,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } @Test @@ -138,6 +141,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } @Test @@ -156,6 +160,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } // -- retrieve(Object key) -- @@ -191,6 +196,7 @@ class SentryCacheWrapperTest { assertNull(result!!.get()) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) assertTrue(tx.spans.first().isFinished) } @@ -206,6 +212,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) val span = tx.spans.first() assertEquals(false, span.getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) assertEquals(SpanStatus.OK, span.status) assertTrue(span.isFinished) } @@ -273,6 +280,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) assertTrue(tx.spans.first().isFinished) } @@ -293,6 +301,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) assertTrue(tx.spans.first().isFinished) } @@ -384,6 +393,7 @@ class SentryCacheWrapperTest { assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) } // -- evictIfPresent -- @@ -401,6 +411,7 @@ class SentryCacheWrapperTest { assertEquals("cache.evictIfPresent", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } // -- clear -- diff --git a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java index 8860d205068..39608e35b2b 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java +++ b/sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java @@ -5,7 +5,7 @@ import io.sentry.SpanDataConvention; import io.sentry.SpanOptions; import io.sentry.SpanStatus; -import java.util.Arrays; +import java.util.Collections; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.ApiStatus; @@ -245,7 +245,7 @@ public boolean invalidate() { return null; } if (keyString != null) { - span.setData(SpanDataConvention.CACHE_KEY, Arrays.asList(keyString)); + span.setData(SpanDataConvention.CACHE_KEY, Collections.singletonList(keyString)); } span.setData(SpanDataConvention.CACHE_OPERATION, operationName); return span; diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt index 405f266cdd4..9feb8fd9637 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt @@ -76,6 +76,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } // -- get(Object key, Class) -- @@ -91,6 +92,7 @@ class SentryCacheWrapperTest { assertEquals("value", result) assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } @Test @@ -104,6 +106,7 @@ class SentryCacheWrapperTest { assertNull(result) assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } @Test @@ -136,6 +139,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } @Test @@ -154,6 +158,7 @@ class SentryCacheWrapperTest { assertEquals(1, tx.spans.size) assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT)) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } // -- put -- @@ -212,6 +217,7 @@ class SentryCacheWrapperTest { assertEquals(SpanStatus.OK, span.status) assertEquals(true, span.getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evict", span.getData(SpanDataConvention.CACHE_OPERATION)) + assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY)) } // -- evictIfPresent -- @@ -229,6 +235,7 @@ class SentryCacheWrapperTest { assertEquals("cache.evictIfPresent", tx.spans.first().operation) assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_WRITE)) assertEquals("evictIfPresent", tx.spans.first().getData(SpanDataConvention.CACHE_OPERATION)) + assertEquals(listOf("myKey"), tx.spans.first().getData(SpanDataConvention.CACHE_KEY)) } // -- clear -- From 638152a5b9c0ce7aa3951dc7120d8608ed9b4553 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 23 Mar 2026 13:41:01 +0000 Subject: [PATCH 47/48] Format code --- .../src/main/java/io/sentry/jcache/SentryJCacheWrapper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java index 37e3d970d3e..e1c3b786e85 100644 --- a/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java +++ b/sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java @@ -468,7 +468,8 @@ public Iterator> iterator() { private @Nullable ISpan startSpan( final @Nullable Object key, final @NotNull String operationName) { final String keyString = key != null ? String.valueOf(key) : null; - return startSpan(operationName, keyString, keyString != null ? Collections.singletonList(keyString) : null); + return startSpan( + operationName, keyString, keyString != null ? Collections.singletonList(keyString) : null); } private @Nullable ISpan startSpanForKeys( From 616aeab41c65654ce44fbc877be521e3730ae10d Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 23 Mar 2026 16:30:35 +0100 Subject: [PATCH 48/48] update PR links in changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4425c3a9ee..39193516ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,10 @@ ### Features -- 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)) +- Add cache tracing instrumentation for Spring Boot 2, 3, and 4 ([#5165](https://github.com/getsentry/sentry-java/pull/5165)) - Wraps Spring `CacheManager` and `Cache` beans to produce cache 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)) +- Add JCache (JSR-107) cache tracing via new `sentry-jcache` module ([#5165](https://github.com/getsentry/sentry-java/pull/5165)) - Wraps JCache `Cache` with `SentryJCacheWrapper` to produce cache spans - Set the `enableCacheTracing` option to `true` to enable this feature - Android: Add `beforeErrorSampling` callback to Session Replay ([#5214](https://github.com/getsentry/sentry-java/pull/5214))