Move code from spring-boot-actuator to spring-boot-cache

This commit is contained in:
Andy Wilkinson
2025-05-09 09:33:02 +01:00
committed by Phillip Webb
parent 302a4212b5
commit adec7f2df2
34 changed files with 118 additions and 111 deletions

View File

@@ -0,0 +1,141 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import java.util.UUID;
import java.util.function.BiConsumer;
import com.redis.testcontainers.RedisContainer;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.cache.autoconfigure.CacheAutoConfiguration;
import org.springframework.boot.data.redis.autoconfigure.RedisAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheManager;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RedisCacheMetrics}.
*
* @author Stephane Nicoll
*/
@Testcontainers(disabledWithoutDocker = true)
class RedisCacheMetricsTests {
@Container
static final RedisContainer redis = TestImage.container(RedisContainer.class);
private static final Tags TAGS = Tags.of("app", "test").and("cache", "test");
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class, CacheAutoConfiguration.class))
.withUserConfiguration(CachingConfiguration.class)
.withPropertyValues("spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort(), "spring.cache.type=redis",
"spring.cache.redis.enable-statistics=true");
@Test
void cacheStatisticsAreExposed() {
this.contextRunner.run(withCacheMetrics((cache, meterRegistry) -> {
assertThat(meterRegistry.find("cache.size").tags(TAGS).functionCounter()).isNull();
assertThat(meterRegistry.find("cache.gets").tags(TAGS.and("result", "hit")).functionCounter()).isNotNull();
assertThat(meterRegistry.find("cache.gets").tags(TAGS.and("result", "miss")).functionCounter()).isNotNull();
assertThat(meterRegistry.find("cache.gets").tags(TAGS.and("result", "pending")).functionCounter())
.isNotNull();
assertThat(meterRegistry.find("cache.evictions").tags(TAGS).functionCounter()).isNull();
assertThat(meterRegistry.find("cache.puts").tags(TAGS).functionCounter()).isNotNull();
assertThat(meterRegistry.find("cache.removals").tags(TAGS).functionCounter()).isNotNull();
assertThat(meterRegistry.find("cache.lock.duration").tags(TAGS).timeGauge()).isNotNull();
}));
}
@Test
void cacheHitsAreExposed() {
this.contextRunner.run(withCacheMetrics((cache, meterRegistry) -> {
String key = UUID.randomUUID().toString();
cache.put(key, "test");
cache.get(key);
cache.get(key);
assertThat(meterRegistry.get("cache.gets").tags(TAGS.and("result", "hit")).functionCounter().count())
.isEqualTo(2.0d);
}));
}
@Test
void cacheMissesAreExposed() {
this.contextRunner.run(withCacheMetrics((cache, meterRegistry) -> {
String key = UUID.randomUUID().toString();
cache.get(key);
cache.get(key);
cache.get(key);
assertThat(meterRegistry.get("cache.gets").tags(TAGS.and("result", "miss")).functionCounter().count())
.isEqualTo(3.0d);
}));
}
@Test
void cacheMetricsMatchCacheStatistics() {
this.contextRunner.run((context) -> {
RedisCache cache = getTestCache(context);
RedisCacheMetrics cacheMetrics = new RedisCacheMetrics(cache, TAGS);
assertThat(cacheMetrics.hitCount()).isEqualTo(cache.getStatistics().getHits());
assertThat(cacheMetrics.missCount()).isEqualTo(cache.getStatistics().getMisses());
assertThat(cacheMetrics.putCount()).isEqualTo(cache.getStatistics().getPuts());
assertThat(cacheMetrics.size()).isNull();
assertThat(cacheMetrics.evictionCount()).isNull();
});
}
private ContextConsumer<AssertableApplicationContext> withCacheMetrics(
BiConsumer<RedisCache, MeterRegistry> stats) {
return (context) -> {
RedisCache cache = getTestCache(context);
SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry();
new RedisCacheMetrics(cache, Tags.of("app", "test")).bindTo(meterRegistry);
stats.accept(cache, meterRegistry);
};
}
private RedisCache getTestCache(AssertableApplicationContext context) {
assertThat(context).hasSingleBean(RedisCacheManager.class);
RedisCacheManager cacheManager = context.getBean(RedisCacheManager.class);
RedisCache cache = (RedisCache) cacheManager.getCache("test");
assertThat(cache).isNotNull();
return cache;
}
@Configuration(proxyBeanMethods = false)
@EnableCaching
static class CachingConfiguration {
}
}

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.endpoint;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
/**
* {@link Endpoint @Endpoint} to expose available {@link Cache caches}.
*
* @author Johannes Edmeier
* @author Stephane Nicoll
* @since 4.0.0
*/
@Endpoint(id = "caches")
public class CachesEndpoint {
private final Map<String, CacheManager> cacheManagers;
/**
* Create a new endpoint with the {@link CacheManager} instances to use.
* @param cacheManagers the cache managers to use, indexed by name
*/
public CachesEndpoint(Map<String, CacheManager> cacheManagers) {
this.cacheManagers = new LinkedHashMap<>(cacheManagers);
}
/**
* Return a {@link CachesDescriptor} of all available {@link Cache caches}.
* @return a caches reports
*/
@ReadOperation
public CachesDescriptor caches() {
Map<String, Map<String, CacheDescriptor>> descriptors = new LinkedHashMap<>();
getCacheEntries(matchAll(), matchAll()).forEach((entry) -> {
String cacheName = entry.getName();
String cacheManager = entry.getCacheManager();
Map<String, CacheDescriptor> cacheManagerDescriptors = descriptors.computeIfAbsent(cacheManager,
(key) -> new LinkedHashMap<>());
cacheManagerDescriptors.put(cacheName, new CacheDescriptor(entry.getTarget()));
});
Map<String, CacheManagerDescriptor> cacheManagerDescriptors = new LinkedHashMap<>();
descriptors.forEach((name, entries) -> cacheManagerDescriptors.put(name, new CacheManagerDescriptor(entries)));
return new CachesDescriptor(cacheManagerDescriptors);
}
/**
* Return a {@link CacheDescriptor} for the specified cache.
* @param cache the name of the cache
* @param cacheManager the name of the cacheManager (can be {@code null}
* @return the descriptor of the cache or {@code null} if no such cache exists
* @throws NonUniqueCacheException if more than one cache with that name exists and no
* {@code cacheManager} was provided to identify a unique candidate
*/
@ReadOperation
public CacheEntryDescriptor cache(@Selector String cache, @OptionalParameter String cacheManager) {
return extractUniqueCacheEntry(cache, getCacheEntries((name) -> name.equals(cache), isNameMatch(cacheManager)));
}
/**
* Clear all the available {@link Cache caches}.
*/
@DeleteOperation
public void clearCaches() {
getCacheEntries(matchAll(), matchAll()).forEach(this::clearCache);
}
/**
* Clear the specific {@link Cache}.
* @param cache the name of the cache
* @param cacheManager the name of the cacheManager (can be {@code null} to match all)
* @return {@code true} if the cache was cleared or {@code false} if no such cache
* exists
* @throws NonUniqueCacheException if more than one cache with that name exists and no
* {@code cacheManager} was provided to identify a unique candidate
*/
@DeleteOperation
public boolean clearCache(@Selector String cache, @OptionalParameter String cacheManager) {
CacheEntryDescriptor entry = extractUniqueCacheEntry(cache,
getCacheEntries((name) -> name.equals(cache), isNameMatch(cacheManager)));
return (entry != null && clearCache(entry));
}
private List<CacheEntryDescriptor> getCacheEntries(Predicate<String> cacheNamePredicate,
Predicate<String> cacheManagerNamePredicate) {
return this.cacheManagers.keySet()
.stream()
.filter(cacheManagerNamePredicate)
.flatMap((cacheManagerName) -> getCacheEntries(cacheManagerName, cacheNamePredicate).stream())
.toList();
}
private List<CacheEntryDescriptor> getCacheEntries(String cacheManagerName, Predicate<String> cacheNamePredicate) {
CacheManager cacheManager = this.cacheManagers.get(cacheManagerName);
return cacheManager.getCacheNames()
.stream()
.filter(cacheNamePredicate)
.map(cacheManager::getCache)
.filter(Objects::nonNull)
.map((cache) -> new CacheEntryDescriptor(cache, cacheManagerName))
.toList();
}
private CacheEntryDescriptor extractUniqueCacheEntry(String cache, List<CacheEntryDescriptor> entries) {
if (entries.size() > 1) {
throw new NonUniqueCacheException(cache,
entries.stream().map(CacheEntryDescriptor::getCacheManager).distinct().toList());
}
return (!entries.isEmpty() ? entries.get(0) : null);
}
private boolean clearCache(CacheEntryDescriptor entry) {
String cacheName = entry.getName();
String cacheManager = entry.getCacheManager();
Cache cache = this.cacheManagers.get(cacheManager).getCache(cacheName);
if (cache != null) {
cache.clear();
return true;
}
return false;
}
private Predicate<String> isNameMatch(String name) {
return (name != null) ? ((requested) -> requested.equals(name)) : matchAll();
}
private Predicate<String> matchAll() {
return (name) -> true;
}
/**
* Description of the caches.
*/
public static final class CachesDescriptor implements OperationResponseBody {
private final Map<String, CacheManagerDescriptor> cacheManagers;
public CachesDescriptor(Map<String, CacheManagerDescriptor> cacheManagers) {
this.cacheManagers = cacheManagers;
}
public Map<String, CacheManagerDescriptor> getCacheManagers() {
return this.cacheManagers;
}
}
/**
* Description of a {@link CacheManager}.
*/
public static final class CacheManagerDescriptor {
private final Map<String, CacheDescriptor> caches;
public CacheManagerDescriptor(Map<String, CacheDescriptor> caches) {
this.caches = caches;
}
public Map<String, CacheDescriptor> getCaches() {
return this.caches;
}
}
/**
* Description of a {@link Cache}.
*/
public static class CacheDescriptor implements OperationResponseBody {
private final String target;
public CacheDescriptor(String target) {
this.target = target;
}
/**
* Return the fully qualified name of the native cache.
* @return the fully qualified name of the native cache
*/
public String getTarget() {
return this.target;
}
}
/**
* Description of a {@link Cache} entry.
*/
public static final class CacheEntryDescriptor extends CacheDescriptor {
private final String name;
private final String cacheManager;
public CacheEntryDescriptor(Cache cache, String cacheManager) {
super(cache.getNativeCache().getClass().getName());
this.name = cache.getName();
this.cacheManager = cacheManager;
}
public String getName() {
return this.name;
}
public String getCacheManager() {
return this.cacheManager;
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.endpoint;
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.OptionalParameter;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension;
import org.springframework.boot.cache.actuate.endpoint.CachesEndpoint.CacheEntryDescriptor;
/**
* {@link EndpointWebExtension @EndpointWebExtension} for the {@link CachesEndpoint}.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
@EndpointWebExtension(endpoint = CachesEndpoint.class)
public class CachesEndpointWebExtension {
private final CachesEndpoint delegate;
public CachesEndpointWebExtension(CachesEndpoint delegate) {
this.delegate = delegate;
}
@ReadOperation
public WebEndpointResponse<CacheEntryDescriptor> cache(@Selector String cache,
@OptionalParameter String cacheManager) {
try {
CacheEntryDescriptor entry = this.delegate.cache(cache, cacheManager);
int status = (entry != null) ? WebEndpointResponse.STATUS_OK : WebEndpointResponse.STATUS_NOT_FOUND;
return new WebEndpointResponse<>(entry, status);
}
catch (NonUniqueCacheException ex) {
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST);
}
}
@DeleteOperation
public WebEndpointResponse<Void> clearCache(@Selector String cache, @OptionalParameter String cacheManager) {
try {
boolean cleared = this.delegate.clearCache(cache, cacheManager);
int status = (cleared ? WebEndpointResponse.STATUS_NO_CONTENT : WebEndpointResponse.STATUS_NOT_FOUND);
return new WebEndpointResponse<>(status);
}
catch (NonUniqueCacheException ex) {
return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST);
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.endpoint;
import java.util.Collection;
import java.util.Collections;
/**
* Exception thrown when multiple caches exist with the same name.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
public class NonUniqueCacheException extends RuntimeException {
private final String cacheName;
private final Collection<String> cacheManagerNames;
public NonUniqueCacheException(String cacheName, Collection<String> cacheManagerNames) {
super(String.format("Multiple caches named %s found, specify the 'cacheManager' to use: %s", cacheName,
cacheManagerNames));
this.cacheName = cacheName;
this.cacheManagerNames = Collections.unmodifiableCollection(cacheManagerNames);
}
public String getCacheName() {
return this.cacheName;
}
public Collection<String> getCacheManagerNames() {
return this.cacheManagerNames;
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Actuator endpoint for caches.
*/
package org.springframework.boot.cache.actuate.endpoint;

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.cache2k.extra.micrometer.Cache2kCacheMetrics;
import org.cache2k.extra.spring.SpringCache2kCache;
/**
* {@link CacheMeterBinderProvider} implementation for cache2k.
*
* @author Jens Wilke
* @since 4.0.0
*/
public class Cache2kCacheMeterBinderProvider implements CacheMeterBinderProvider<SpringCache2kCache> {
@Override
public MeterBinder getMeterBinder(SpringCache2kCache cache, Iterable<Tag> tags) {
return new Cache2kCacheMetrics(cache.getNativeCache(), tags);
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.cache.Cache;
/**
* Provide a {@link MeterBinder} based on a {@link Cache}.
*
* @param <C> the cache type
* @author Stephane Nicoll
* @since 4.0.0
*/
@FunctionalInterface
public interface CacheMeterBinderProvider<C extends Cache> {
/**
* Return the {@link MeterBinder} managing the specified {@link Cache} or {@code null}
* if the specified {@link Cache} is not supported.
* @param cache the cache to instrument
* @param tags tags to apply to all recorded metrics
* @return a {@link MeterBinder} handling the specified {@link Cache} or {@code null}
*/
MeterBinder getMeterBinder(C cache, Iterable<Tag> tags);
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import java.util.Collection;
import java.util.Objects;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.boot.util.LambdaSafe;
import org.springframework.cache.Cache;
import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
import org.springframework.util.ClassUtils;
/**
* Register supported {@link Cache} to a {@link MeterRegistry}.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
public class CacheMetricsRegistrar {
private final MeterRegistry registry;
private final Collection<CacheMeterBinderProvider<?>> binderProviders;
/**
* Creates a new registrar.
* @param registry the {@link MeterRegistry} to use
* @param binderProviders the {@link CacheMeterBinderProvider} instances that should
* be used to detect compatible caches
*/
public CacheMetricsRegistrar(MeterRegistry registry, Collection<CacheMeterBinderProvider<?>> binderProviders) {
this.registry = registry;
this.binderProviders = binderProviders;
}
/**
* Attempt to bind the specified {@link Cache} to the registry. Return {@code true} if
* the cache is supported and was bound to the registry, {@code false} otherwise.
* @param cache the cache to handle
* @param tags the tags to associate with the metrics of that cache
* @return {@code true} if the {@code cache} is supported and was registered
*/
public boolean bindCacheToRegistry(Cache cache, Tag... tags) {
MeterBinder meterBinder = getMeterBinder(unwrapIfNecessary(cache), Tags.of(tags));
if (meterBinder != null) {
meterBinder.bindTo(this.registry);
return true;
}
return false;
}
@SuppressWarnings({ "unchecked" })
private MeterBinder getMeterBinder(Cache cache, Tags tags) {
Tags cacheTags = tags.and(getAdditionalTags(cache));
return LambdaSafe.callbacks(CacheMeterBinderProvider.class, this.binderProviders, cache)
.withLogger(CacheMetricsRegistrar.class)
.invokeAnd((binderProvider) -> binderProvider.getMeterBinder(cache, cacheTags))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
/**
* Return additional {@link Tag tags} to be associated with the given {@link Cache}.
* @param cache the cache
* @return a list of additional tags to associate to that {@code cache}.
*/
protected Iterable<Tag> getAdditionalTags(Cache cache) {
return Tags.of("name", cache.getName());
}
private Cache unwrapIfNecessary(Cache cache) {
if (ClassUtils.isPresent("org.springframework.cache.transaction.TransactionAwareCacheDecorator",
getClass().getClassLoader())) {
return TransactionAwareCacheDecoratorHandler.unwrapIfNecessary(cache);
}
return cache;
}
private static final class TransactionAwareCacheDecoratorHandler {
private static Cache unwrapIfNecessary(Cache cache) {
try {
if (cache instanceof TransactionAwareCacheDecorator decorator) {
return decorator.getTargetCache();
}
}
catch (NoClassDefFoundError ex) {
// Ignore
}
return cache;
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics;
import org.springframework.cache.caffeine.CaffeineCache;
/**
* {@link CacheMeterBinderProvider} implementation for Caffeine.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
public class CaffeineCacheMeterBinderProvider implements CacheMeterBinderProvider<CaffeineCache> {
@Override
public MeterBinder getMeterBinder(CaffeineCache cache, Iterable<Tag> tags) {
return new CaffeineCacheMetrics<>(cache.getNativeCache(), cache.getName(), tags);
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import com.hazelcast.spring.cache.HazelcastCache;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.boot.cache.actuate.metrics.HazelcastCacheMeterBinderProvider.HazelcastCacheMeterBinderProviderRuntimeHints;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* {@link CacheMeterBinderProvider} implementation for Hazelcast.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
@ImportRuntimeHints(HazelcastCacheMeterBinderProviderRuntimeHints.class)
public class HazelcastCacheMeterBinderProvider implements CacheMeterBinderProvider<HazelcastCache> {
@Override
public MeterBinder getMeterBinder(HazelcastCache cache, Iterable<Tag> tags) {
try {
return new HazelcastCacheMetrics(cache.getNativeCache(), tags);
}
catch (NoSuchMethodError ex) {
// Hazelcast 4
return createHazelcast4CacheMetrics(cache, tags);
}
}
private MeterBinder createHazelcast4CacheMetrics(HazelcastCache cache, Iterable<Tag> tags) {
try {
Method nativeCacheAccessor = ReflectionUtils.findMethod(HazelcastCache.class, "getNativeCache");
Object nativeCache = ReflectionUtils.invokeMethod(nativeCacheAccessor, cache);
return HazelcastCacheMetrics.class.getConstructor(Object.class, Iterable.class)
.newInstance(nativeCache, tags);
}
catch (Exception ex) {
throw new IllegalStateException("Failed to create MeterBinder for Hazelcast", ex);
}
}
static class HazelcastCacheMeterBinderProviderRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
try {
Method getNativeCacheMethod = ReflectionUtils.findMethod(HazelcastCache.class, "getNativeCache");
Assert.state(getNativeCacheMethod != null, "Unable to find 'getNativeCache' method");
Constructor<?> constructor = HazelcastCacheMetrics.class.getConstructor(Object.class, Iterable.class);
hints.reflection()
.registerMethod(getNativeCacheMethod, ExecutableMode.INVOKE)
.registerConstructor(constructor, ExecutableMode.INVOKE);
}
catch (NoSuchMethodException ex) {
throw new IllegalStateException(ex);
}
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.binder.cache.JCacheMetrics;
import org.springframework.cache.jcache.JCacheCache;
/**
* {@link CacheMeterBinderProvider} implementation for JCache.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
public class JCacheCacheMeterBinderProvider implements CacheMeterBinderProvider<JCacheCache> {
@Override
public MeterBinder getMeterBinder(JCacheCache cache, Iterable<Tag> tags) {
return new JCacheMetrics<>(cache.getNativeCache(), tags);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.data.redis.cache.RedisCache;
/**
* {@link CacheMeterBinderProvider} implementation for Redis.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
public class RedisCacheMeterBinderProvider implements CacheMeterBinderProvider<RedisCache> {
@Override
public MeterBinder getMeterBinder(RedisCache cache, Iterable<Tag> tags) {
return new RedisCacheMetrics(cache, tags);
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import java.util.concurrent.TimeUnit;
import io.micrometer.core.instrument.FunctionCounter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.TimeGauge;
import io.micrometer.core.instrument.binder.cache.CacheMeterBinder;
import org.springframework.data.redis.cache.RedisCache;
/**
* {@link CacheMeterBinder} for {@link RedisCache}.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
public class RedisCacheMetrics extends CacheMeterBinder<RedisCache> {
private final RedisCache cache;
public RedisCacheMetrics(RedisCache cache, Iterable<Tag> tags) {
super(cache, cache.getName(), tags);
this.cache = cache;
}
@Override
protected Long size() {
return null;
}
@Override
protected long hitCount() {
return this.cache.getStatistics().getHits();
}
@Override
protected Long missCount() {
return this.cache.getStatistics().getMisses();
}
@Override
protected Long evictionCount() {
return null;
}
@Override
protected long putCount() {
return this.cache.getStatistics().getPuts();
}
@Override
protected void bindImplementationSpecificMetrics(MeterRegistry registry) {
FunctionCounter.builder("cache.removals", this.cache, (cache) -> cache.getStatistics().getDeletes())
.tags(getTagsWithCacheName())
.description("Cache removals")
.register(registry);
FunctionCounter.builder("cache.gets", this.cache, (cache) -> cache.getStatistics().getPending())
.tags(getTagsWithCacheName())
.tag("result", "pending")
.description("The number of pending requests")
.register(registry);
TimeGauge
.builder("cache.lock.duration", this.cache, TimeUnit.NANOSECONDS,
(cache) -> cache.getStatistics().getLockWaitDuration(TimeUnit.NANOSECONDS))
.tags(getTagsWithCacheName())
.description("The time the cache has spent waiting on a lock")
.register(registry);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Metrics for caches.
*/
package org.springframework.boot.cache.actuate.metrics;

View File

@@ -0,0 +1,203 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.endpoint;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.junit.jupiter.api.Test;
import org.springframework.boot.cache.actuate.endpoint.CachesEndpoint.CacheEntryDescriptor;
import org.springframework.boot.cache.actuate.endpoint.CachesEndpoint.CacheManagerDescriptor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.support.SimpleCacheManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link CachesEndpoint}.
*
* @author Stephane Nicoll
*/
class CachesEndpointTests {
@Test
void allCachesWithSingleCacheManager() {
CachesEndpoint endpoint = new CachesEndpoint(
Collections.singletonMap("test", new ConcurrentMapCacheManager("a", "b")));
Map<String, CacheManagerDescriptor> allDescriptors = endpoint.caches().getCacheManagers();
assertThat(allDescriptors).containsOnlyKeys("test");
CacheManagerDescriptor descriptors = allDescriptors.get("test");
assertThat(descriptors.getCaches()).containsOnlyKeys("a", "b");
assertThat(descriptors.getCaches().get("a").getTarget()).isEqualTo(ConcurrentHashMap.class.getName());
assertThat(descriptors.getCaches().get("b").getTarget()).isEqualTo(ConcurrentHashMap.class.getName());
}
@Test
void allCachesWithSeveralCacheManagers() {
Map<String, CacheManager> cacheManagers = new LinkedHashMap<>();
cacheManagers.put("test", new ConcurrentMapCacheManager("a", "b"));
cacheManagers.put("another", new ConcurrentMapCacheManager("a", "c"));
CachesEndpoint endpoint = new CachesEndpoint(cacheManagers);
Map<String, CacheManagerDescriptor> allDescriptors = endpoint.caches().getCacheManagers();
assertThat(allDescriptors).containsOnlyKeys("test", "another");
assertThat(allDescriptors.get("test").getCaches()).containsOnlyKeys("a", "b");
assertThat(allDescriptors.get("another").getCaches()).containsOnlyKeys("a", "c");
}
@Test
void namedCacheWithSingleCacheManager() {
CachesEndpoint endpoint = new CachesEndpoint(
Collections.singletonMap("test", new ConcurrentMapCacheManager("b", "a")));
CacheEntryDescriptor entry = endpoint.cache("a", null);
assertThat(entry).isNotNull();
assertThat(entry.getCacheManager()).isEqualTo("test");
assertThat(entry.getName()).isEqualTo("a");
assertThat(entry.getTarget()).isEqualTo(ConcurrentHashMap.class.getName());
}
@Test
void namedCacheWithSeveralCacheManagers() {
Map<String, CacheManager> cacheManagers = new LinkedHashMap<>();
cacheManagers.put("test", new ConcurrentMapCacheManager("b", "dupe-cache"));
cacheManagers.put("another", new ConcurrentMapCacheManager("c", "dupe-cache"));
CachesEndpoint endpoint = new CachesEndpoint(cacheManagers);
assertThatExceptionOfType(NonUniqueCacheException.class).isThrownBy(() -> endpoint.cache("dupe-cache", null))
.withMessageContaining("dupe-cache")
.withMessageContaining("test")
.withMessageContaining("another");
}
@Test
void namedCacheWithUnknownCache() {
CachesEndpoint endpoint = new CachesEndpoint(
Collections.singletonMap("test", new ConcurrentMapCacheManager("b", "a")));
CacheEntryDescriptor entry = endpoint.cache("unknown", null);
assertThat(entry).isNull();
}
@Test
void namedCacheWithWrongCacheManager() {
Map<String, CacheManager> cacheManagers = new LinkedHashMap<>();
cacheManagers.put("test", new ConcurrentMapCacheManager("b", "a"));
cacheManagers.put("another", new ConcurrentMapCacheManager("c", "a"));
CachesEndpoint endpoint = new CachesEndpoint(cacheManagers);
CacheEntryDescriptor entry = endpoint.cache("c", "test");
assertThat(entry).isNull();
}
@Test
void namedCacheWithSeveralCacheManagersWithCacheManagerFilter() {
Map<String, CacheManager> cacheManagers = new LinkedHashMap<>();
cacheManagers.put("test", new ConcurrentMapCacheManager("b", "a"));
cacheManagers.put("another", new ConcurrentMapCacheManager("c", "a"));
CachesEndpoint endpoint = new CachesEndpoint(cacheManagers);
CacheEntryDescriptor entry = endpoint.cache("a", "test");
assertThat(entry).isNotNull();
assertThat(entry.getCacheManager()).isEqualTo("test");
assertThat(entry.getName()).isEqualTo("a");
}
@Test
void clearAllCaches() {
Cache a = mockCache("a");
Cache b = mockCache("b");
CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap("test", cacheManager(a, b)));
endpoint.clearCaches();
then(a).should().clear();
then(b).should().clear();
}
@Test
void clearCache() {
Cache a = mockCache("a");
Cache b = mockCache("b");
CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap("test", cacheManager(a, b)));
assertThat(endpoint.clearCache("a", null)).isTrue();
then(a).should().clear();
then(b).should(never()).clear();
}
@Test
void clearCacheWithSeveralCacheManagers() {
Map<String, CacheManager> cacheManagers = new LinkedHashMap<>();
cacheManagers.put("test", cacheManager(mockCache("dupe-cache"), mockCache("b")));
cacheManagers.put("another", cacheManager(mockCache("dupe-cache")));
CachesEndpoint endpoint = new CachesEndpoint(cacheManagers);
assertThatExceptionOfType(NonUniqueCacheException.class)
.isThrownBy(() -> endpoint.clearCache("dupe-cache", null))
.withMessageContaining("dupe-cache")
.withMessageContaining("test")
.withMessageContaining("another");
}
@Test
void clearCacheWithSeveralCacheManagersWithCacheManagerFilter() {
Map<String, CacheManager> cacheManagers = new LinkedHashMap<>();
Cache a = mockCache("a");
Cache b = mockCache("b");
cacheManagers.put("test", cacheManager(a, b));
Cache anotherA = mockCache("a");
cacheManagers.put("another", cacheManager(anotherA));
CachesEndpoint endpoint = new CachesEndpoint(cacheManagers);
assertThat(endpoint.clearCache("a", "another")).isTrue();
then(a).should(never()).clear();
then(anotherA).should().clear();
then(b).should(never()).clear();
}
@Test
void clearCacheWithUnknownCache() {
Cache a = mockCache("a");
CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap("test", cacheManager(a)));
assertThat(endpoint.clearCache("unknown", null)).isFalse();
then(a).should(never()).clear();
}
@Test
void clearCacheWithUnknownCacheManager() {
Cache a = mockCache("a");
CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap("test", cacheManager(a)));
assertThat(endpoint.clearCache("a", "unknown")).isFalse();
then(a).should(never()).clear();
}
private CacheManager cacheManager(Cache... caches) {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(caches));
cacheManager.afterPropertiesSet();
return cacheManager;
}
private Cache mockCache(String name) {
Cache cache = mock(Cache.class);
given(cache.getName()).willReturn(name);
given(cache.getNativeCache()).willReturn(new Object());
return cache;
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.endpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link CachesEndpoint} exposed by Jersey, Spring MVC, and
* WebFlux.
*
* @author Stephane Nicoll
*/
class CachesEndpointWebIntegrationTests {
@WebEndpointTest
void allCaches(WebTestClient client) {
client.get()
.uri("/actuator/caches")
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("cacheManagers.one.caches.a.target")
.isEqualTo(ConcurrentHashMap.class.getName())
.jsonPath("cacheManagers.one.caches.b.target")
.isEqualTo(ConcurrentHashMap.class.getName())
.jsonPath("cacheManagers.two.caches.a.target")
.isEqualTo(ConcurrentHashMap.class.getName())
.jsonPath("cacheManagers.two.caches.c.target")
.isEqualTo(ConcurrentHashMap.class.getName());
}
@WebEndpointTest
void namedCache(WebTestClient client) {
client.get()
.uri("/actuator/caches/b")
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("name")
.isEqualTo("b")
.jsonPath("cacheManager")
.isEqualTo("one")
.jsonPath("target")
.isEqualTo(ConcurrentHashMap.class.getName());
}
@WebEndpointTest
void namedCacheWithUnknownName(WebTestClient client) {
client.get().uri("/actuator/caches/does-not-exist").exchange().expectStatus().isNotFound();
}
@WebEndpointTest
void namedCacheWithNonUniqueName(WebTestClient client) {
client.get().uri("/actuator/caches/a").exchange().expectStatus().isBadRequest();
}
@WebEndpointTest
void clearNamedCache(WebTestClient client, ApplicationContext context) {
Cache b = context.getBean("one", CacheManager.class).getCache("b");
b.put("test", "value");
client.delete().uri("/actuator/caches/b").exchange().expectStatus().isNoContent();
assertThat(b.get("test")).isNull();
}
@WebEndpointTest
void cleanNamedCacheWithUnknownName(WebTestClient client) {
client.delete().uri("/actuator/caches/does-not-exist").exchange().expectStatus().isNotFound();
}
@WebEndpointTest
void clearNamedCacheWithNonUniqueName(WebTestClient client) {
client.get().uri("/actuator/caches/a").exchange().expectStatus().isBadRequest();
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration {
@Bean
CacheManager one() {
return new ConcurrentMapCacheManager("a", "b");
}
@Bean
CacheManager two() {
return new ConcurrentMapCacheManager("a", "c");
}
@Bean
CachesEndpoint endpoint(Map<String, CacheManager> cacheManagers) {
return new CachesEndpoint(cacheManagers);
}
@Bean
CachesEndpointWebExtension cachesEndpointWebExtension(CachesEndpoint endpoint) {
return new CachesEndpointWebExtension(endpoint);
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import java.util.Collections;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.cache2k.extra.micrometer.Cache2kCacheMetrics;
import org.cache2k.extra.spring.SpringCache2kCacheManager;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Cache2kCacheMeterBinderProvider}.
*
* @author Stephane Nicoll
*/
class Cache2kCacheMeterBinderProviderTests {
@Test
void cache2kCacheProvider() {
SpringCache2kCacheManager cacheManager = new SpringCache2kCacheManager()
.addCaches((builder) -> builder.name("test"));
MeterBinder meterBinder = new Cache2kCacheMeterBinderProvider().getMeterBinder(cacheManager.getCache("test"),
Collections.emptyList());
assertThat(meterBinder).isInstanceOf(Cache2kCacheMetrics.class);
cacheManager.destroy();
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import java.util.Collections;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CacheMetricsRegistrar}.
*
* @author Stephane Nicoll
*/
class CacheMetricsRegistrarTests {
private final MeterRegistry meterRegistry = new SimpleMeterRegistry();
@Test
void bindToSupportedCache() {
CacheMetricsRegistrar registrar = new CacheMetricsRegistrar(this.meterRegistry,
Collections.singleton(new CaffeineCacheMeterBinderProvider()));
assertThat(
registrar.bindCacheToRegistry(new CaffeineCache("test", Caffeine.newBuilder().recordStats().build())))
.isTrue();
assertThat(this.meterRegistry.get("cache.gets").tags("name", "test").meter()).isNotNull();
}
@Test
void bindToSupportedCacheWrappedInTransactionProxy() {
CacheMetricsRegistrar registrar = new CacheMetricsRegistrar(this.meterRegistry,
Collections.singleton(new CaffeineCacheMeterBinderProvider()));
assertThat(registrar.bindCacheToRegistry(new TransactionAwareCacheDecorator(
new CaffeineCache("test", Caffeine.newBuilder().recordStats().build()))))
.isTrue();
assertThat(this.meterRegistry.get("cache.gets").tags("name", "test").meter()).isNotNull();
}
@Test
void bindToUnsupportedCache() {
CacheMetricsRegistrar registrar = new CacheMetricsRegistrar(this.meterRegistry, Collections.emptyList());
assertThat(
registrar.bindCacheToRegistry(new CaffeineCache("test", Caffeine.newBuilder().recordStats().build())))
.isFalse();
assertThat(this.meterRegistry.find("cache.gets").tags("name", "test").meter()).isNull();
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import java.util.Collections;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics;
import org.junit.jupiter.api.Test;
import org.springframework.cache.caffeine.CaffeineCache;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CaffeineCacheMeterBinderProvider}.
*
* @author Stephane Nicoll
*/
class CaffeineCacheMeterBinderProviderTests {
@Test
void caffeineCacheProvider() {
CaffeineCache cache = new CaffeineCache("test", Caffeine.newBuilder().build());
MeterBinder meterBinder = new CaffeineCacheMeterBinderProvider().getMeterBinder(cache, Collections.emptyList());
assertThat(meterBinder).isInstanceOf(CaffeineCacheMetrics.class);
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import java.util.Collections;
import com.hazelcast.map.IMap;
import com.hazelcast.spring.cache.HazelcastCache;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.boot.cache.actuate.metrics.HazelcastCacheMeterBinderProvider.HazelcastCacheMeterBinderProviderRuntimeHints;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link HazelcastCacheMeterBinderProvider}.
*
* @author Stephane Nicoll
* @author Moritz Halbritter
*/
class HazelcastCacheMeterBinderProviderTests {
@SuppressWarnings("unchecked")
@Test
void hazelcastCacheProvider() {
IMap<Object, Object> nativeCache = mock(IMap.class);
given(nativeCache.getName()).willReturn("test");
HazelcastCache cache = new HazelcastCache(nativeCache);
MeterBinder meterBinder = new HazelcastCacheMeterBinderProvider().getMeterBinder(cache,
Collections.emptyList());
assertThat(meterBinder).isInstanceOf(HazelcastCacheMetrics.class);
}
@Test
void shouldRegisterHints() {
RuntimeHints runtimeHints = new RuntimeHints();
new HazelcastCacheMeterBinderProviderRuntimeHints().registerHints(runtimeHints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(HazelcastCache.class, "getNativeCache"))
.accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection().onType(HazelcastCacheMetrics.class)).accepts(runtimeHints);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.binder.cache.JCacheMetrics;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.cache.jcache.JCacheCache;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JCacheCacheMeterBinderProvider}.
*
* @author Stephane Nicoll
*/
@ExtendWith(MockitoExtension.class)
class JCacheCacheMeterBinderProviderTests {
@Mock
private javax.cache.Cache<Object, Object> nativeCache;
@Test
void jCacheCacheProvider() throws URISyntaxException {
javax.cache.CacheManager cacheManager = mock(javax.cache.CacheManager.class);
given(cacheManager.getURI()).willReturn(new URI("/test"));
given(this.nativeCache.getCacheManager()).willReturn(cacheManager);
given(this.nativeCache.getName()).willReturn("test");
JCacheCache cache = new JCacheCache(this.nativeCache);
MeterBinder meterBinder = new JCacheCacheMeterBinderProvider().getMeterBinder(cache, Collections.emptyList());
assertThat(meterBinder).isInstanceOf(JCacheMetrics.class);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cache.actuate.metrics;
import java.util.Collections;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.cache.RedisCache;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RedisCacheMeterBinderProvider}.
*
* @author Stephane Nicoll
*/
class RedisCacheMeterBinderProviderTests {
@Test
void redisCacheProvider() {
RedisCache cache = mock(RedisCache.class);
given(cache.getName()).willReturn("test");
MeterBinder meterBinder = new RedisCacheMeterBinderProvider().getMeterBinder(cache, Collections.emptyList());
assertThat(meterBinder).isInstanceOf(RedisCacheMetrics.class);
}
}