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