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