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}