Skip to content

Commit 0b14621

Browse files
checkettsbrian-brazil
authored andcommitted
Add caffeine metric collector (#198)
Add caffeine metric collector
1 parent d9b9414 commit 0b14621

File tree

6 files changed

+347
-2
lines changed

6 files changed

+347
-2
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,28 @@ To register the log4j2 collector at root level:
274274
</Configuration>
275275
```
276276

277+
#### Caches
278+
279+
To register the Guava cache collector, be certain to add `recordStats()` when building
280+
the cache and adding it to the registered collector.
281+
282+
```java
283+
CacheMetricsCollector cacheMetrics = new CacheMetricsCollector().register();
284+
285+
Cache<String, String> cache = CacheBuilder.newBuilder().recordStats().build();
286+
cacheMetrics.addCache("myCacheLabel", cache);
287+
```
288+
289+
The Caffeine equivalent is nearly identical. Again, be certain to call `recordStats()`
290+
when building the cache so that metrics are collected.
291+
292+
```java
293+
CacheMetricsCollector cacheMetrics = new CacheMetricsCollector().register();
294+
295+
Cache<String, String> cache = Caffeine.newBuilder().recordStats().build();
296+
cacheMetrics.addCache("myCacheLabel", cache);
297+
```
298+
277299
#### Hibernate
278300

279301
There is a collector for Hibernate which allows to collect metrics from one or more

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<modules>
4646
<module>simpleclient</module>
4747
<module>simpleclient_common</module>
48+
<module>simpleclient_caffeine</module>
4849
<module>simpleclient_dropwizard</module>
4950
<module>simpleclient_graphite_bridge</module>
5051
<module>simpleclient_hibernate</module>

simpleclient_caffeine/pom.xml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<parent>
6+
<groupId>io.prometheus</groupId>
7+
<artifactId>parent</artifactId>
8+
<version>0.0.21-SNAPSHOT</version>
9+
</parent>
10+
11+
<groupId>io.prometheus</groupId>
12+
<artifactId>simpleclient_caffeine</artifactId>
13+
<packaging>bundle</packaging>
14+
15+
<name>Prometheus Java Simpleclient Caffeine</name>
16+
<description>
17+
Metrics collector for caffeine based caches
18+
</description>
19+
20+
<licenses>
21+
<license>
22+
<name>The Apache Software License, Version 2.0</name>
23+
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
24+
<distribution>repo</distribution>
25+
</license>
26+
</licenses>
27+
28+
<developers>
29+
<developer>
30+
<id>checketts</id>
31+
<name>Clint Checketts</name>
32+
<email>[email protected]</email>
33+
</developer>
34+
</developers>
35+
36+
<dependencies>
37+
<dependency>
38+
<groupId>io.prometheus</groupId>
39+
<artifactId>simpleclient</artifactId>
40+
<version>0.0.21-SNAPSHOT</version>
41+
</dependency>
42+
<dependency>
43+
<groupId>com.github.ben-manes.caffeine</groupId>
44+
<artifactId>caffeine</artifactId>
45+
<version>2.3.0</version>
46+
</dependency>
47+
48+
49+
<dependency>
50+
<groupId>junit</groupId>
51+
<artifactId>junit</artifactId>
52+
<version>4.11</version>
53+
<scope>test</scope>
54+
</dependency>
55+
56+
<dependency>
57+
<groupId>org.mockito</groupId>
58+
<artifactId>mockito-core</artifactId>
59+
<version>1.9.5</version>
60+
<scope>test</scope>
61+
</dependency>
62+
<dependency>
63+
<groupId>org.assertj</groupId>
64+
<artifactId>assertj-core</artifactId>
65+
<version>2.6.0</version>
66+
<scope>test</scope>
67+
</dependency>
68+
69+
</dependencies>
70+
</project>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package io.prometheus.client.cache.caffeine;
2+
3+
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
4+
import com.github.benmanes.caffeine.cache.Cache;
5+
import com.github.benmanes.caffeine.cache.LoadingCache;
6+
import com.github.benmanes.caffeine.cache.stats.CacheStats;
7+
import io.prometheus.client.Collector;
8+
import io.prometheus.client.CounterMetricFamily;
9+
import io.prometheus.client.GaugeMetricFamily;
10+
import io.prometheus.client.SummaryMetricFamily;
11+
12+
import java.util.ArrayList;
13+
import java.util.Arrays;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.concurrent.ConcurrentHashMap;
17+
import java.util.concurrent.ConcurrentMap;
18+
19+
20+
/**
21+
* Collect metrics from Caffiene's com.github.benmanes.caffeine.cache.Cache.
22+
* <p>
23+
* <pre>{@code
24+
*
25+
* // Note that `recordStats()` is required to gather non-zero statistics
26+
* Cache<String, String> cache = Caffeine.newBuilder().recordStats().build();
27+
* CacheMetricsCollector cacheMetrics = new CacheMetricsCollector().register();
28+
* cacheMetrics.addCache("mycache", cache);
29+
*
30+
* }</pre>
31+
*
32+
* Exposed metrics are labeled with the provided cache name.
33+
*
34+
* With the example above, sample metric names would be:
35+
* <pre>
36+
* caffeine_cache_hit_total{cache="mycache"} 10.0
37+
* caffeine_cache_miss_total{cache="mycache"} 3.0
38+
* caffeine_cache_requests_total{cache="mycache"} 13.0
39+
* caffeine_cache_eviction_total{cache="mycache"} 1.0
40+
* </pre>
41+
*
42+
* Additionally if the cache includes a loader, the following metrics would be provided:
43+
* <pre>
44+
* caffeine_cache_load_failure_total{cache="mycache"} 2.0
45+
* caffeine_cache_loads_total{cache="mycache"} 7.0
46+
* caffeine_cache_load_duration_seconds_count{cache="mycache"} 7.0
47+
* caffeine_cache_load_duration_seconds_sum{cache="mycache"} 0.0034
48+
* </pre>
49+
*
50+
*/
51+
public class CacheMetricsCollector extends Collector {
52+
protected final ConcurrentMap<String, Cache> children = new ConcurrentHashMap<String, Cache>();
53+
54+
/**
55+
* Add or replace the cache with the given name.
56+
* <p>
57+
* Any references any previous cache with this name is invalidated.
58+
*
59+
* @param cacheName The name of the cache, will be the metrics label value
60+
* @param cache The cache being monitored
61+
*/
62+
public void addCache(String cacheName, Cache cache) {
63+
children.put(cacheName, cache);
64+
}
65+
66+
/**
67+
* Add or replace the cache with the given name.
68+
* <p>
69+
* Any references any previous cache with this name is invalidated.
70+
*
71+
* @param cacheName The name of the cache, will be the metrics label value
72+
* @param cache The cache being monitored
73+
*/
74+
public void addCache(String cacheName, AsyncLoadingCache cache) {
75+
children.put(cacheName, cache.synchronous());
76+
}
77+
78+
/**
79+
* Remove the cache with the given name.
80+
* <p>
81+
* Any references to the cache are invalidated.
82+
*
83+
* @param cacheName cache to be removed
84+
*/
85+
public Cache removeCache(String cacheName) {
86+
return children.remove(cacheName);
87+
}
88+
89+
/**
90+
* Remove all caches.
91+
* <p>
92+
* Any references to all caches are invalidated.
93+
*/
94+
public void clear(){
95+
children.clear();
96+
}
97+
98+
@Override
99+
public List<MetricFamilySamples> collect() {
100+
List<MetricFamilySamples> mfs = new ArrayList<MetricFamilySamples>();
101+
List<String> labelNames = Arrays.asList("cache");
102+
103+
CounterMetricFamily cacheHitTotal = new CounterMetricFamily("caffeine_cache_hit_total",
104+
"Cache hit totals", labelNames);
105+
mfs.add(cacheHitTotal);
106+
107+
CounterMetricFamily cacheMissTotal = new CounterMetricFamily("caffeine_cache_miss_total",
108+
"Cache miss totals", labelNames);
109+
mfs.add(cacheMissTotal);
110+
111+
CounterMetricFamily cacheRequestsTotal = new CounterMetricFamily("caffeine_cache_requests_total",
112+
"Cache request totals, hits + misses", labelNames);
113+
mfs.add(cacheRequestsTotal);
114+
115+
CounterMetricFamily cacheEvictionTotal = new CounterMetricFamily("caffeine_cache_eviction_total",
116+
"Cache eviction totals, doesn't include manually removed entries", labelNames);
117+
mfs.add(cacheEvictionTotal);
118+
119+
GaugeMetricFamily cacheEvictionWeight = new GaugeMetricFamily("caffeine_cache_eviction_weight",
120+
"Cache eviction weight", labelNames);
121+
mfs.add(cacheEvictionWeight);
122+
123+
CounterMetricFamily cacheLoadFailure = new CounterMetricFamily("caffeine_cache_load_failure_total",
124+
"Cache load failures", labelNames);
125+
mfs.add(cacheLoadFailure);
126+
127+
CounterMetricFamily cacheLoadTotal = new CounterMetricFamily("caffeine_cache_loads_total",
128+
"Cache loads: both success and failures", labelNames);
129+
mfs.add(cacheLoadTotal);
130+
131+
SummaryMetricFamily cacheLoadSummary = new SummaryMetricFamily("caffeine_cache_load_duration_seconds",
132+
"Cache load duration: both success and failures", labelNames);
133+
mfs.add(cacheLoadSummary);
134+
135+
for(Map.Entry<String, Cache> c: children.entrySet()) {
136+
List<String> cacheName = Arrays.asList(c.getKey());
137+
CacheStats stats = c.getValue().stats();
138+
139+
try{
140+
cacheEvictionWeight.addMetric(cacheName, stats.evictionWeight());
141+
} catch (Exception e) {
142+
// EvictionWeight metric is unavailable, newer version of Caffeine is needed.
143+
}
144+
145+
cacheHitTotal.addMetric(cacheName, stats.hitCount());
146+
cacheMissTotal.addMetric(cacheName, stats.missCount());
147+
cacheRequestsTotal.addMetric(cacheName, stats.requestCount());
148+
cacheEvictionTotal.addMetric(cacheName, stats.evictionCount());
149+
150+
if(c.getValue() instanceof LoadingCache) {
151+
cacheLoadFailure.addMetric(cacheName, stats.loadFailureCount());
152+
cacheLoadTotal.addMetric(cacheName, stats.loadCount());
153+
154+
cacheLoadSummary.addMetric(cacheName, stats.loadCount(), stats.totalLoadTime() / Collector.NANOSECONDS_PER_SECOND);
155+
}
156+
}
157+
return mfs;
158+
}
159+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.prometheus.client.cache.caffeine;
2+
3+
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
4+
import com.github.benmanes.caffeine.cache.Cache;
5+
import com.github.benmanes.caffeine.cache.CacheLoader;
6+
import com.github.benmanes.caffeine.cache.Caffeine;
7+
import com.github.benmanes.caffeine.cache.LoadingCache;
8+
import io.prometheus.client.CollectorRegistry;
9+
import org.junit.Test;
10+
11+
import java.util.concurrent.CompletableFuture;
12+
import java.util.concurrent.Executor;
13+
14+
import static org.assertj.core.api.Java6Assertions.assertThat;
15+
import static org.mockito.Matchers.anyString;
16+
import static org.mockito.Mockito.mock;
17+
import static org.mockito.Mockito.when;
18+
19+
public class CacheMetricsCollectorTest {
20+
21+
@Test
22+
public void cacheExposesMetricsForHitMissAndEviction() throws Exception {
23+
Cache<String, String> cache = Caffeine.newBuilder().maximumSize(2).recordStats().executor(new Executor() {
24+
@Override
25+
public void execute(Runnable command) {
26+
// Run cleanup in same thread, to remove async behavior with evictions
27+
command.run();
28+
}
29+
}).build();
30+
CollectorRegistry registry = new CollectorRegistry();
31+
32+
CacheMetricsCollector collector = new CacheMetricsCollector().register(registry);
33+
collector.addCache("users", cache);
34+
35+
cache.getIfPresent("user1");
36+
cache.getIfPresent("user1");
37+
cache.put("user1", "First User");
38+
cache.getIfPresent("user1");
39+
40+
// Add to cache to trigger eviction.
41+
cache.put("user2", "Second User");
42+
cache.put("user3", "Third User");
43+
cache.put("user4", "Fourth User");
44+
45+
assertMetric(registry, "caffeine_cache_hit_total", "users", 1.0);
46+
assertMetric(registry, "caffeine_cache_miss_total", "users", 2.0);
47+
assertMetric(registry, "caffeine_cache_requests_total", "users", 3.0);
48+
assertMetric(registry, "caffeine_cache_eviction_total", "users", 2.0);
49+
}
50+
51+
52+
@SuppressWarnings("unchecked")
53+
@Test
54+
public void loadingCacheExposesMetricsForLoadsAndExceptions() throws Exception {
55+
CacheLoader<String, String> loader = mock(CacheLoader.class);
56+
when(loader.load(anyString()))
57+
.thenReturn("First User")
58+
.thenThrow(new RuntimeException("Seconds time fails"))
59+
.thenReturn("Third User");
60+
61+
LoadingCache<String, String> cache = Caffeine.newBuilder().recordStats().build(loader);
62+
CollectorRegistry registry = new CollectorRegistry();
63+
CacheMetricsCollector collector = new CacheMetricsCollector().register(registry);
64+
collector.addCache("loadingusers", cache);
65+
66+
cache.get("user1");
67+
cache.get("user1");
68+
try {
69+
cache.get("user2");
70+
} catch (Exception e) {
71+
// ignoring.
72+
}
73+
cache.get("user3");
74+
75+
assertMetric(registry, "caffeine_cache_hit_total", "loadingusers", 1.0);
76+
assertMetric(registry, "caffeine_cache_miss_total", "loadingusers", 3.0);
77+
78+
assertMetric(registry, "caffeine_cache_load_failure_total", "loadingusers", 1.0);
79+
assertMetric(registry, "caffeine_cache_loads_total", "loadingusers", 3.0);
80+
81+
assertMetric(registry, "caffeine_cache_load_duration_seconds_count", "loadingusers", 3.0);
82+
assertMetricGreatThan(registry, "caffeine_cache_load_duration_seconds_sum", "loadingusers", 0.0);
83+
}
84+
85+
private void assertMetric(CollectorRegistry registry, String name, String cacheName, double value) {
86+
assertThat(registry.getSampleValue(name, new String[]{"cache"}, new String[]{cacheName})).isEqualTo(value);
87+
}
88+
89+
90+
private void assertMetricGreatThan(CollectorRegistry registry, String name, String cacheName, double value) {
91+
assertThat(registry.getSampleValue(name, new String[]{"cache"}, new String[]{cacheName})).isGreaterThan(value);
92+
}
93+
94+
95+
}

simpleclient_guava/src/main/java/io/prometheus/client/guava/cache/CacheMetricsCollector.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
import com.google.common.cache.CacheStats;
55
import com.google.common.cache.LoadingCache;
66
import io.prometheus.client.Collector;
7-
import io.prometheus.client.Counter;
87
import io.prometheus.client.CounterMetricFamily;
9-
import io.prometheus.client.Gauge;
108
import io.prometheus.client.SummaryMetricFamily;
119

1210
import java.util.ArrayList;

0 commit comments

Comments
 (0)