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  private static final String METRIC_NAME_CACHE_HIT = "caffeine_cache_hit";
059  private static final String METRIC_NAME_CACHE_MISS = "caffeine_cache_miss";
060  private static final String METRIC_NAME_CACHE_REQUESTS = "caffeine_cache_requests";
061  private static final String METRIC_NAME_CACHE_EVICTION = "caffeine_cache_eviction";
062  private static final String METRIC_NAME_CACHE_EVICTION_WEIGHT = "caffeine_cache_eviction_weight";
063  private static final String METRIC_NAME_CACHE_LOAD_FAILURE = "caffeine_cache_load_failure";
064  private static final String METRIC_NAME_CACHE_LOADS = "caffeine_cache_loads";
065  private static final String METRIC_NAME_CACHE_ESTIMATED_SIZE = "caffeine_cache_estimated_size";
066  private static final String METRIC_NAME_CACHE_LOAD_DURATION_SECONDS =
067      "caffeine_cache_load_duration_seconds";
068
069  private static final List<String> ALL_METRIC_NAMES =
070      Collections.unmodifiableList(
071          Arrays.asList(
072              METRIC_NAME_CACHE_HIT,
073              METRIC_NAME_CACHE_MISS,
074              METRIC_NAME_CACHE_REQUESTS,
075              METRIC_NAME_CACHE_EVICTION,
076              METRIC_NAME_CACHE_EVICTION_WEIGHT,
077              METRIC_NAME_CACHE_LOAD_FAILURE,
078              METRIC_NAME_CACHE_LOADS,
079              METRIC_NAME_CACHE_ESTIMATED_SIZE,
080              METRIC_NAME_CACHE_LOAD_DURATION_SECONDS));
081
082  protected final ConcurrentMap<String, Cache<?, ?>> children = new ConcurrentHashMap<>();
083
084  /**
085   * Add or replace the cache with the given name.
086   *
087   * <p>Any references any previous cache with this name is invalidated.
088   *
089   * @param cacheName The name of the cache, will be the metrics label value
090   * @param cache The cache being monitored
091   */
092  public void addCache(String cacheName, Cache<?, ?> cache) {
093    children.put(cacheName, cache);
094  }
095
096  /**
097   * Add or replace the cache with the given name.
098   *
099   * <p>Any references any previous cache with this name is invalidated.
100   *
101   * @param cacheName The name of the cache, will be the metrics label value
102   * @param cache The cache being monitored
103   */
104  public void addCache(String cacheName, AsyncCache<?, ?> cache) {
105    children.put(cacheName, cache.synchronous());
106  }
107
108  /**
109   * Remove the cache with the given name.
110   *
111   * <p>Any references to the cache are invalidated.
112   *
113   * @param cacheName cache to be removed
114   */
115  public Cache<?, ?> removeCache(String cacheName) {
116    return children.remove(cacheName);
117  }
118
119  /**
120   * Remove all caches.
121   *
122   * <p>Any references to all caches are invalidated.
123   */
124  public void clear() {
125    children.clear();
126  }
127
128  @Override
129  public MetricSnapshots collect() {
130    final MetricSnapshots.Builder metricSnapshotsBuilder = MetricSnapshots.builder();
131    final List<String> labelNames = Arrays.asList("cache");
132
133    final CounterSnapshot.Builder cacheHitTotal =
134        CounterSnapshot.builder().name(METRIC_NAME_CACHE_HIT).help("Cache hit totals");
135
136    final CounterSnapshot.Builder cacheMissTotal =
137        CounterSnapshot.builder().name(METRIC_NAME_CACHE_MISS).help("Cache miss totals");
138
139    final CounterSnapshot.Builder cacheRequestsTotal =
140        CounterSnapshot.builder()
141            .name(METRIC_NAME_CACHE_REQUESTS)
142            .help("Cache request totals, hits + misses");
143
144    final CounterSnapshot.Builder cacheEvictionTotal =
145        CounterSnapshot.builder()
146            .name(METRIC_NAME_CACHE_EVICTION)
147            .help("Cache eviction totals, doesn't include manually removed entries");
148
149    final GaugeSnapshot.Builder cacheEvictionWeight =
150        GaugeSnapshot.builder()
151            .name(METRIC_NAME_CACHE_EVICTION_WEIGHT)
152            .help("Cache eviction weight");
153
154    final CounterSnapshot.Builder cacheLoadFailure =
155        CounterSnapshot.builder().name(METRIC_NAME_CACHE_LOAD_FAILURE).help("Cache load failures");
156
157    final CounterSnapshot.Builder cacheLoadTotal =
158        CounterSnapshot.builder()
159            .name(METRIC_NAME_CACHE_LOADS)
160            .help("Cache loads: both success and failures");
161
162    final GaugeSnapshot.Builder cacheSize =
163        GaugeSnapshot.builder().name(METRIC_NAME_CACHE_ESTIMATED_SIZE).help("Estimated cache size");
164
165    final SummarySnapshot.Builder cacheLoadSummary =
166        SummarySnapshot.builder()
167            .name(METRIC_NAME_CACHE_LOAD_DURATION_SECONDS)
168            .help("Cache load duration: both success and failures");
169
170    for (final Map.Entry<String, Cache<?, ?>> c : children.entrySet()) {
171      final List<String> cacheName = Collections.singletonList(c.getKey());
172      final Labels labels = Labels.of(labelNames, cacheName);
173
174      final CacheStats stats = c.getValue().stats();
175
176      try {
177        cacheEvictionWeight.dataPoint(
178            GaugeSnapshot.GaugeDataPointSnapshot.builder()
179                .labels(labels)
180                .value(stats.evictionWeight())
181                .build());
182      } catch (Exception e) {
183        // EvictionWeight metric is unavailable, newer version of Caffeine is needed.
184      }
185
186      cacheHitTotal.dataPoint(
187          CounterSnapshot.CounterDataPointSnapshot.builder()
188              .labels(labels)
189              .value(stats.hitCount())
190              .build());
191
192      cacheMissTotal.dataPoint(
193          CounterSnapshot.CounterDataPointSnapshot.builder()
194              .labels(labels)
195              .value(stats.missCount())
196              .build());
197
198      cacheRequestsTotal.dataPoint(
199          CounterSnapshot.CounterDataPointSnapshot.builder()
200              .labels(labels)
201              .value(stats.requestCount())
202              .build());
203
204      cacheEvictionTotal.dataPoint(
205          CounterSnapshot.CounterDataPointSnapshot.builder()
206              .labels(labels)
207              .value(stats.evictionCount())
208              .build());
209
210      cacheSize.dataPoint(
211          GaugeSnapshot.GaugeDataPointSnapshot.builder()
212              .labels(labels)
213              .value(c.getValue().estimatedSize())
214              .build());
215
216      if (c.getValue() instanceof LoadingCache) {
217        cacheLoadFailure.dataPoint(
218            CounterSnapshot.CounterDataPointSnapshot.builder()
219                .labels(labels)
220                .value(stats.loadFailureCount())
221                .build());
222
223        cacheLoadTotal.dataPoint(
224            CounterSnapshot.CounterDataPointSnapshot.builder()
225                .labels(labels)
226                .value(stats.loadCount())
227                .build());
228
229        cacheLoadSummary.dataPoint(
230            SummarySnapshot.SummaryDataPointSnapshot.builder()
231                .labels(labels)
232                .count(stats.loadCount())
233                .sum(stats.totalLoadTime() / NANOSECONDS_PER_SECOND)
234                .build());
235      }
236    }
237
238    return metricSnapshotsBuilder
239        .metricSnapshot(cacheHitTotal.build())
240        .metricSnapshot(cacheMissTotal.build())
241        .metricSnapshot(cacheRequestsTotal.build())
242        .metricSnapshot(cacheEvictionTotal.build())
243        .metricSnapshot(cacheEvictionWeight.build())
244        .metricSnapshot(cacheLoadFailure.build())
245        .metricSnapshot(cacheLoadTotal.build())
246        .metricSnapshot(cacheSize.build())
247        .metricSnapshot(cacheLoadSummary.build())
248        .build();
249  }
250
251  @Override
252  public List<String> getPrometheusNames() {
253    return ALL_METRIC_NAMES;
254  }
255}