Expose cache statistics as metrics
Add an abstraction that provides a standard manner to retrieve a statistics snapshot of a cache. Specific implementations for JSR-107, ehcache, hazelcast, guava and concurrent map are provided. At the moment the size of the cache and the hit/miss ratios are recorded. Cache metrics are exposed via the `cache.` prefix followed by the name of the cache. In case of conflict, the name of the cache manager is added as a qualifier. It is possible to easily register a new CacheStatisticsProvider for an unsupported cache system and the CacheStatistics object itself can be extended to provide additional metrics. See gh-2633 Closes gh-2770
This commit is contained in:
committed by
Andy Wilkinson
parent
578bd5dc37
commit
bbbb34a690
@@ -28,6 +28,7 @@ import javax.sql.DataSource;
|
||||
import org.apache.commons.dbcp.BasicDataSource;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.springframework.boot.actuate.endpoint.CachePublicMetrics;
|
||||
import org.springframework.boot.actuate.endpoint.DataSourcePublicMetrics;
|
||||
import org.springframework.boot.actuate.endpoint.MetricReaderPublicMetrics;
|
||||
import org.springframework.boot.actuate.endpoint.PublicMetrics;
|
||||
@@ -41,10 +42,13 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
|
||||
import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvidersConfiguration;
|
||||
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.ConnectionCallback;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -193,6 +197,29 @@ public class PublicMetricsAutoConfigurationTests {
|
||||
assertEquals(1, this.context.getBeansOfType(TomcatPublicMetrics.class).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noCacheMetrics() {
|
||||
load();
|
||||
assertEquals(0, this.context.getBeansOfType(CachePublicMetrics.class).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void autoCacheManager() {
|
||||
load(CacheConfiguration.class);
|
||||
CachePublicMetrics bean = this.context.getBean(CachePublicMetrics.class);
|
||||
Collection<Metric<?>> metrics = bean.metrics();
|
||||
assertMetrics(metrics, "cache.books.size", "cache.speakers.size");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleCacheManagers() {
|
||||
load(MultipleCacheConfiguration.class);
|
||||
CachePublicMetrics bean = this.context.getBean(CachePublicMetrics.class);
|
||||
Collection<Metric<?>> metrics = bean.metrics();
|
||||
assertMetrics(metrics, "cache.books.size", "cache.second_speakers.size",
|
||||
"cache.speakers.size", "cache.users.size");
|
||||
}
|
||||
|
||||
private void assertHasMetric(Collection<Metric<?>> metrics, Metric<?> metric) {
|
||||
for (Metric<?> m : metrics) {
|
||||
if (m.getValue().equals(metric.getValue())
|
||||
@@ -317,4 +344,29 @@ public class PublicMetricsAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CacheConfiguration {
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
return new ConcurrentMapCacheManager("books", "speakers");
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class MultipleCacheConfiguration {
|
||||
|
||||
@Bean
|
||||
@Order(1)
|
||||
public CacheManager first() {
|
||||
return new ConcurrentMapCacheManager("books", "speakers");
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(2)
|
||||
public CacheManager second() {
|
||||
return new ConcurrentMapCacheManager("users", "speakers");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Copyright 2012-2015 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
|
||||
*
|
||||
* http://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.actuate.cache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.cache.Caching;
|
||||
import javax.cache.configuration.MutableConfiguration;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.cache.ehcache.EhCacheCacheManager;
|
||||
import org.springframework.cache.ehcache.EhCacheManagerUtils;
|
||||
import org.springframework.cache.guava.GuavaCacheManager;
|
||||
import org.springframework.cache.jcache.JCacheCacheManager;
|
||||
import org.springframework.cache.support.NoOpCacheManager;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.hazelcast.cache.HazelcastCachingProvider;
|
||||
import com.hazelcast.config.Config;
|
||||
import com.hazelcast.config.XmlConfigBuilder;
|
||||
import com.hazelcast.core.Hazelcast;
|
||||
import com.hazelcast.core.HazelcastInstance;
|
||||
import com.hazelcast.spring.cache.HazelcastCacheManager;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
/**
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class CacheStatisticsProviderTests {
|
||||
|
||||
private AnnotationConfigApplicationContext context;
|
||||
|
||||
private CacheManager cacheManager;
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void basicJCacheCacheStatistics() {
|
||||
load(JCacheCacheConfig.class);
|
||||
CacheStatisticsProvider provider = this.context.getBean(
|
||||
"jCacheCacheStatisticsProvider", CacheStatisticsProvider.class);
|
||||
doTestCoreStatistics(provider, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void basicEhCacheCacheStatistics() {
|
||||
load(EhCacheConfig.class);
|
||||
CacheStatisticsProvider provider = this.context.getBean(
|
||||
"ehCacheCacheStatisticsProvider", CacheStatisticsProvider.class);
|
||||
doTestCoreStatistics(provider, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void basicHazelcastCacheStatistics() {
|
||||
load(HazelcastConfig.class);
|
||||
CacheStatisticsProvider provider = this.context.getBean(
|
||||
"hazelcastCacheStatisticsProvider", CacheStatisticsProvider.class);
|
||||
doTestCoreStatistics(provider, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void basicGuavaCacheStatistics() {
|
||||
load(GuavaConfig.class);
|
||||
CacheStatisticsProvider provider = this.context.getBean(
|
||||
"guavaCacheStatisticsProvider", CacheStatisticsProvider.class);
|
||||
doTestCoreStatistics(provider, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrentMapCacheStatistics() {
|
||||
load(ConcurrentMapConfig.class);
|
||||
CacheStatisticsProvider provider = this.context.getBean(
|
||||
"concurrentMapCacheStatisticsProvider", CacheStatisticsProvider.class);
|
||||
Cache books = getCache("books");
|
||||
CacheStatistics cacheStatistics = provider.getCacheStatistics(books,
|
||||
this.cacheManager);
|
||||
assertCoreStatistics(cacheStatistics, 0L, null, null);
|
||||
getOrCreate(books, "a", "b", "b", "a", "a");
|
||||
CacheStatistics updatedCacheStatistics = provider.getCacheStatistics(books,
|
||||
this.cacheManager);
|
||||
assertCoreStatistics(updatedCacheStatistics, 2L, null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noOpCacheStatistics() {
|
||||
load(NoOpCacheConfig.class);
|
||||
CacheStatisticsProvider provider = this.context.getBean(
|
||||
"noOpCacheStatisticsProvider", CacheStatisticsProvider.class);
|
||||
Cache books = getCache("books");
|
||||
CacheStatistics cacheStatistics = provider.getCacheStatistics(books,
|
||||
this.cacheManager);
|
||||
assertCoreStatistics(cacheStatistics, null, null, null);
|
||||
getOrCreate(books, "a", "b", "b", "a", "a");
|
||||
CacheStatistics updatedCacheStatistics = provider.getCacheStatistics(books,
|
||||
this.cacheManager);
|
||||
assertCoreStatistics(updatedCacheStatistics, null, null, null);
|
||||
}
|
||||
|
||||
private void doTestCoreStatistics(CacheStatisticsProvider provider,
|
||||
boolean supportSize) {
|
||||
Cache books = getCache("books");
|
||||
CacheStatistics cacheStatistics = provider.getCacheStatistics(books,
|
||||
this.cacheManager);
|
||||
assertCoreStatistics(cacheStatistics, (supportSize ? 0L : null), null, null);
|
||||
getOrCreate(books, "a", "b", "b", "a", "a", "a");
|
||||
CacheStatistics updatedCacheStatistics = provider.getCacheStatistics(books,
|
||||
this.cacheManager);
|
||||
assertCoreStatistics(updatedCacheStatistics, (supportSize ? 2L : null), 0.66D,
|
||||
0.33D);
|
||||
}
|
||||
|
||||
private void assertCoreStatistics(CacheStatistics metrics, Long size,
|
||||
Double hitRatio, Double missRatio) {
|
||||
assertNotNull("Cache metrics must not be null", metrics);
|
||||
assertEquals("Wrong size for metrics " + metrics, size, metrics.getSize());
|
||||
checkRatio("Wrong hit ratio for metrics " + metrics, hitRatio,
|
||||
metrics.getHitRatio());
|
||||
checkRatio("Wrong miss ratio for metrics " + metrics, missRatio,
|
||||
metrics.getMissRatio());
|
||||
}
|
||||
|
||||
private void checkRatio(String message, Double expected, Double actual) {
|
||||
if (expected == null || actual == null) {
|
||||
assertEquals(message, expected, actual);
|
||||
}
|
||||
else {
|
||||
assertEquals(message, expected, actual, 0.01D);
|
||||
}
|
||||
}
|
||||
|
||||
private void getOrCreate(Cache cache, String... ids) {
|
||||
for (String id : ids) {
|
||||
Cache.ValueWrapper wrapper = cache.get(id);
|
||||
if (wrapper == null) {
|
||||
cache.put(id, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Cache getCache(String cacheName) {
|
||||
Cache cache = this.cacheManager.getCache(cacheName);
|
||||
Assert.notNull("No cache with name '" + cacheName + "' found.");
|
||||
return cache;
|
||||
}
|
||||
|
||||
private void load(Class<?>... config) {
|
||||
this.context = new AnnotationConfigApplicationContext();
|
||||
if (config.length > 0) {
|
||||
this.context.register(config);
|
||||
}
|
||||
this.context.register(CacheStatisticsProvidersConfiguration.class);
|
||||
this.context.refresh();
|
||||
this.cacheManager = this.context.getBean(CacheManager.class);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class JCacheCacheConfig {
|
||||
|
||||
@Bean
|
||||
public JCacheCacheManager cacheManager() {
|
||||
javax.cache.CacheManager cacheManager = jCacheCacheManager();
|
||||
return new JCacheCacheManager(cacheManager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public javax.cache.CacheManager jCacheCacheManager() {
|
||||
javax.cache.CacheManager cacheManager = Caching.getCachingProvider(
|
||||
HazelcastCachingProvider.class.getName()).getCacheManager();
|
||||
MutableConfiguration<Object, Object> config = new MutableConfiguration<Object, Object>();
|
||||
config.setStatisticsEnabled(true);
|
||||
cacheManager.createCache("books", config);
|
||||
cacheManager.createCache("speakers", config);
|
||||
return cacheManager;
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class EhCacheConfig {
|
||||
|
||||
@Bean
|
||||
public EhCacheCacheManager cacheManager() {
|
||||
return new EhCacheCacheManager(ehCacheCacheManager());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public net.sf.ehcache.CacheManager ehCacheCacheManager() {
|
||||
return EhCacheManagerUtils.buildCacheManager(new ClassPathResource(
|
||||
"cache/test-ehcache.xml"));
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class HazelcastConfig {
|
||||
|
||||
@Bean
|
||||
public HazelcastCacheManager cacheManager() throws IOException {
|
||||
return new HazelcastCacheManager(hazelcastInstance());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HazelcastInstance hazelcastInstance() throws IOException {
|
||||
Resource resource = new ClassPathResource("cache/test-hazelcast.xml");
|
||||
Config cfg = new XmlConfigBuilder(resource.getURL()).build();
|
||||
return Hazelcast.newHazelcastInstance(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class GuavaConfig {
|
||||
|
||||
@Bean
|
||||
public GuavaCacheManager cacheManager() throws IOException {
|
||||
GuavaCacheManager cacheManager = new GuavaCacheManager();
|
||||
cacheManager.setCacheBuilder(CacheBuilder.newBuilder().recordStats());
|
||||
cacheManager.setCacheNames(Arrays.asList("books", "speakers"));
|
||||
return cacheManager;
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ConcurrentMapConfig {
|
||||
|
||||
@Bean
|
||||
public ConcurrentMapCacheManager cacheManager() {
|
||||
return new ConcurrentMapCacheManager("books", "speakers");
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class NoOpCacheConfig {
|
||||
|
||||
@Bean
|
||||
public NoOpCacheManager cacheManager() {
|
||||
return new NoOpCacheManager();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
10
spring-boot-actuator/src/test/resources/cache/test-ehcache.xml
vendored
Normal file
10
spring-boot-actuator/src/test/resources/cache/test-ehcache.xml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<ehcache>
|
||||
<diskStore path="java.io.tmpdir"/>
|
||||
|
||||
<defaultCache/>
|
||||
|
||||
<cache name="books" maxEntriesLocalHeap="50"/>
|
||||
|
||||
<cache name="players" maxEntriesLocalHeap="50"/>
|
||||
|
||||
</ehcache>
|
||||
4
spring-boot-actuator/src/test/resources/cache/test-hazelcast.xml
vendored
Normal file
4
spring-boot-actuator/src/test/resources/cache/test-hazelcast.xml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<hazelcast>
|
||||
<map name="books"/>
|
||||
<map name="players"/>
|
||||
</hazelcast>
|
||||
8
spring-boot-actuator/src/test/resources/hazelcast.xml
Normal file
8
spring-boot-actuator/src/test/resources/hazelcast.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<hazelcast
|
||||
xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.4.xsd"
|
||||
xmlns="http://www.hazelcast.com/schema/config"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
|
||||
<map name="defaultCache" />
|
||||
|
||||
</hazelcast>
|
||||
Reference in New Issue
Block a user