001package io.prometheus.metrics.instrumentation.guava;
002
003import com.google.common.cache.Cache;
004import com.google.common.cache.CacheStats;
005import com.google.common.cache.LoadingCache;
006import io.prometheus.metrics.model.registry.MultiCollector;
007import io.prometheus.metrics.model.snapshots.CounterSnapshot;
008import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
009import io.prometheus.metrics.model.snapshots.Labels;
010import io.prometheus.metrics.model.snapshots.MetricSnapshots;
011import io.prometheus.metrics.model.snapshots.SummarySnapshot;
012import java.util.Collections;
013import java.util.List;
014import java.util.Map;
015import java.util.concurrent.ConcurrentHashMap;
016import java.util.concurrent.ConcurrentMap;
017
018/**
019 * Collect metrics from Guava's com.google.common.cache.Cache.
020 *
021 * <p>
022 *
023 * <pre>{@code
024 * // Note that `recordStats()` is required to gather non-zero statistics
025 * Cache<String, String> cache = CacheBuilder.newBuilder().recordStats().build();
026 * CacheMetricsCollector cacheMetrics = new CacheMetricsCollector();
027 * PrometheusRegistry.defaultRegistry.register(cacheMetrics);
028 * cacheMetrics.addCache("mycache", cache);
029 *
030 * }</pre>
031 *
032 * Exposed metrics are labeled with the provided cache name.
033 *
034 * <p>With the example above, sample metric names would be:
035 *
036 * <pre>
037 *     guava_cache_hit_total{cache="mycache"} 10.0
038 *     guava_cache_miss_total{cache="mycache"} 3.0
039 *     guava_cache_requests_total{cache="mycache"} 13.0
040 *     guava_cache_eviction_total{cache="mycache"} 1.0
041 *     guava_cache_size{cache="mycache"} 5.0
042 * </pre>
043 *
044 * Additionally, if the cache includes a loader, the following metrics would be provided:
045 *
046 * <pre>
047 *     guava_cache_load_failure_total{cache="mycache"} 2.0
048 *     guava_cache_loads_total{cache="mycache"} 7.0
049 *     guava_cache_load_duration_seconds_count{cache="mycache"} 7.0
050 *     guava_cache_load_duration_seconds_sum{cache="mycache"} 0.0034
051 * </pre>
052 */
053public class CacheMetricsCollector implements MultiCollector {
054
055  private static final double NANOSECONDS_PER_SECOND = 1_000_000_000.0;
056
057  protected final ConcurrentMap<String, Cache<?, ?>> children = new ConcurrentHashMap<>();
058
059  /**
060   * Add or replace the cache with the given name.
061   *
062   * <p>Any references any previous cache with this name is invalidated.
063   *
064   * @param cacheName The name of the cache, will be the metrics label value
065   * @param cache The cache being monitored
066   */
067  public void addCache(String cacheName, Cache<?, ?> cache) {
068    children.put(cacheName, cache);
069  }
070
071  /**
072   * Remove the cache with the given name.
073   *
074   * <p>Any references to the cache are invalidated.
075   *
076   * @param cacheName cache to be removed
077   */
078  public Cache<?, ?> removeCache(String cacheName) {
079    return children.remove(cacheName);
080  }
081
082  /**
083   * Remove all caches.
084   *
085   * <p>Any references to all caches are invalidated.
086   */
087  public void clear() {
088    children.clear();
089  }
090
091  @Override
092  public MetricSnapshots collect() {
093    final MetricSnapshots.Builder metricSnapshotsBuilder = MetricSnapshots.builder();
094    final List<String> labelNames = Collections.singletonList("cache");
095
096    final CounterSnapshot.Builder cacheHitTotal =
097        CounterSnapshot.builder().name("guava_cache_hit").help("Cache hit totals");
098
099    final CounterSnapshot.Builder cacheMissTotal =
100        CounterSnapshot.builder().name("guava_cache_miss").help("Cache miss totals");
101
102    final CounterSnapshot.Builder cacheRequestsTotal =
103        CounterSnapshot.builder().name("guava_cache_requests").help("Cache request totals");
104
105    final CounterSnapshot.Builder cacheEvictionTotal =
106        CounterSnapshot.builder()
107            .name("guava_cache_eviction")
108            .help("Cache eviction totals, doesn't include manually removed entries");
109
110    final CounterSnapshot.Builder cacheLoadFailure =
111        CounterSnapshot.builder().name("guava_cache_load_failure").help("Cache load failures");
112
113    final CounterSnapshot.Builder cacheLoadTotal =
114        CounterSnapshot.builder()
115            .name("guava_cache_loads")
116            .help("Cache loads: both success and failures");
117
118    final GaugeSnapshot.Builder cacheSize =
119        GaugeSnapshot.builder().name("guava_cache_size").help("Cache size");
120
121    final SummarySnapshot.Builder cacheLoadSummary =
122        SummarySnapshot.builder()
123            .name("guava_cache_load_duration_seconds")
124            .help("Cache load duration: both success and failures");
125
126    for (final Map.Entry<String, Cache<?, ?>> c : children.entrySet()) {
127      final List<String> cacheName = Collections.singletonList(c.getKey());
128      final Labels labels = Labels.of(labelNames, cacheName);
129
130      final CacheStats stats = c.getValue().stats();
131
132      cacheHitTotal.dataPoint(
133          CounterSnapshot.CounterDataPointSnapshot.builder()
134              .labels(labels)
135              .value(stats.hitCount())
136              .build());
137
138      cacheMissTotal.dataPoint(
139          CounterSnapshot.CounterDataPointSnapshot.builder()
140              .labels(labels)
141              .value(stats.missCount())
142              .build());
143
144      cacheRequestsTotal.dataPoint(
145          CounterSnapshot.CounterDataPointSnapshot.builder()
146              .labels(labels)
147              .value(stats.requestCount())
148              .build());
149
150      cacheEvictionTotal.dataPoint(
151          CounterSnapshot.CounterDataPointSnapshot.builder()
152              .labels(labels)
153              .value(stats.evictionCount())
154              .build());
155
156      cacheSize.dataPoint(
157          GaugeSnapshot.GaugeDataPointSnapshot.builder()
158              .labels(labels)
159              .value(c.getValue().size())
160              .build());
161
162      if (c.getValue() instanceof LoadingCache) {
163        cacheLoadFailure.dataPoint(
164            CounterSnapshot.CounterDataPointSnapshot.builder()
165                .labels(labels)
166                .value(stats.loadExceptionCount())
167                .build());
168
169        cacheLoadTotal.dataPoint(
170            CounterSnapshot.CounterDataPointSnapshot.builder()
171                .labels(labels)
172                .value(stats.loadCount())
173                .build());
174
175        cacheLoadSummary.dataPoint(
176            SummarySnapshot.SummaryDataPointSnapshot.builder()
177                .labels(labels)
178                .count(stats.loadCount())
179                .sum(stats.totalLoadTime() / NANOSECONDS_PER_SECOND)
180                .build());
181      }
182    }
183
184    metricSnapshotsBuilder.metricSnapshot(cacheHitTotal.build());
185    metricSnapshotsBuilder.metricSnapshot(cacheMissTotal.build());
186    metricSnapshotsBuilder.metricSnapshot(cacheRequestsTotal.build());
187    metricSnapshotsBuilder.metricSnapshot(cacheEvictionTotal.build());
188    metricSnapshotsBuilder.metricSnapshot(cacheLoadFailure.build());
189    metricSnapshotsBuilder.metricSnapshot(cacheLoadTotal.build());
190    metricSnapshotsBuilder.metricSnapshot(cacheSize.build());
191    metricSnapshotsBuilder.metricSnapshot(cacheLoadSummary.build());
192
193    return metricSnapshotsBuilder.build();
194  }
195}