Skip to content

Commit b8911d9

Browse files
adinauerclaude
andcommitted
feat(spring): Instrument putIfAbsent, replace, and getAndReplace cache operations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5d8a445 commit b8911d9

File tree

8 files changed

+159
-43
lines changed

8 files changed

+159
-43
lines changed

sentry-jcache/src/main/java/io/sentry/jcache/SentryJCacheWrapper.java

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,29 +153,80 @@ public void putAll(final Map<? extends K, ? extends V> map) {
153153
}
154154
}
155155

156-
// putIfAbsent is not instrumented — we cannot know ahead of time whether the put
157-
// will actually happen, and emitting a cache.put span for a no-op would be misleading.
158156
@Override
159157
public boolean putIfAbsent(final K key, final V value) {
160-
return delegate.putIfAbsent(key, value);
158+
final ISpan span = startSpan("cache.put", key, "putIfAbsent");
159+
if (span == null) {
160+
return delegate.putIfAbsent(key, value);
161+
}
162+
try {
163+
final boolean result = delegate.putIfAbsent(key, value);
164+
span.setStatus(SpanStatus.OK);
165+
return result;
166+
} catch (Throwable e) {
167+
span.setStatus(SpanStatus.INTERNAL_ERROR);
168+
span.setThrowable(e);
169+
throw e;
170+
} finally {
171+
span.finish();
172+
}
161173
}
162174

163-
// replace and getAndReplace are not instrumented — like putIfAbsent, they are conditional
164-
// writes (only happen if the key exists / value matches). Emitting a cache.put span for a
165-
// potential no-op would be misleading.
166175
@Override
167176
public boolean replace(final K key, final V oldValue, final V newValue) {
168-
return delegate.replace(key, oldValue, newValue);
177+
final ISpan span = startSpan("cache.put", key, "replace");
178+
if (span == null) {
179+
return delegate.replace(key, oldValue, newValue);
180+
}
181+
try {
182+
final boolean result = delegate.replace(key, oldValue, newValue);
183+
span.setStatus(SpanStatus.OK);
184+
return result;
185+
} catch (Throwable e) {
186+
span.setStatus(SpanStatus.INTERNAL_ERROR);
187+
span.setThrowable(e);
188+
throw e;
189+
} finally {
190+
span.finish();
191+
}
169192
}
170193

171194
@Override
172195
public boolean replace(final K key, final V value) {
173-
return delegate.replace(key, value);
196+
final ISpan span = startSpan("cache.put", key, "replace");
197+
if (span == null) {
198+
return delegate.replace(key, value);
199+
}
200+
try {
201+
final boolean result = delegate.replace(key, value);
202+
span.setStatus(SpanStatus.OK);
203+
return result;
204+
} catch (Throwable e) {
205+
span.setStatus(SpanStatus.INTERNAL_ERROR);
206+
span.setThrowable(e);
207+
throw e;
208+
} finally {
209+
span.finish();
210+
}
174211
}
175212

176213
@Override
177214
public V getAndReplace(final K key, final V value) {
178-
return delegate.getAndReplace(key, value);
215+
final ISpan span = startSpan("cache.put", key, "getAndReplace");
216+
if (span == null) {
217+
return delegate.getAndReplace(key, value);
218+
}
219+
try {
220+
final V result = delegate.getAndReplace(key, value);
221+
span.setStatus(SpanStatus.OK);
222+
return result;
223+
} catch (Throwable e) {
224+
span.setStatus(SpanStatus.INTERNAL_ERROR);
225+
span.setThrowable(e);
226+
throw e;
227+
} finally {
228+
span.finish();
229+
}
179230
}
180231

181232
// -- remove operations --

