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}