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