sentry-jcache/src/test/kotlin/io/sentry/jcache/SentryJCacheWrapperTest.kt

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ class SentryJCacheWrapperTest {
171171
// -- putIfAbsent --
172172

173173
@Test
174-
fun `putIfAbsent delegates without creating span`() {
174+
fun `putIfAbsent creates cache put span`() {
175175
val tx = createTransaction()
176176
val wrapper = SentryJCacheWrapper(delegate, scopes)
177177
whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(true)
@@ -180,13 +180,18 @@ class SentryJCacheWrapperTest {
180180

181181
assertTrue(result)
182182
verify(delegate).putIfAbsent("myKey", "myValue")
183-
assertEquals(0, tx.spans.size)
183+
assertEquals(1, tx.spans.size)
184+
val span = tx.spans.first()
185+
assertEquals("cache.put", span.operation)
186+
assertEquals(SpanStatus.OK, span.status)
187+
assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY))
188+
assertEquals("putIfAbsent", span.getData("db.operation.name"))
184189
}
185190

186-
// -- replace (passthrough, no span — conditional write) --
191+
// -- replace --
187192

188193
@Test
189-
fun `replace with old value delegates without creating span`() {
194+
fun `replace with old value creates cache put span`() {
190195
val tx = createTransaction()
191196
val wrapper = SentryJCacheWrapper(delegate, scopes)
192197
whenever(delegate.replace("myKey", "old", "new")).thenReturn(true)
@@ -195,11 +200,15 @@ class SentryJCacheWrapperTest {
195200

196201
assertTrue(result)
197202
verify(delegate).replace("myKey", "old", "new")
198-
assertEquals(0, tx.spans.size)
203+
assertEquals(1, tx.spans.size)
204+
val span = tx.spans.first()
205+
assertEquals("cache.put", span.operation)
206+
assertEquals(SpanStatus.OK, span.status)
207+
assertEquals("replace", span.getData("db.operation.name"))
199208
}
200209

201210
@Test
202-
fun `replace delegates without creating span`() {
211+
fun `replace creates cache put span`() {
203212
val tx = createTransaction()
204213
val wrapper = SentryJCacheWrapper(delegate, scopes)
205214
whenever(delegate.replace("myKey", "value")).thenReturn(true)
@@ -208,13 +217,17 @@ class SentryJCacheWrapperTest {
208217

209218
assertTrue(result)
210219
verify(delegate).replace("myKey", "value")
211-
assertEquals(0, tx.spans.size)
220+
assertEquals(1, tx.spans.size)
221+
val span = tx.spans.first()
222+
assertEquals("cache.put", span.operation)
223+
assertEquals(SpanStatus.OK, span.status)
224+
assertEquals("replace", span.getData("db.operation.name"))
212225
}
213226

214-
// -- getAndReplace (passthrough, no span — conditional write) --
227+
// -- getAndReplace --
215228

216229
@Test
217-
fun `getAndReplace delegates without creating span`() {
230+
fun `getAndReplace creates cache put span`() {
218231
val tx = createTransaction()
219232
val wrapper = SentryJCacheWrapper(delegate, scopes)
220233
whenever(delegate.getAndReplace("myKey", "newValue")).thenReturn("oldValue")
@@ -223,7 +236,11 @@ class SentryJCacheWrapperTest {
223236

224237
assertEquals("oldValue", result)
225238
verify(delegate).getAndReplace("myKey", "newValue")
226-
assertEquals(0, tx.spans.size)
239+
assertEquals(1, tx.spans.size)
240+
val span = tx.spans.first()
241+
assertEquals("cache.put", span.operation)
242+
assertEquals(SpanStatus.OK, span.status)
243+
assertEquals("getAndReplace", span.getData("db.operation.name"))
227244
}
228245

229246
// -- remove(K) --

sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,24 @@ public void put(final @NotNull Object key, final @Nullable Object value) {
196196
}
197197
}
198198

199-
// putIfAbsent is not instrumented — we cannot know ahead of time whether the put
200-
// will actually happen, and emitting a cache.put span for a no-op would be misleading.
201-
// This matches sentry-python and sentry-javascript which also skip conditional puts.
202-
// We must override to bypass the default implementation which calls this.get() + this.put().
203199
@Override
204200
public @Nullable ValueWrapper putIfAbsent(
205201
final @NotNull Object key, final @Nullable Object value) {
206-
return delegate.putIfAbsent(key, value);
202+
final ISpan span = startSpan("cache.put", key, "putIfAbsent");
203+
if (span == null) {
204+
return delegate.putIfAbsent(key, value);
205+
}
206+
try {
207+
final ValueWrapper result = delegate.putIfAbsent(key, value);
208+
span.setStatus(SpanStatus.OK);
209+
return result;
210+
} catch (Throwable e) {
211+
span.setStatus(SpanStatus.INTERNAL_ERROR);
212+
span.setThrowable(e);
213+
throw e;
214+
} finally {
215+
span.finish();
216+
}
207217
}
208218

209219
@Override

sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,15 +328,21 @@ class SentryCacheWrapperTest {
328328
// -- putIfAbsent --
329329

330330
@Test
331-
fun `putIfAbsent delegates without creating span`() {
331+
fun `putIfAbsent creates cache put span`() {
332332
val tx = createTransaction()
333333
val wrapper = SentryCacheWrapper(delegate, scopes)
334334
whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(null)
335335

336-
wrapper.putIfAbsent("myKey", "myValue")
336+
val result = wrapper.putIfAbsent("myKey", "myValue")
337337

338+
assertNull(result)
338339
verify(delegate).putIfAbsent("myKey", "myValue")
339-
assertEquals(0, tx.spans.size)
340+
assertEquals(1, tx.spans.size)
341+
val span = tx.spans.first()
342+
assertEquals("cache.put", span.operation)
343+
assertEquals(SpanStatus.OK, span.status)
344+
assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY))
345+
assertEquals("putIfAbsent", span.getData("db.operation.name"))
340346
}
341347

342348
// -- evict --

sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/cache/SentryCacheWrapper.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,24 @@ public void put(final @NotNull Object key, final @Nullable Object value) {
196196
}
197197
}
198198

199-
// putIfAbsent is not instrumented — we cannot know ahead of time whether the put
200-
// will actually happen, and emitting a cache.put span for a no-op would be misleading.
201-
// This matches sentry-python and sentry-javascript which also skip conditional puts.
202-
// We must override to bypass the default implementation which calls this.get() + this.put().
203199
@Override
204200
public @Nullable ValueWrapper putIfAbsent(
205201
final @NotNull Object key, final @Nullable Object value) {
206-
return delegate.putIfAbsent(key, value);
202+
final ISpan span = startSpan("cache.put", key, "putIfAbsent");
203+
if (span == null) {
204+
return delegate.putIfAbsent(key, value);
205+
}
206+
try {
207+
final ValueWrapper result = delegate.putIfAbsent(key, value);
208+
span.setStatus(SpanStatus.OK);
209+
return result;
210+
} catch (Throwable e) {
211+
span.setStatus(SpanStatus.INTERNAL_ERROR);
212+
span.setThrowable(e);
213+
throw e;
214+
} finally {
215+
span.finish();
216+
}
207217
}
208218

209219
@Override

sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/cache/SentryCacheWrapperTest.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,15 +328,21 @@ class SentryCacheWrapperTest {
328328
// -- putIfAbsent --
329329

330330
@Test
331-
fun `putIfAbsent delegates without creating span`() {
331+
fun `putIfAbsent creates cache put span`() {
332332
val tx = createTransaction()
333333
val wrapper = SentryCacheWrapper(delegate, scopes)
334334
whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(null)
335335

336-
wrapper.putIfAbsent("myKey", "myValue")
336+
val result = wrapper.putIfAbsent("myKey", "myValue")
337337

338+
assertNull(result)
338339
verify(delegate).putIfAbsent("myKey", "myValue")
339-
assertEquals(0, tx.spans.size)
340+
assertEquals(1, tx.spans.size)
341+
val span = tx.spans.first()
342+
assertEquals("cache.put", span.operation)
343+
assertEquals(SpanStatus.OK, span.status)
344+
assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY))
345+
assertEquals("putIfAbsent", span.getData("db.operation.name"))
340346
}
341347

342348
// -- evict --

sentry-spring/src/main/java/io/sentry/spring/cache/SentryCacheWrapper.java

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,24 @@ public void put(final @NotNull Object key, final @Nullable Object value) {
124124
}
125125
}
126126

127-
// putIfAbsent is not instrumented — we cannot know ahead of time whether the put
128-
// will actually happen, and emitting a cache.put span for a no-op would be misleading.
129-
// This matches sentry-python and sentry-javascript which also skip conditional puts.
130-
// We must override to bypass the default implementation which calls this.get() + this.put().
131127
@Override
132128
public @Nullable ValueWrapper putIfAbsent(
133129
final @NotNull Object key, final @Nullable Object value) {
134-
return delegate.putIfAbsent(key, value);
130+
final ISpan span = startSpan("cache.put", key, "putIfAbsent");
131+
if (span == null) {
132+
return delegate.putIfAbsent(key, value);
133+
}
134+
try {
135+
final ValueWrapper result = delegate.putIfAbsent(key, value);
136+
span.setStatus(SpanStatus.OK);
137+
return result;
138+
} catch (Throwable e) {
139+
span.setStatus(SpanStatus.INTERNAL_ERROR);
140+
span.setThrowable(e);
141+
throw e;
142+
} finally {
143+
span.finish();
144+
}
135145
}
136146

137147
@Override

sentry-spring/src/test/kotlin/io/sentry/spring/cache/SentryCacheWrapperTest.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,21 @@ class SentryCacheWrapperTest {
159159
// -- putIfAbsent --
160160

161161
@Test
162-
fun `putIfAbsent delegates without creating span`() {
162+
fun `putIfAbsent creates cache put span`() {
163163
val tx = createTransaction()
164164
val wrapper = SentryCacheWrapper(delegate, scopes)
165165
whenever(delegate.putIfAbsent("myKey", "myValue")).thenReturn(null)
166166

167-
wrapper.putIfAbsent("myKey", "myValue")
167+
val result = wrapper.putIfAbsent("myKey", "myValue")
168168

169+
assertNull(result)
169170
verify(delegate).putIfAbsent("myKey", "myValue")
170-
assertEquals(0, tx.spans.size)
171+
assertEquals(1, tx.spans.size)
172+
val span = tx.spans.first()
173+
assertEquals("cache.put", span.operation)
174+
assertEquals(SpanStatus.OK, span.status)
175+
assertEquals(listOf("myKey"), span.getData(SpanDataConvention.CACHE_KEY_KEY))
176+
assertEquals("putIfAbsent", span.getData("db.operation.name"))
171177
}
172178

173179
// -- evict --

0 commit comments

Comments
 (0)