Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions sentry-spring-7/api/sentry-spring-7.api
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,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 <init> (Lorg/springframework/cache/CacheManager;Lio/sentry/IScopes;)V
public fun getCache (Ljava/lang/String;)Lorg/springframework/cache/Cache;
public fun getCacheNames ()Ljava/util/Collection;
}

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

public abstract interface annotation class io/sentry/spring7/checkin/SentryCheckIn : java/lang/annotation/Annotation {
public abstract fun heartbeat ()Z
public abstract fun monitorSlug ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,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<String> getCacheNames() {
return delegate.getCacheNames();
}
}
Original file line number Diff line number Diff line change
@@ -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> T get(final @NotNull Object key, final @Nullable Class<T> 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> T get(final @NotNull Object key, final @NotNull Callable<T> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Cache>()
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)
}
}
Loading
Loading