001package io.prometheus.metrics.instrumentation.caffeine; 002 003import com.github.benmanes.caffeine.cache.AsyncCache; 004import com.github.benmanes.caffeine.cache.Cache; 005import com.github.benmanes.caffeine.cache.LoadingCache; 006import com.github.benmanes.caffeine.cache.stats.CacheStats; 007import io.prometheus.metrics.model.registry.MultiCollector; 008import io.prometheus.metrics.model.snapshots.CounterSnapshot; 009import io.prometheus.metrics.model.snapshots.GaugeSnapshot; 010import io.prometheus.metrics.model.snapshots.Labels; 011import io.prometheus.metrics.model.snapshots.MetricSnapshots; 012import io.prometheus.metrics.model.snapshots.SummarySnapshot; 013import java.util.Arrays; 014import java.util.Collections; 015import java.util.List; 016import java.util.Map; 017import java.util.concurrent.ConcurrentHashMap; 018import java.util.concurrent.ConcurrentMap; 019 020/** 021 * Collect metrics from Caffeine's com.github.benmanes.caffeine.cache.Cache. 022 * 023 * <p> 024 * 025 * <pre>{@code 026 * // Note that `recordStats()` is required to gather non-zero statistics 027 * Cache<String, String> cache = Caffeine.newBuilder().recordStats().build(); 028 * CacheMetricsCollector cacheMetrics = new CacheMetricsCollector(); 029 * PrometheusRegistry.defaultRegistry.register(cacheMetrics); 030 * cacheMetrics.addCache("mycache", cache); 031 * 032 * }</pre> 033 * 034 * Exposed metrics are labeled with the provided cache name. 035 * 036 * <p>With the example above, sample metric names would be: 037 * 038 * <pre> 039 * caffeine_cache_hit_total{cache="mycache"} 10.0 040 * caffeine_cache_miss_total{cache="mycache"} 3.0 041 * caffeine_cache_requests_total{cache="mycache"} 13.0 042 * caffeine_cache_eviction_total{cache="mycache"} 1.0 043 * caffeine_cache_estimated_size{cache="mycache"} 5.0 044 * </pre> 045 * 046 * Additionally, if the cache includes a loader, the following metrics would be provided: 047 * 048 * <pre> 049 * caffeine_cache_load_failure_total{cache="mycache"} 2.0 050 * caffeine_cache_loads_total{cache="mycache"} 7.0 051 * caffeine_cache_load_duration_seconds_count{cache="mycache"} 7.0 052 * caffeine_cache_load_duration_seconds_sum{cache="mycache"} 0.0034 053 * </pre> 054 */ 055public class CacheMetricsCollector implements MultiCollector { 056 private static final double NANOSECONDS_PER_SECOND = 1_000_000_000.0; 057 058 protected final ConcurrentMap<String, Cache<?, ?>> children = new ConcurrentHashMap<>(); 059 060 /** 061 * Add or replace the cache with the given name. 062 * 063 * <p>Any references any previous cache with this name is invalidated. 064 * 065 * @param cacheName The name of the cache, will be the metrics label value 066 * @param cache The cache being monitored 067 */ 068 public void addCache(String cacheName, Cache<?, ?> cache) { 069 children.put(cacheName, cache); 070 } 071 072 /** 073 * Add or replace the cache with the given name. 074 * 075 * <p>Any references any previous cache with this name is invalidated. 076 * 077 * @param cacheName The name of the cache, will be the metrics label value 078 * @param cache The cache being monitored 079 */ 080 public void addCache(String cacheName, AsyncCache<?, ?> cache) { 081 children.put(cacheName, cache.synchronous()); 082 } 083 084 /** 085 * Remove the cache with the given name. 086 * 087 * <p>Any references to the cache are invalidated. 088 * 089 * @param cacheName cache to be removed 090 */ 091 public Cache<?, ?> removeCache(String cacheName) { 092 return children.remove(cacheName); 093 } 094 095 /** 096 * Remove all caches. 097 * 098 * <p>Any references to all caches are invalidated. 099 */ 100 public void clear() { 101 children.clear(); 102 } 103 104 @Override 105 public MetricSnapshots collect() { 106 final MetricSnapshots.Builder metricSnapshotsBuilder = MetricSnapshots.builder(); 107 final List<String> labelNames = Arrays.asList("cache"); 108 109 final CounterSnapshot.Builder cacheHitTotal = 110 CounterSnapshot.builder().name("caffeine_cache_hit").help("Cache hit totals"); 111 112 final CounterSnapshot.Builder cacheMissTotal = 113 CounterSnapshot.builder().name("caffeine_cache_miss").help("Cache miss totals"); 114 115 final CounterSnapshot.Builder cacheRequestsTotal = 116 CounterSnapshot.builder() 117 .name("caffeine_cache_requests") 118 .help("Cache request totals, hits + misses"); 119 120 final CounterSnapshot.Builder cacheEvictionTotal = 121 CounterSnapshot.builder() 122 .name("caffeine_cache_eviction") 123 .help("Cache eviction totals, doesn't include manually removed entries"); 124 125 final GaugeSnapshot.Builder cacheEvictionWeight = 126 GaugeSnapshot.builder() 127 .name("caffeine_cache_eviction_weight") 128 .help("Cache eviction weight"); 129 130 final CounterSnapshot.Builder cacheLoadFailure = 131 CounterSnapshot.builder().name("caffeine_cache_load_failure").help("Cache load failures"); 132 133 final CounterSnapshot.Builder cacheLoadTotal = 134 CounterSnapshot.builder() 135 .name("caffeine_cache_loads") 136 .help("Cache loads: both success and failures"); 137 138 final GaugeSnapshot.Builder cacheSize = 139 GaugeSnapshot.builder().name("caffeine_cache_estimated_size").help("Estimated cache size"); 140 141 final SummarySnapshot.Builder cacheLoadSummary = 142 SummarySnapshot.builder() 143 .name("caffeine_cache_load_duration_seconds") 144 .help("Cache load duration: both success and failures"); 145 146 for (final Map.Entry<String, Cache<?, ?>> c : children.entrySet()) { 147 final List<String> cacheName = Collections.singletonList(c.getKey()); 148 final Labels labels = Labels.of(labelNames, cacheName); 149 150 final CacheStats stats = c.getValue().stats(); 151 152 try { 153 cacheEvictionWeight.dataPoint( 154 GaugeSnapshot.GaugeDataPointSnapshot.builder() 155 .labels(labels) 156 .value(stats.evictionWeight()) 157 .build()); 158 } catch (Exception e) { 159 // EvictionWeight metric is unavailable, newer version of Caffeine is needed. 160 } 161 162 cacheHitTotal.dataPoint( 163 CounterSnapshot.CounterDataPointSnapshot.builder() 164 .labels(labels) 165 .value(stats.hitCount()) 166 .build()); 167 168 cacheMissTotal.dataPoint( 169 CounterSnapshot.CounterDataPointSnapshot.builder() 170 .labels(labels) 171 .value(stats.missCount()) 172 .build()); 173 174 cacheRequestsTotal.dataPoint( 175 CounterSnapshot.CounterDataPointSnapshot.builder() 176 .labels(labels) 177 .value(stats.requestCount()) 178 .build()); 179 180 cacheEvictionTotal.dataPoint( 181 CounterSnapshot.CounterDataPointSnapshot.builder() 182 .labels(labels) 183 .value(stats.evictionCount()) 184 .build()); 185 186 cacheSize.dataPoint( 187 GaugeSnapshot.GaugeDataPointSnapshot.builder() 188 .labels(labels) 189 .value(c.getValue().estimatedSize()) 190 .build()); 191 192 if (c.getValue() instanceof LoadingCache) { 193 cacheLoadFailure.dataPoint( 194 CounterSnapshot.CounterDataPointSnapshot.builder() 195 .labels(labels) 196 .value(stats.loadFailureCount()) 197 .build()); 198 199 cacheLoadTotal.dataPoint( 200 CounterSnapshot.CounterDataPointSnapshot.builder() 201 .labels(labels) 202 .value(stats.loadCount()) 203 .build()); 204 205 cacheLoadSummary.dataPoint( 206 SummarySnapshot.SummaryDataPointSnapshot.builder() 207 .labels(labels) 208 .count(stats.loadCount()) 209 .sum(stats.totalLoadTime() / NANOSECONDS_PER_SECOND) 210 .build()); 211 } 212 } 213 214 return metricSnapshotsBuilder 215 .metricSnapshot(cacheHitTotal.build()) 216 .metricSnapshot(cacheMissTotal.build()) 217 .metricSnapshot(cacheRequestsTotal.build()) 218 .metricSnapshot(cacheEvictionTotal.build()) 219 .metricSnapshot(cacheEvictionWeight.build()) 220 .metricSnapshot(cacheLoadFailure.build()) 221 .metricSnapshot(cacheLoadTotal.build()) 222 .metricSnapshot(cacheSize.build()) 223 .metricSnapshot(cacheLoadSummary.build()) 224 .build(); 225 } 226}