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.stats.CacheStats;
007import io.prometheus.metrics.model.registry.MultiCollector;
008import io.prometheus.metrics.model.snapshots.CounterSnapshot;
009import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
010import io.prometheus.metrics.model.snapshots.Labels;
011import io.prometheus.metrics.model.snapshots.MetricSnapshots;
012import io.prometheus.metrics.model.snapshots.SummarySnapshot;
013import java.util.Arrays;
014import java.util.Collections;
015import java.util.List;
016import java.util.Map;
017import java.util.concurrent.ConcurrentHashMap;
018import java.util.concurrent.ConcurrentMap;
019
020/**
021 * Collect metrics from Caffeine's com.github.benmanes.caffeine.cache.Cache.
022 *
023 * <p>
024 *
025 * <pre>{@code
026 * // Note that `recordStats()` is required to gather non-zero statistics
027 * Cache<String, String> cache = Caffeine.newBuilder().recordStats().build();
028 * CacheMetricsCollector cacheMetrics = new CacheMetricsCollector();
029 * PrometheusRegistry.defaultRegistry.register(cacheMetrics);
030 * cacheMetrics.addCache("mycache", cache);
031 *
032 * }</pre>
033 *
034 * Exposed metrics are labeled with the provided cache name.
035 *
036 * <p>With the example above, sample metric names would be:
037 *
038 * <pre>
039 *     caffeine_cache_hit_total{cache="mycache"} 10.0
040 *     caffeine_cache_miss_total{cache="mycache"} 3.0
041 *     caffeine_cache_requests_total{cache="mycache"} 13.0
042 *     caffeine_cache_eviction_total{cache="mycache"} 1.0
043 *     caffeine_cache_estimated_size{cache="mycache"} 5.0
044 * </pre>
045 *
046 * Additionally, if the cache includes a loader, the following metrics would be provided:
047 *
048 * <pre>
049 *     caffeine_cache_load_failure_total{cache="mycache"} 2.0
050 *     caffeine_cache_loads_total{cache="mycache"} 7.0
051 *     caffeine_cache_load_duration_seconds_count{cache="mycache"} 7.0
052 *     caffeine_cache_load_duration_seconds_sum{cache="mycache"} 0.0034
053 * </pre>
054 */
055public class CacheMetricsCollector implements MultiCollector {
056  private static final double NANOSECONDS_PER_SECOND = 1_000_000_000.0;
057
058  protected final ConcurrentMap<String, Cache<?, ?>> children = new ConcurrentHashMap<>();
059
060  /**
061   * Add or replace the cache with the given name.
062   *
063   * <p>Any references any previous cache with this name is invalidated.
064   *
065   * @param cacheName The name of the cache, will be the metrics label value
066   * @param cache The cache being monitored
067   */
068  public void addCache(String cacheName, Cache<?, ?> cache) {
069    children.put(cacheName, cache);
070  }
071
072  /**
073   * Add or replace the cache with the given name.
074   *
075   * <p>Any references any previous cache with this name is invalidated.
076   *
077   * @param cacheName The name of the cache, will be the metrics label value
078   * @param cache The cache being monitored
079   */
080  public void addCache(String cacheName, AsyncCache<?, ?> cache) {
081    children.put(cacheName, cache.synchronous());
082  }
083
084  /**
085   * Remove the cache with the given name.
086   *
087   * <p>Any references to the cache are invalidated.
088   *
089   * @param cacheName cache to be removed
090   */
091  public Cache<?, ?> removeCache(String cacheName) {
092    return children.remove(cacheName);
093  }
094
095  /**
096   * Remove all caches.
097   *
098   * <p>Any references to all caches are invalidated.
099   */
100  public void clear() {
101    children.clear();
102  }
103
104  @Override
105  public MetricSnapshots collect() {
106    final MetricSnapshots.Builder metricSnapshotsBuilder = MetricSnapshots.builder();
107    final List<String> labelNames = Arrays.asList("cache");
108
109    final CounterSnapshot.Builder cacheHitTotal =
110        CounterSnapshot.builder().name("caffeine_cache_hit").help("Cache hit totals");
111
112    final CounterSnapshot.Builder cacheMissTotal =
113        CounterSnapshot.builder().name("caffeine_cache_miss").help("Cache miss totals");
114
115    final CounterSnapshot.Builder cacheRequestsTotal =
116        CounterSnapshot.builder()
117            .name("caffeine_cache_requests")
118            .help("Cache request totals, hits + misses");
119
120    final CounterSnapshot.Builder cacheEvictionTotal =
121        CounterSnapshot.builder()
122            .name("caffeine_cache_eviction")
123            .help("Cache eviction totals, doesn't include manually removed entries");
124
125    final GaugeSnapshot.Builder cacheEvictionWeight =
126        GaugeSnapshot.builder()
127            .name("caffeine_cache_eviction_weight")
128            .help("Cache eviction weight");
129
130    final CounterSnapshot.Builder cacheLoadFailure =
131        CounterSnapshot.builder().name("caffeine_cache_load_failure").help("Cache load failures");
132
133    final CounterSnapshot.Builder cacheLoadTotal =
134        CounterSnapshot.builder()
135            .name("caffeine_cache_loads")
136            .help("Cache loads: both success and failures");
137
138    final GaugeSnapshot.Builder cacheSize =
139        GaugeSnapshot.builder().name("caffeine_cache_estimated_size").help("Estimated cache size");
140
141    final SummarySnapshot.Builder cacheLoadSummary =
142        SummarySnapshot.builder()
143            .name("caffeine_cache_load_duration_seconds")
144            .help("Cache load duration: both success and failures");
145
146    for (final Map.Entry<String, Cache<?, ?>> c : children.entrySet()) {
147      final List<String> cacheName = Collections.singletonList(c.getKey());
148      final Labels labels = Labels.of(labelNames, cacheName);
149
150      final CacheStats stats = c.getValue().stats();
151
152      try {
153        cacheEvictionWeight.dataPoint(
154            GaugeSnapshot.GaugeDataPointSnapshot.builder()
155                .labels(labels)
156                .value(stats.evictionWeight())
157                .build());
158      } catch (Exception e) {
159        // EvictionWeight metric is unavailable, newer version of Caffeine is needed.
160      }
161
162      cacheHitTotal.dataPoint(
163          CounterSnapshot.CounterDataPointSnapshot.builder()
164              .labels(labels)
165              .value(stats.hitCount())
166              .build());
167
168      cacheMissTotal.dataPoint(
169          CounterSnapshot.CounterDataPointSnapshot.builder()
170              .labels(labels)
171              .value(stats.missCount())
172              .build());
173
174      cacheRequestsTotal.dataPoint(
175          CounterSnapshot.CounterDataPointSnapshot.builder()
176              .labels(labels)
177              .value(stats.requestCount())
178              .build());
179
180      cacheEvictionTotal.dataPoint(
181          CounterSnapshot.CounterDataPointSnapshot.builder()
182              .labels(labels)
183              .value(stats.evictionCount())
184              .build());
185
186      cacheSize.dataPoint(
187          GaugeSnapshot.GaugeDataPointSnapshot.builder()
188              .labels(labels)
189              .value(c.getValue().estimatedSize())
190              .build());
191
192      if (c.getValue() instanceof LoadingCache) {
193        cacheLoadFailure.dataPoint(
194            CounterSnapshot.CounterDataPointSnapshot.builder()
195                .labels(labels)
196                .value(stats.loadFailureCount())
197                .build());
198
199        cacheLoadTotal.dataPoint(
200            CounterSnapshot.CounterDataPointSnapshot.builder()
201                .labels(labels)
202                .value(stats.loadCount())
203                .build());
204
205        cacheLoadSummary.dataPoint(
206            SummarySnapshot.SummaryDataPointSnapshot.builder()
207                .labels(labels)
208                .count(stats.loadCount())
209                .sum(stats.totalLoadTime() / NANOSECONDS_PER_SECOND)
210                .build());
211      }
212    }
213
214    return metricSnapshotsBuilder
215        .metricSnapshot(cacheHitTotal.build())
216        .metricSnapshot(cacheMissTotal.build())
217        .metricSnapshot(cacheRequestsTotal.build())
218        .metricSnapshot(cacheEvictionTotal.build())
219        .metricSnapshot(cacheEvictionWeight.build())
220        .metricSnapshot(cacheLoadFailure.build())
221        .metricSnapshot(cacheLoadTotal.build())
222        .metricSnapshot(cacheSize.build())
223        .metricSnapshot(cacheLoadSummary.build())
224        .build();
225  }
226}