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}