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.Policy; 007import com.github.benmanes.caffeine.cache.stats.CacheStats; 008import io.prometheus.metrics.model.registry.MultiCollector; 009import io.prometheus.metrics.model.snapshots.CounterSnapshot; 010import io.prometheus.metrics.model.snapshots.GaugeSnapshot; 011import io.prometheus.metrics.model.snapshots.Labels; 012import io.prometheus.metrics.model.snapshots.MetricSnapshots; 013import io.prometheus.metrics.model.snapshots.SummarySnapshot; 014import java.util.Arrays; 015import java.util.Collections; 016import java.util.List; 017import java.util.Map; 018import java.util.Optional; 019import java.util.concurrent.ConcurrentHashMap; 020import java.util.concurrent.ConcurrentMap; 021import java.util.stream.Collectors; 022 023/** 024 * Collect metrics from Caffeine's com.github.benmanes.caffeine.cache.Cache. 025 * 026 * <p> 027 * 028 * <pre>{@code 029 * // Note that `recordStats()` is required to gather non-zero statistics 030 * Cache<String, String> cache = Caffeine.newBuilder().recordStats().build(); 031 * CacheMetricsCollector cacheMetrics = CacheMetricsCollector.builder().build(); 032 * PrometheusRegistry.defaultRegistry.register(cacheMetrics); 033 * cacheMetrics.addCache("mycache", cache); 034 * 035 * }</pre> 036 * 037 * Exposed metrics are labeled with the provided cache name. 038 * 039 * <p>With the example above, sample metric names would be: 040 * 041 * <pre> 042 * caffeine_cache_hit_total{cache="mycache"} 10.0 043 * caffeine_cache_miss_total{cache="mycache"} 3.0 044 * caffeine_cache_requests_total{cache="mycache"} 13.0 045 * caffeine_cache_eviction_total{cache="mycache"} 1.0 046 * caffeine_cache_estimated_size{cache="mycache"} 5.0 047 * </pre> 048 * 049 * Additionally, if the cache includes a loader, the following metrics would be provided: 050 * 051 * <pre> 052 * caffeine_cache_load_failure_total{cache="mycache"} 2.0 053 * caffeine_cache_loads_total{cache="mycache"} 7.0 054 * caffeine_cache_load_duration_seconds_count{cache="mycache"} 7.0 055 * caffeine_cache_load_duration_seconds_sum{cache="mycache"} 0.0034 056 * </pre> 057 */ 058public class CacheMetricsCollector implements MultiCollector { 059 private static final double NANOSECONDS_PER_SECOND = 1_000_000_000.0; 060 061 private static final String METRIC_NAME_CACHE_HIT = "caffeine_cache_hit"; 062 private static final String METRIC_NAME_CACHE_MISS = "caffeine_cache_miss"; 063 private static final String METRIC_NAME_CACHE_REQUESTS = "caffeine_cache_requests"; 064 private static final String METRIC_NAME_CACHE_EVICTION = "caffeine_cache_eviction"; 065 private static final String METRIC_NAME_CACHE_EVICTION_WEIGHT = "caffeine_cache_eviction_weight"; 066 private static final String METRIC_NAME_CACHE_LOAD_FAILURE = "caffeine_cache_load_failure"; 067 private static final String METRIC_NAME_CACHE_LOADS = "caffeine_cache_loads"; 068 private static final String METRIC_NAME_CACHE_ESTIMATED_SIZE = "caffeine_cache_estimated_size"; 069 private static final String METRIC_NAME_CACHE_WEIGHTED_SIZE = "caffeine_cache_weighted_size"; 070 private static final String METRIC_NAME_CACHE_LOAD_DURATION_SECONDS = 071 "caffeine_cache_load_duration_seconds"; 072 073 private static final List<String> ALL_METRIC_NAMES = 074 Collections.unmodifiableList( 075 Arrays.asList( 076 METRIC_NAME_CACHE_HIT, 077 METRIC_NAME_CACHE_MISS, 078 METRIC_NAME_CACHE_REQUESTS, 079 METRIC_NAME_CACHE_EVICTION, 080 METRIC_NAME_CACHE_EVICTION_WEIGHT, 081 METRIC_NAME_CACHE_LOAD_FAILURE, 082 METRIC_NAME_CACHE_LOADS, 083 METRIC_NAME_CACHE_ESTIMATED_SIZE, 084 METRIC_NAME_CACHE_WEIGHTED_SIZE, 085 METRIC_NAME_CACHE_LOAD_DURATION_SECONDS)); 086 087 protected final ConcurrentMap<String, Cache<?, ?>> children = new ConcurrentHashMap<>(); 088 private final boolean collectEvictionWeightAsCounter; 089 private final boolean collectWeightedSize; 090 091 /** 092 * Instantiates a {@link CacheMetricsCollector}, with the legacy parameters. 093 * 094 * <p>The use of this constructor is discouraged, in favor of a Builder pattern {@link #builder()} 095 * 096 * <p>Note that the {@link #builder()} API has different default values than this deprecated 097 * constructor. 098 */ 099 @Deprecated 100 public CacheMetricsCollector() { 101 this(false, false); 102 } 103 104 /** 105 * Instantiate a {@link CacheMetricsCollector} 106 * 107 * @param collectEvictionWeightAsCounter If true, {@code caffeine_cache_eviction_weight} will be 108 * observed as an incrementing counter instead of a gauge. 109 * @param collectWeightedSize If true, {@code caffeine_cache_weighted_size} will be observed. 110 */ 111 protected CacheMetricsCollector( 112 boolean collectEvictionWeightAsCounter, boolean collectWeightedSize) { 113 this.collectEvictionWeightAsCounter = collectEvictionWeightAsCounter; 114 this.collectWeightedSize = collectWeightedSize; 115 } 116 117 /** 118 * Add or replace the cache with the given name. 119 * 120 * <p>Any references any previous cache with this name is invalidated. 121 * 122 * @param cacheName The name of the cache, will be the metrics label value 123 * @param cache The cache being monitored 124 */ 125 public void addCache(String cacheName, Cache<?, ?> cache) { 126 children.put(cacheName, cache); 127 } 128 129 /** 130 * Add or replace the cache with the given name. 131 * 132 * <p>Any references any previous cache with this name is invalidated. 133 * 134 * @param cacheName The name of the cache, will be the metrics label value 135 * @param cache The cache being monitored 136 */ 137 public void addCache(String cacheName, AsyncCache<?, ?> cache) { 138 children.put(cacheName, cache.synchronous()); 139 } 140 141 /** 142 * Remove the cache with the given name. 143 * 144 * <p>Any references to the cache are invalidated. 145 * 146 * @param cacheName cache to be removed 147 */ 148 public Cache<?, ?> removeCache(String cacheName) { 149 return children.remove(cacheName); 150 } 151 152 /** 153 * Remove all caches. 154 * 155 * <p>Any references to all caches are invalidated. 156 */ 157 public void clear() { 158 children.clear(); 159 } 160 161 @Override 162 public MetricSnapshots collect() { 163 final MetricSnapshots.Builder metricSnapshotsBuilder = MetricSnapshots.builder(); 164 final List<String> labelNames = Arrays.asList("cache"); 165 166 final CounterSnapshot.Builder cacheHitTotal = 167 CounterSnapshot.builder().name(METRIC_NAME_CACHE_HIT).help("Cache hit totals"); 168 169 final CounterSnapshot.Builder cacheMissTotal = 170 CounterSnapshot.builder().name(METRIC_NAME_CACHE_MISS).help("Cache miss totals"); 171 172 final CounterSnapshot.Builder cacheRequestsTotal = 173 CounterSnapshot.builder() 174 .name(METRIC_NAME_CACHE_REQUESTS) 175 .help("Cache request totals, hits + misses"); 176 177 final CounterSnapshot.Builder cacheEvictionTotal = 178 CounterSnapshot.builder() 179 .name(METRIC_NAME_CACHE_EVICTION) 180 .help("Cache eviction totals, doesn't include manually removed entries"); 181 182 final CounterSnapshot.Builder cacheEvictionWeight = 183 CounterSnapshot.builder() 184 .name(METRIC_NAME_CACHE_EVICTION_WEIGHT) 185 .help("Weight of evicted cache entries, doesn't include manually removed entries"); 186 final GaugeSnapshot.Builder cacheEvictionWeightLegacyGauge = 187 GaugeSnapshot.builder() 188 .name(METRIC_NAME_CACHE_EVICTION_WEIGHT) 189 .help("Weight of evicted cache entries, doesn't include manually removed entries"); 190 191 final CounterSnapshot.Builder cacheLoadFailure = 192 CounterSnapshot.builder().name(METRIC_NAME_CACHE_LOAD_FAILURE).help("Cache load failures"); 193 194 final CounterSnapshot.Builder cacheLoadTotal = 195 CounterSnapshot.builder() 196 .name(METRIC_NAME_CACHE_LOADS) 197 .help("Cache loads: both success and failures"); 198 199 final GaugeSnapshot.Builder cacheSize = 200 GaugeSnapshot.builder().name(METRIC_NAME_CACHE_ESTIMATED_SIZE).help("Estimated cache size"); 201 202 final GaugeSnapshot.Builder cacheWeightedSize = 203 GaugeSnapshot.builder() 204 .name(METRIC_NAME_CACHE_WEIGHTED_SIZE) 205 .help("Approximate accumulated weight of cache entries"); 206 207 final SummarySnapshot.Builder cacheLoadSummary = 208 SummarySnapshot.builder() 209 .name(METRIC_NAME_CACHE_LOAD_DURATION_SECONDS) 210 .help("Cache load duration: both success and failures"); 211 212 for (final Map.Entry<String, Cache<?, ?>> c : children.entrySet()) { 213 final List<String> cacheName = Collections.singletonList(c.getKey()); 214 final Labels labels = Labels.of(labelNames, cacheName); 215 216 final CacheStats stats = c.getValue().stats(); 217 218 try { 219 cacheEvictionWeight.dataPoint( 220 CounterSnapshot.CounterDataPointSnapshot.builder() 221 .labels(labels) 222 .value(stats.evictionWeight()) 223 .build()); 224 cacheEvictionWeightLegacyGauge.dataPoint( 225 GaugeSnapshot.GaugeDataPointSnapshot.builder() 226 .labels(labels) 227 .value(stats.evictionWeight()) 228 .build()); 229 } catch (Exception e) { 230 // EvictionWeight metric is unavailable, newer version of Caffeine is needed. 231 } 232 233 if (collectWeightedSize) { 234 final Optional<? extends Policy.Eviction<?, ?>> eviction = c.getValue().policy().eviction(); 235 if (eviction.isPresent() && eviction.get().weightedSize().isPresent()) { 236 cacheWeightedSize.dataPoint( 237 GaugeSnapshot.GaugeDataPointSnapshot.builder() 238 .labels(labels) 239 .value(eviction.get().weightedSize().getAsLong()) 240 .build()); 241 } 242 } 243 244 cacheHitTotal.dataPoint( 245 CounterSnapshot.CounterDataPointSnapshot.builder() 246 .labels(labels) 247 .value(stats.hitCount()) 248 .build()); 249 250 cacheMissTotal.dataPoint( 251 CounterSnapshot.CounterDataPointSnapshot.builder() 252 .labels(labels) 253 .value(stats.missCount()) 254 .build()); 255 256 cacheRequestsTotal.dataPoint( 257 CounterSnapshot.CounterDataPointSnapshot.builder() 258 .labels(labels) 259 .value(stats.requestCount()) 260 .build()); 261 262 cacheEvictionTotal.dataPoint( 263 CounterSnapshot.CounterDataPointSnapshot.builder() 264 .labels(labels) 265 .value(stats.evictionCount()) 266 .build()); 267 268 cacheSize.dataPoint( 269 GaugeSnapshot.GaugeDataPointSnapshot.builder() 270 .labels(labels) 271 .value(c.getValue().estimatedSize()) 272 .build()); 273 274 if (c.getValue() instanceof LoadingCache) { 275 cacheLoadFailure.dataPoint( 276 CounterSnapshot.CounterDataPointSnapshot.builder() 277 .labels(labels) 278 .value(stats.loadFailureCount()) 279 .build()); 280 281 cacheLoadTotal.dataPoint( 282 CounterSnapshot.CounterDataPointSnapshot.builder() 283 .labels(labels) 284 .value(stats.loadCount()) 285 .build()); 286 287 cacheLoadSummary.dataPoint( 288 SummarySnapshot.SummaryDataPointSnapshot.builder() 289 .labels(labels) 290 .count(stats.loadCount()) 291 .sum(stats.totalLoadTime() / NANOSECONDS_PER_SECOND) 292 .build()); 293 } 294 } 295 296 if (collectWeightedSize) { 297 metricSnapshotsBuilder.metricSnapshot(cacheWeightedSize.build()); 298 } 299 300 return metricSnapshotsBuilder 301 .metricSnapshot(cacheHitTotal.build()) 302 .metricSnapshot(cacheMissTotal.build()) 303 .metricSnapshot(cacheRequestsTotal.build()) 304 .metricSnapshot(cacheEvictionTotal.build()) 305 .metricSnapshot( 306 collectEvictionWeightAsCounter 307 ? cacheEvictionWeight.build() 308 : cacheEvictionWeightLegacyGauge.build()) 309 .metricSnapshot(cacheLoadFailure.build()) 310 .metricSnapshot(cacheLoadTotal.build()) 311 .metricSnapshot(cacheSize.build()) 312 .metricSnapshot(cacheLoadSummary.build()) 313 .build(); 314 } 315 316 @Override 317 public List<String> getPrometheusNames() { 318 if (!collectWeightedSize) { 319 return ALL_METRIC_NAMES.stream() 320 .filter(s -> !METRIC_NAME_CACHE_WEIGHTED_SIZE.equals(s)) 321 .collect(Collectors.toList()); 322 } 323 return ALL_METRIC_NAMES; 324 } 325 326 public static Builder builder() { 327 return new Builder(); 328 } 329 330 public static class Builder { 331 332 private boolean collectEvictionWeightAsCounter = true; 333 private boolean collectWeightedSize = true; 334 335 public Builder collectEvictionWeightAsCounter(boolean collectEvictionWeightAsCounter) { 336 this.collectEvictionWeightAsCounter = collectEvictionWeightAsCounter; 337 return this; 338 } 339 340 public Builder collectWeightedSize(boolean collectWeightedSize) { 341 this.collectWeightedSize = collectWeightedSize; 342 return this; 343 } 344 345 public CacheMetricsCollector build() { 346 return new CacheMetricsCollector(collectEvictionWeightAsCounter, collectWeightedSize); 347 } 348 } 349}