From 0b146218c43adc2eef5514c3381c1eb53d635747 Mon Sep 17 00:00:00 2001 From: checketts Date: Sun, 19 Feb 2017 12:45:49 -0700 Subject: [PATCH] Add caffeine metric collector (#198) Add caffeine metric collector --- README.md | 22 +++ pom.xml | 1 + simpleclient_caffeine/pom.xml | 70 ++++++++ .../cache/caffeine/CacheMetricsCollector.java | 159 ++++++++++++++++++ .../caffeine/CacheMetricsCollectorTest.java | 95 +++++++++++ .../guava/cache/CacheMetricsCollector.java | 2 - 6 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 simpleclient_caffeine/pom.xml create mode 100644 simpleclient_caffeine/src/main/java/io/prometheus/client/cache/caffeine/CacheMetricsCollector.java create mode 100644 simpleclient_caffeine/src/test/java/io/prometheus/client/cache/caffeine/CacheMetricsCollectorTest.java diff --git a/README.md b/README.md index 261d9fdfb..2d70ee18d 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,28 @@ To register the log4j2 collector at root level: ``` +#### Caches + +To register the Guava cache collector, be certain to add `recordStats()` when building +the cache and adding it to the registered collector. + +```java +CacheMetricsCollector cacheMetrics = new CacheMetricsCollector().register(); + +Cache cache = CacheBuilder.newBuilder().recordStats().build(); +cacheMetrics.addCache("myCacheLabel", cache); +``` + +The Caffeine equivalent is nearly identical. Again, be certain to call `recordStats()` + when building the cache so that metrics are collected. + +```java +CacheMetricsCollector cacheMetrics = new CacheMetricsCollector().register(); + +Cache cache = Caffeine.newBuilder().recordStats().build(); +cacheMetrics.addCache("myCacheLabel", cache); +``` + #### Hibernate There is a collector for Hibernate which allows to collect metrics from one or more diff --git a/pom.xml b/pom.xml index a1cfbe499..4d62f9d8e 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,7 @@ simpleclient simpleclient_common + simpleclient_caffeine simpleclient_dropwizard simpleclient_graphite_bridge simpleclient_hibernate diff --git a/simpleclient_caffeine/pom.xml b/simpleclient_caffeine/pom.xml new file mode 100644 index 000000000..0a17682ab --- /dev/null +++ b/simpleclient_caffeine/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + + io.prometheus + parent + 0.0.21-SNAPSHOT + + + io.prometheus + simpleclient_caffeine + bundle + + Prometheus Java Simpleclient Caffeine + + Metrics collector for caffeine based caches + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + checketts + Clint Checketts + checketts@gmail.com + + + + + + io.prometheus + simpleclient + 0.0.21-SNAPSHOT + + + com.github.ben-manes.caffeine + caffeine + 2.3.0 + + + + + junit + junit + 4.11 + test + + + + org.mockito + mockito-core + 1.9.5 + test + + + org.assertj + assertj-core + 2.6.0 + test + + + + diff --git a/simpleclient_caffeine/src/main/java/io/prometheus/client/cache/caffeine/CacheMetricsCollector.java b/simpleclient_caffeine/src/main/java/io/prometheus/client/cache/caffeine/CacheMetricsCollector.java new file mode 100644 index 000000000..8e3e6f161 --- /dev/null +++ b/simpleclient_caffeine/src/main/java/io/prometheus/client/cache/caffeine/CacheMetricsCollector.java @@ -0,0 +1,159 @@ +package io.prometheus.client.cache.caffeine; + +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.github.benmanes.caffeine.cache.stats.CacheStats; +import io.prometheus.client.Collector; +import io.prometheus.client.CounterMetricFamily; +import io.prometheus.client.GaugeMetricFamily; +import io.prometheus.client.SummaryMetricFamily; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + + +/** + * Collect metrics from Caffiene's com.github.benmanes.caffeine.cache.Cache. + *

+ *

{@code
+ *
+ * // Note that `recordStats()` is required to gather non-zero statistics
+ * Cache cache = Caffeine.newBuilder().recordStats().build();
+ * CacheMetricsCollector cacheMetrics = new CacheMetricsCollector().register();
+ * cacheMetrics.addCache("mycache", cache);
+ *
+ * }
+ * + * Exposed metrics are labeled with the provided cache name. + * + * With the example above, sample metric names would be: + *
+ *     caffeine_cache_hit_total{cache="mycache"} 10.0
+ *     caffeine_cache_miss_total{cache="mycache"} 3.0
+ *     caffeine_cache_requests_total{cache="mycache"} 13.0
+ *     caffeine_cache_eviction_total{cache="mycache"} 1.0
+ * 
+ * + * Additionally if the cache includes a loader, the following metrics would be provided: + *
+ *     caffeine_cache_load_failure_total{cache="mycache"} 2.0
+ *     caffeine_cache_loads_total{cache="mycache"} 7.0
+ *     caffeine_cache_load_duration_seconds_count{cache="mycache"} 7.0
+ *     caffeine_cache_load_duration_seconds_sum{cache="mycache"} 0.0034
+ * 
+ * + */ +public class CacheMetricsCollector extends Collector { + protected final ConcurrentMap children = new ConcurrentHashMap(); + + /** + * Add or replace the cache with the given name. + *

+ * Any references any previous cache with this name is invalidated. + * + * @param cacheName The name of the cache, will be the metrics label value + * @param cache The cache being monitored + */ + public void addCache(String cacheName, Cache cache) { + children.put(cacheName, cache); + } + + /** + * Add or replace the cache with the given name. + *

+ * Any references any previous cache with this name is invalidated. + * + * @param cacheName The name of the cache, will be the metrics label value + * @param cache The cache being monitored + */ + public void addCache(String cacheName, AsyncLoadingCache cache) { + children.put(cacheName, cache.synchronous()); + } + + /** + * Remove the cache with the given name. + *

+ * Any references to the cache are invalidated. + * + * @param cacheName cache to be removed + */ + public Cache removeCache(String cacheName) { + return children.remove(cacheName); + } + + /** + * Remove all caches. + *

+ * Any references to all caches are invalidated. + */ + public void clear(){ + children.clear(); + } + + @Override + public List collect() { + List mfs = new ArrayList(); + List labelNames = Arrays.asList("cache"); + + CounterMetricFamily cacheHitTotal = new CounterMetricFamily("caffeine_cache_hit_total", + "Cache hit totals", labelNames); + mfs.add(cacheHitTotal); + + CounterMetricFamily cacheMissTotal = new CounterMetricFamily("caffeine_cache_miss_total", + "Cache miss totals", labelNames); + mfs.add(cacheMissTotal); + + CounterMetricFamily cacheRequestsTotal = new CounterMetricFamily("caffeine_cache_requests_total", + "Cache request totals, hits + misses", labelNames); + mfs.add(cacheRequestsTotal); + + CounterMetricFamily cacheEvictionTotal = new CounterMetricFamily("caffeine_cache_eviction_total", + "Cache eviction totals, doesn't include manually removed entries", labelNames); + mfs.add(cacheEvictionTotal); + + GaugeMetricFamily cacheEvictionWeight = new GaugeMetricFamily("caffeine_cache_eviction_weight", + "Cache eviction weight", labelNames); + mfs.add(cacheEvictionWeight); + + CounterMetricFamily cacheLoadFailure = new CounterMetricFamily("caffeine_cache_load_failure_total", + "Cache load failures", labelNames); + mfs.add(cacheLoadFailure); + + CounterMetricFamily cacheLoadTotal = new CounterMetricFamily("caffeine_cache_loads_total", + "Cache loads: both success and failures", labelNames); + mfs.add(cacheLoadTotal); + + SummaryMetricFamily cacheLoadSummary = new SummaryMetricFamily("caffeine_cache_load_duration_seconds", + "Cache load duration: both success and failures", labelNames); + mfs.add(cacheLoadSummary); + + for(Map.Entry c: children.entrySet()) { + List cacheName = Arrays.asList(c.getKey()); + CacheStats stats = c.getValue().stats(); + + try{ + cacheEvictionWeight.addMetric(cacheName, stats.evictionWeight()); + } catch (Exception e) { + // EvictionWeight metric is unavailable, newer version of Caffeine is needed. + } + + cacheHitTotal.addMetric(cacheName, stats.hitCount()); + cacheMissTotal.addMetric(cacheName, stats.missCount()); + cacheRequestsTotal.addMetric(cacheName, stats.requestCount()); + cacheEvictionTotal.addMetric(cacheName, stats.evictionCount()); + + if(c.getValue() instanceof LoadingCache) { + cacheLoadFailure.addMetric(cacheName, stats.loadFailureCount()); + cacheLoadTotal.addMetric(cacheName, stats.loadCount()); + + cacheLoadSummary.addMetric(cacheName, stats.loadCount(), stats.totalLoadTime() / Collector.NANOSECONDS_PER_SECOND); + } + } + return mfs; + } +} diff --git a/simpleclient_caffeine/src/test/java/io/prometheus/client/cache/caffeine/CacheMetricsCollectorTest.java b/simpleclient_caffeine/src/test/java/io/prometheus/client/cache/caffeine/CacheMetricsCollectorTest.java new file mode 100644 index 000000000..3bc034b17 --- /dev/null +++ b/simpleclient_caffeine/src/test/java/io/prometheus/client/cache/caffeine/CacheMetricsCollectorTest.java @@ -0,0 +1,95 @@ +package io.prometheus.client.cache.caffeine; + +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import io.prometheus.client.CollectorRegistry; +import org.junit.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CacheMetricsCollectorTest { + + @Test + public void cacheExposesMetricsForHitMissAndEviction() throws Exception { + Cache cache = Caffeine.newBuilder().maximumSize(2).recordStats().executor(new Executor() { + @Override + public void execute(Runnable command) { + // Run cleanup in same thread, to remove async behavior with evictions + command.run(); + } + }).build(); + CollectorRegistry registry = new CollectorRegistry(); + + CacheMetricsCollector collector = new CacheMetricsCollector().register(registry); + collector.addCache("users", cache); + + cache.getIfPresent("user1"); + cache.getIfPresent("user1"); + cache.put("user1", "First User"); + cache.getIfPresent("user1"); + + // Add to cache to trigger eviction. + cache.put("user2", "Second User"); + cache.put("user3", "Third User"); + cache.put("user4", "Fourth User"); + + assertMetric(registry, "caffeine_cache_hit_total", "users", 1.0); + assertMetric(registry, "caffeine_cache_miss_total", "users", 2.0); + assertMetric(registry, "caffeine_cache_requests_total", "users", 3.0); + assertMetric(registry, "caffeine_cache_eviction_total", "users", 2.0); + } + + + @SuppressWarnings("unchecked") + @Test + public void loadingCacheExposesMetricsForLoadsAndExceptions() throws Exception { + CacheLoader loader = mock(CacheLoader.class); + when(loader.load(anyString())) + .thenReturn("First User") + .thenThrow(new RuntimeException("Seconds time fails")) + .thenReturn("Third User"); + + LoadingCache cache = Caffeine.newBuilder().recordStats().build(loader); + CollectorRegistry registry = new CollectorRegistry(); + CacheMetricsCollector collector = new CacheMetricsCollector().register(registry); + collector.addCache("loadingusers", cache); + + cache.get("user1"); + cache.get("user1"); + try { + cache.get("user2"); + } catch (Exception e) { + // ignoring. + } + cache.get("user3"); + + assertMetric(registry, "caffeine_cache_hit_total", "loadingusers", 1.0); + assertMetric(registry, "caffeine_cache_miss_total", "loadingusers", 3.0); + + assertMetric(registry, "caffeine_cache_load_failure_total", "loadingusers", 1.0); + assertMetric(registry, "caffeine_cache_loads_total", "loadingusers", 3.0); + + assertMetric(registry, "caffeine_cache_load_duration_seconds_count", "loadingusers", 3.0); + assertMetricGreatThan(registry, "caffeine_cache_load_duration_seconds_sum", "loadingusers", 0.0); + } + + private void assertMetric(CollectorRegistry registry, String name, String cacheName, double value) { + assertThat(registry.getSampleValue(name, new String[]{"cache"}, new String[]{cacheName})).isEqualTo(value); + } + + + private void assertMetricGreatThan(CollectorRegistry registry, String name, String cacheName, double value) { + assertThat(registry.getSampleValue(name, new String[]{"cache"}, new String[]{cacheName})).isGreaterThan(value); + } + + +} diff --git a/simpleclient_guava/src/main/java/io/prometheus/client/guava/cache/CacheMetricsCollector.java b/simpleclient_guava/src/main/java/io/prometheus/client/guava/cache/CacheMetricsCollector.java index 4194e78ab..72492fe64 100644 --- a/simpleclient_guava/src/main/java/io/prometheus/client/guava/cache/CacheMetricsCollector.java +++ b/simpleclient_guava/src/main/java/io/prometheus/client/guava/cache/CacheMetricsCollector.java @@ -4,9 +4,7 @@ import com.google.common.cache.CacheStats; import com.google.common.cache.LoadingCache; import io.prometheus.client.Collector; -import io.prometheus.client.Counter; import io.prometheus.client.CounterMetricFamily; -import io.prometheus.client.Gauge; import io.prometheus.client.SummaryMetricFamily; import java.util.ArrayList;