Skip to content

Commit

Permalink
Add caffeine metric collector (#198)
Browse files Browse the repository at this point in the history
Add caffeine metric collector
  • Loading branch information
checketts authored and brian-brazil committed Feb 19, 2017
1 parent d9b9414 commit 0b14621
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 2 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,28 @@ To register the log4j2 collector at root level:
</Configuration>
```

#### 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<String, String> 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<String, String> 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
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<modules>
<module>simpleclient</module>
<module>simpleclient_common</module>
<module>simpleclient_caffeine</module>
<module>simpleclient_dropwizard</module>
<module>simpleclient_graphite_bridge</module>
<module>simpleclient_hibernate</module>
Expand Down
70 changes: 70 additions & 0 deletions simpleclient_caffeine/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.prometheus</groupId>
<artifactId>parent</artifactId>
<version>0.0.21-SNAPSHOT</version>
</parent>

<groupId>io.prometheus</groupId>
<artifactId>simpleclient_caffeine</artifactId>
<packaging>bundle</packaging>

<name>Prometheus Java Simpleclient Caffeine</name>
<description>
Metrics collector for caffeine based caches
</description>

<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>

<developers>
<developer>
<id>checketts</id>
<name>Clint Checketts</name>
<email>checketts@gmail.com</email>
</developer>
</developers>

<dependencies>
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient</artifactId>
<version>0.0.21-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.3.0</version>
</dependency>


<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>2.6.0</version>
<scope>test</scope>
</dependency>

</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* <pre>{@code
*
* // Note that `recordStats()` is required to gather non-zero statistics
* Cache<String, String> cache = Caffeine.newBuilder().recordStats().build();
* CacheMetricsCollector cacheMetrics = new CacheMetricsCollector().register();
* cacheMetrics.addCache("mycache", cache);
*
* }</pre>
*
* Exposed metrics are labeled with the provided cache name.
*
* With the example above, sample metric names would be:
* <pre>
* 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
* </pre>
*
* Additionally if the cache includes a loader, the following metrics would be provided:
* <pre>
* 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
* </pre>
*
*/
public class CacheMetricsCollector extends Collector {
protected final ConcurrentMap<String, Cache> children = new ConcurrentHashMap<String, Cache>();

/**
* Add or replace the cache with the given name.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* Any references to all caches are invalidated.
*/
public void clear(){
children.clear();
}

@Override
public List<MetricFamilySamples> collect() {
List<MetricFamilySamples> mfs = new ArrayList<MetricFamilySamples>();
List<String> 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<String, Cache> c: children.entrySet()) {
List<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> loader = mock(CacheLoader.class);
when(loader.load(anyString()))
.thenReturn("First User")
.thenThrow(new RuntimeException("Seconds time fails"))
.thenReturn("Third User");

LoadingCache<String, String> 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);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 0b14621

Please sign in to comment.