diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index 924506687e..caa62a59fe 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -41,6 +41,21 @@ spring-context + + com.google.guava + guava + true + + + com.hazelcast + hazelcast + true + + + com.hazelcast + hazelcast-spring + true + com.sun.mail javax.mail @@ -51,11 +66,21 @@ metrics-core true + + javax.cache + cache-api + true + javax.servlet javax.servlet-api true + + net.sf.ehcache + ehcache + true + org.apache.activemq activemq-broker diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfiguration.java index a70ecd13f2..182e2e8aed 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2014 the original author or authors. + * 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. @@ -21,6 +21,9 @@ import javax.sql.DataSource; import org.apache.catalina.startup.Tomcat; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.cache.CacheStatisticsProvider; +import org.springframework.boot.actuate.cache.CacheStatisticsProvidersConfiguration; +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; @@ -33,13 +36,16 @@ import org.springframework.boot.actuate.metrics.rich.RichGaugeReader; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.metadata.DataSourcePoolMetadataProvider; +import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link PublicMetrics}. @@ -50,7 +56,7 @@ import org.springframework.context.annotation.Configuration; * @since 1.2.0 */ @Configuration -@AutoConfigureAfter({ DataSourceAutoConfiguration.class, +@AutoConfigureAfter({ DataSourceAutoConfiguration.class, CacheAutoConfiguration.class, MetricRepositoryAutoConfiguration.class }) @AutoConfigureBefore(EndpointAutoConfiguration.class) public class PublicMetricsAutoConfiguration { @@ -75,6 +81,7 @@ public class PublicMetricsAutoConfiguration { return new RichGaugeReaderPublicMetrics(richGaugeReader); } + @Configuration @ConditionalOnClass(DataSource.class) @ConditionalOnBean(DataSource.class) static class DataSourceMetricsConfiguration { @@ -88,6 +95,7 @@ public class PublicMetricsAutoConfiguration { } + @Configuration @ConditionalOnClass({ Servlet.class, Tomcat.class }) static class TomcatMetricsConfiguration { @@ -99,4 +107,19 @@ public class PublicMetricsAutoConfiguration { } + @Configuration + @ConditionalOnClass(CacheManager.class) + @ConditionalOnBean(CacheManager.class) + @Import(CacheStatisticsProvidersConfiguration.class) + static class CacheStatisticsConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean(CacheStatisticsProvider.class) + public CachePublicMetrics cachePublicMetrics() { + return new CachePublicMetrics(); + } + + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatistics.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatistics.java new file mode 100644 index 0000000000..f6cc96e1f9 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatistics.java @@ -0,0 +1,66 @@ +/* + * 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.util.Collection; + +import org.springframework.boot.actuate.metrics.Metric; + +/** + * Snapshot of the statistics of a given cache. {@code CacheStatistics} instances have a + * very short life as it represents the statistics of a cache at one particular point in + * time. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public interface CacheStatistics { + + /** + * Generate the relevant {@link Metric} instances based on the specified prefix. + * @param prefix the metrics prefix (ends with '.') + * @return the metrics corresponding to this instance + */ + Collection> toMetrics(String prefix); + + /** + * Return the size of the cache or {@code null} if that information is not available. + * @return the size of the cache or {@code null} + */ + Long getSize(); + + /** + * Return the ratio of cache requests which were hits as a value between 0 and 1 where + * 0 means that the hit ratio is 0% and 1 means it is 100%. + *

+ * This may also return {@code null} if the cache-specifics statistics does not + * provide the necessary information + * @return the hit ratio or {@code null} + */ + Double getHitRatio(); + + /** + * Return the ratio of cache requests which were misses as value between 0 and 1 where + * 0 means that the miss ratio is 0% and 1 means it is 100%. + *

+ * This may also return {@code null} if the cache-specifics statistics does not + * provide the necessary information + * @return the miss ratio or {@code null} + */ + Double getMissRatio(); + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatisticsProvider.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatisticsProvider.java new file mode 100644 index 0000000000..efa876c80c --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatisticsProvider.java @@ -0,0 +1,39 @@ +/* + * 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 org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +/** + * Provide a {@link CacheStatistics} based on a {@link Cache}. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public interface CacheStatisticsProvider { + + /** + * Return the current {@link CacheStatistics} snapshot for the specified {@link Cache} + * or {@code null} if the given cache could not be handled. + * @param cache the cache to handle + * @param cacheManager the {@link CacheManager} handling this cache + * @return the current cache statistics or {@code null} + */ + CacheStatistics getCacheStatistics(Cache cache, CacheManager cacheManager); + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatisticsProviders.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatisticsProviders.java new file mode 100644 index 0000000000..cf85f6931d --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatisticsProviders.java @@ -0,0 +1,62 @@ +/* + * 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.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +/** + * A {@link CacheStatisticsProvider} that returns the first {@link CacheStatistics} that + * can be retrieved by one of its delegates. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class CacheStatisticsProviders implements CacheStatisticsProvider { + + private final List providers; + + /** + * Create a {@link CacheStatisticsProviders} instance with the collection of delegates + * to use. + * @param providers the cache statistics providers + */ + public CacheStatisticsProviders( + Collection providers) { + this.providers = (providers == null ? Collections + . emptyList() + : new ArrayList(providers)); + } + + @Override + public CacheStatistics getCacheStatistics(Cache cache, CacheManager cacheManager) { + for (CacheStatisticsProvider provider : this.providers) { + CacheStatistics cacheStatistics = provider.getCacheStatistics(cache, + cacheManager); + if (cacheStatistics != null) { + return cacheStatistics; + } + } + return null; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatisticsProvidersConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatisticsProvidersConfiguration.java new file mode 100644 index 0000000000..ee70e1edfc --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CacheStatisticsProvidersConfiguration.java @@ -0,0 +1,204 @@ +/* + * 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.util.concurrent.ConcurrentHashMap; + +import javax.cache.Caching; + +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.statistics.StatisticsGateway; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.ehcache.EhCacheCache; +import org.springframework.cache.guava.GuavaCache; +import org.springframework.cache.jcache.JCacheCache; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.google.common.cache.CacheStats; +import com.hazelcast.core.IMap; +import com.hazelcast.monitor.LocalMapStats; +import com.hazelcast.spring.cache.HazelcastCache; + +/** + * Register the {@link CacheStatisticsProvider} instances for the supported cache + * libraries. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@Configuration +@ConditionalOnBean(CacheManager.class) +public class CacheStatisticsProvidersConfiguration { + + @Configuration + @ConditionalOnClass({ Caching.class, JCacheCache.class }) + static class JCacheCacheStatisticsProviderConfiguration { + + @Bean + public CacheStatisticsProvider jCacheCacheStatisticsProvider() { + return new JCacheCacheStatisticsProvider(); + } + } + + @Configuration + @ConditionalOnClass(Ehcache.class) + static class EhCacheCacheStatisticsProviderConfiguration { + + @Bean + public CacheStatisticsProvider ehCacheCacheStatisticsProvider() { + return new CacheStatisticsProvider() { + @Override + public CacheStatistics getCacheStatistics(Cache cache, + CacheManager cacheManager) { + if (cache instanceof EhCacheCache) { + return getEhCacheStatistics((Ehcache) cache.getNativeCache()); + } + return null; + } + }; + } + + private CacheStatistics getEhCacheStatistics(Ehcache cache) { + StatisticsGateway statistics = cache.getStatistics(); + DefaultCacheStatistics stats = new DefaultCacheStatistics(); + stats.setSize(statistics.getSize()); + Double hitRatio = statistics.cacheHitRatio(); + if (!hitRatio.isNaN()) { + stats.setHitRatio(hitRatio); + stats.setMissRatio(1 - hitRatio); + } + return stats; + } + } + + @Configuration + @ConditionalOnClass(IMap.class) + static class HazelcastCacheStatisticsConfiguration { + + @Bean + public CacheStatisticsProvider hazelcastCacheStatisticsProvider() { + return new CacheStatisticsProvider() { + @Override + public CacheStatistics getCacheStatistics(Cache cache, + CacheManager cacheManager) { + if (cache instanceof HazelcastCache) { + return getHazelcastStatistics((IMap) cache.getNativeCache()); + } + return null; + } + }; + } + + private CacheStatistics getHazelcastStatistics(IMap cache) { + DefaultCacheStatistics stats = new DefaultCacheStatistics(); + LocalMapStats mapStats = cache.getLocalMapStats(); + stats.setSize(mapStats.getOwnedEntryCount()); + stats.setGetCacheCounts(mapStats.getHits(), mapStats.getGetOperationCount() + - mapStats.getHits()); + return stats; + } + } + + @Configuration + @ConditionalOnClass(com.google.common.cache.Cache.class) + static class GuavaCacheStatisticsConfiguration { + + @Bean + public CacheStatisticsProvider guavaCacheStatisticsProvider() { + return new CacheStatisticsProvider() { + @SuppressWarnings("unchecked") + @Override + public CacheStatistics getCacheStatistics(Cache cache, + CacheManager cacheManager) { + if (cache instanceof GuavaCache) { + return getGuavaStatistics((com.google.common.cache.Cache) cache + .getNativeCache()); + } + return null; + } + }; + } + + private CacheStatistics getGuavaStatistics( + com.google.common.cache.Cache cache) { + DefaultCacheStatistics stats = new DefaultCacheStatistics(); + stats.setSize(cache.size()); + CacheStats guavaStats = cache.stats(); + if (guavaStats.requestCount() > 0) { + stats.setHitRatio(guavaStats.hitRate()); + stats.setMissRatio(guavaStats.missRate()); + } + return stats; + } + + } + + @Configuration + @ConditionalOnClass(ConcurrentMapCache.class) + static class ConcurrentMapCacheStatisticsConfiguration { + + @Bean + public CacheStatisticsProvider concurrentMapCacheStatisticsProvider() { + return new CacheStatisticsProvider() { + @Override + public CacheStatistics getCacheStatistics(Cache cache, + CacheManager cacheManager) { + if (cache instanceof ConcurrentMapCache) { + return getConcurrentMapStatistics((ConcurrentHashMap) cache + .getNativeCache()); + } + return null; + } + }; + } + + private CacheStatistics getConcurrentMapStatistics(ConcurrentHashMap map) { + DefaultCacheStatistics stats = new DefaultCacheStatistics(); + stats.setSize((long) map.size()); + return stats; + } + } + + @Configuration + @ConditionalOnClass(NoOpCacheManager.class) + static class NoOpCacheStatisticsConfiguration { + + private static final CacheStatistics NO_OP_STATS = new DefaultCacheStatistics(); + + @Bean + public CacheStatisticsProvider noOpCacheStatisticsProvider() { + return new CacheStatisticsProvider() { + @Override + public CacheStatistics getCacheStatistics(Cache cache, + CacheManager cacheManager) { + if (cacheManager instanceof NoOpCacheManager) { + return NO_OP_STATS; + } + return null; + } + }; + } + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/DefaultCacheStatistics.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/DefaultCacheStatistics.java new file mode 100644 index 0000000000..d080a0cac9 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/DefaultCacheStatistics.java @@ -0,0 +1,89 @@ +/* + * 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.util.ArrayList; +import java.util.Collection; + +import org.springframework.boot.actuate.metrics.Metric; + +/** + * A default {@link CacheStatistics} implementation. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class DefaultCacheStatistics implements CacheStatistics { + + private Long size; + + private Double hitRatio; + + private Double missRatio; + + @Override + public Collection> toMetrics(String prefix) { + Collection> result = new ArrayList>(); + addMetric(result, prefix + "size", getSize()); + addMetric(result, prefix + "hit.ratio", getHitRatio()); + addMetric(result, prefix + "miss.ratio", getMissRatio()); + return result; + } + + public void setGetCacheCounts(long hitCount, long missCount) { + long total = hitCount + missCount; + if (total > 0) { + double hitRatio = hitCount / (double) total; + setHitRatio(hitRatio); + setMissRatio(1 - hitRatio); + } + } + + @Override + public Long getSize() { + return this.size; + } + + public void setSize(Long size) { + this.size = size; + } + + @Override + public Double getHitRatio() { + return this.hitRatio; + } + + public void setHitRatio(Double hitRatio) { + this.hitRatio = hitRatio; + } + + @Override + public Double getMissRatio() { + return this.missRatio; + } + + public void setMissRatio(Double missRatio) { + this.missRatio = missRatio; + } + + private void addMetric(Collection> metrics, String name, + T value) { + if (value != null) { + metrics.add(new Metric(name, value)); + } + } +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/JCacheCacheStatisticsProvider.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/JCacheCacheStatisticsProvider.java new file mode 100644 index 0000000000..5cedca14b2 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/JCacheCacheStatisticsProvider.java @@ -0,0 +1,136 @@ +/* + * 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.lang.management.ManagementFactory; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectInstance; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.jcache.JCacheCache; + +/** + * {@link CacheStatisticsProvider} implementation for a JSR-107 compliant cache. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +class JCacheCacheStatisticsProvider implements CacheStatisticsProvider { + + private static final Logger logger = LoggerFactory + .getLogger(JCacheCacheStatisticsProvider.class); + + private MBeanServer mBeanServer; + + private Map caches = new ConcurrentHashMap(); + + @Override + public CacheStatistics getCacheStatistics(Cache cache, CacheManager cacheManager) { + if (cache instanceof JCacheCache) { + return getCacheStatistics((JCacheCache) cache); + } + return null; + } + + protected CacheStatistics getCacheStatistics(JCacheCache cache) { + try { + ObjectName objectName = getObjectName(cache); + if (objectName != null) { + return getCacheStatistics(objectName); + } + return null; + } + catch (MalformedObjectNameException e) { + throw new IllegalStateException(e); + } + } + + protected CacheStatistics getCacheStatistics(ObjectName objectName) { + MBeanServer mBeanServer = getMBeanServer(); + DefaultCacheStatistics stats = new DefaultCacheStatistics(); + Float hitPercentage = getAttribute(mBeanServer, objectName, "CacheHitPercentage", + Float.class); + Float missPercentage = getAttribute(mBeanServer, objectName, + "CacheMissPercentage", Float.class); + if ((hitPercentage != null && missPercentage != null) + && (hitPercentage > 0 || missPercentage > 0)) { + stats.setHitRatio(hitPercentage / (double) 100); + stats.setMissRatio(missPercentage / (double) 100); + } + return stats; + } + + protected ObjectName getObjectName(JCacheCache cache) + throws MalformedObjectNameException { + if (this.caches.containsKey(cache)) { + return this.caches.get(cache); + } + + Set instances = getMBeanServer().queryMBeans( + new ObjectName("javax.cache:type=CacheStatistics,Cache=" + + cache.getName() + ",*"), null); + if (instances.size() == 1) { + ObjectName objectName = instances.iterator().next().getObjectName(); + this.caches.put(cache, objectName); + return objectName; + } + return null; // None or more than one + } + + protected MBeanServer getMBeanServer() { + if (this.mBeanServer == null) { + this.mBeanServer = ManagementFactory.getPlatformMBeanServer(); + } + return this.mBeanServer; + } + + private static T getAttribute(MBeanServer mBeanServer, ObjectName objectName, + String attributeName, Class type) { + try { + Object attribute = mBeanServer.getAttribute(objectName, attributeName); + return type.cast(attribute); + } + catch (MBeanException e) { + throw new IllegalStateException(e); + } + catch (AttributeNotFoundException e) { + throw new IllegalStateException("Unexpected: jcache provider does not " + + "expose standard attribute " + attributeName, e); + } + catch (InstanceNotFoundException e) { + logger.warn("Cache statistics are no longer available", e); + } + catch (ReflectionException e) { + throw new IllegalStateException(e); + } + return null; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/CachePublicMetrics.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/CachePublicMetrics.java new file mode 100644 index 0000000000..89eee1b3a1 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/CachePublicMetrics.java @@ -0,0 +1,104 @@ +/* + * 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.endpoint; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.cache.CacheStatistics; +import org.springframework.boot.actuate.cache.CacheStatisticsProvider; +import org.springframework.boot.actuate.cache.CacheStatisticsProviders; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +/** + * A {@link PublicMetrics} implementation that provides cache statistics. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class CachePublicMetrics implements PublicMetrics { + + @Autowired + private Collection providers; + + @Autowired + private Map cacheManagers; + + private CacheStatisticsProvider cacheStatisticsProvider; + + @PostConstruct + public void initialize() { + this.cacheStatisticsProvider = new CacheStatisticsProviders(this.providers); + } + + @Override + public Collection> metrics() { + Set targetNames = new HashSet(); + Collection> metrics = new HashSet>(); + for (Map.Entry entry : this.cacheManagers.entrySet()) { + String cacheManagerName = entry.getKey(); + CacheManager cacheManager = entry.getValue(); + for (String cacheName : cacheManager.getCacheNames()) { + Cache cache = cacheManager.getCache(cacheName); + CacheStatistics cacheStatistics = this.cacheStatisticsProvider + .getCacheStatistics(cache, cacheManager); + if (cacheStatistics != null) { + String prefix = cleanPrefix(createPrefix(targetNames, cacheName, + cacheManagerName)); + metrics.addAll(cacheStatistics.toMetrics(prefix)); + } + } + } + return metrics; + } + + /** + * Create the prefix to use for the specified cache. The specified {@code targetNames} + * set contains the names that have been acquired so far. + * @param targetNames the target names that have been used for other caches + * @param cacheName the name of the cache + * @param cacheManagerName the name of its cache manager + * @return a prefix to use for the specified cache + */ + protected String createPrefix(Set targetNames, String cacheName, + String cacheManagerName) { + if (targetNames.contains(cacheName)) { + String target = cacheManagerName + "_" + cacheName; + return createPrefixFor(target); + } + else { + targetNames.add(cacheName); + return createPrefixFor(cacheName); + } + } + + protected String createPrefixFor(String name) { + return "cache." + name; + } + + private String cleanPrefix(String prefix) { + return (prefix.endsWith(".") ? prefix : prefix + "."); + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfigurationTests.java index 24cc15bf01..2a6bbf95db 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/PublicMetricsAutoConfigurationTests.java @@ -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> 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> metrics = bean.metrics(); + assertMetrics(metrics, "cache.books.size", "cache.second_speakers.size", + "cache.speakers.size", "cache.users.size"); + } + private void assertHasMetric(Collection> 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"); + } + } + } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CacheStatisticsProviderTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CacheStatisticsProviderTests.java new file mode 100644 index 0000000000..b90e5677db --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CacheStatisticsProviderTests.java @@ -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 config = new MutableConfiguration(); + 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(); + } + } + +} diff --git a/spring-boot-actuator/src/test/resources/cache/test-ehcache.xml b/spring-boot-actuator/src/test/resources/cache/test-ehcache.xml new file mode 100644 index 0000000000..9a50bbab4b --- /dev/null +++ b/spring-boot-actuator/src/test/resources/cache/test-ehcache.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-actuator/src/test/resources/cache/test-hazelcast.xml b/spring-boot-actuator/src/test/resources/cache/test-hazelcast.xml new file mode 100644 index 0000000000..ce85d2df92 --- /dev/null +++ b/spring-boot-actuator/src/test/resources/cache/test-hazelcast.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/spring-boot-actuator/src/test/resources/hazelcast.xml b/spring-boot-actuator/src/test/resources/hazelcast.xml new file mode 100644 index 0000000000..0806a6dcfc --- /dev/null +++ b/spring-boot-actuator/src/test/resources/hazelcast.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/spring-boot-parent/pom.xml b/spring-boot-parent/pom.xml index cf40c72b88..9d2c7b62d2 100644 --- a/spring-boot-parent/pom.xml +++ b/spring-boot-parent/pom.xml @@ -46,6 +46,11 @@ + + com.google.guava + guava + 18.0 + io.spring.gradle dependency-management-plugin