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