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.annotations.StableApi;
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 Guava's com.google.common.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 = CacheBuilder.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 *     guava_cache_hit_total{cache="mycache"} 10.0
040 *     guava_cache_miss_total{cache="mycache"} 3.0
041 *     guava_cache_requests_total{cache="mycache"} 13.0
042 *     guava_cache_eviction_total{cache="mycache"} 1.0
043 *     guava_cache_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 *     guava_cache_load_failure_total{cache="mycache"} 2.0
050 *     guava_cache_loads_total{cache="mycache"} 7.0
051 *     guava_cache_load_duration_seconds_count{cache="mycache"} 7.0
052 *     guava_cache_load_duration_seconds_sum{cache="mycache"} 0.0034
053 * </pre>
054 */
055@StableApi
056public class CacheMetricsCollector implements MultiCollector {
057
058  private static final double NANOSECONDS_PER_SECOND = 1_000_000_000.0;
059
060  private static final String METRIC_NAME_CACHE_HIT = "guava_cache_hit";
061  private static final String METRIC_NAME_CACHE_MISS = "guava_cache_miss";
062  private static final String METRIC_NAME_CACHE_REQUESTS = "guava_cache_requests";
063  private static final String METRIC_NAME_CACHE_EVICTION = "guava_cache_eviction";
064  private static final String METRIC_NAME_CACHE_LOAD_FAILURE = "guava_cache_load_failure";
065  private static final String METRIC_NAME_CACHE_LOADS = "guava_cache_loads";
066  private static final String METRIC_NAME_CACHE_SIZE = "guava_cache_size";
067  private static final String METRIC_NAME_CACHE_LOAD_DURATION_SECONDS =
068      "guava_cache_load_duration_seconds";
069
070  private static final List<String> ALL_METRIC_NAMES =
071      Collections.unmodifiableList(
072          Arrays.asList(
073              METRIC_NAME_CACHE_HIT,
074              METRIC_NAME_CACHE_MISS,
075              METRIC_NAME_CACHE_REQUESTS,
076              METRIC_NAME_CACHE_EVICTION,
077              METRIC_NAME_CACHE_LOAD_FAILURE,
078              METRIC_NAME_CACHE_LOADS,
079              METRIC_NAME_CACHE_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   * Remove the cache with the given name.
098   *
099   * <p>Any references to the cache are invalidated.
100   *
101   * @param cacheName cache to be removed
102   */
103  public Cache<?, ?> removeCache(String cacheName) {
104    return children.remove(cacheName);
105  }
106
107  /**
108   * Remove all caches.
109   *
110   * <p>Any references to all caches are invalidated.
111   */
112  public void clear() {
113    children.clear();
114  }
115
116  @Override
117  public MetricSnapshots collect() {
118    final MetricSnapshots.Builder metricSnapshotsBuilder = MetricSnapshots.builder();
119    final List<String> labelNames = Collections.singletonList("cache");
120
121    final CounterSnapshot.Builder cacheHitTotal =
122        CounterSnapshot.builder().name(METRIC_NAME_CACHE_HIT).help("Cache hit totals");
123
124    final CounterSnapshot.Builder cacheMissTotal =
125        CounterSnapshot.builder().name(METRIC_NAME_CACHE_MISS).help("Cache miss totals");
126
127    final CounterSnapshot.Builder cacheRequestsTotal =
128        CounterSnapshot.builder().name(METRIC_NAME_CACHE_REQUESTS).help("Cache request totals");
129
130    final CounterSnapshot.Builder cacheEvictionTotal =
131        CounterSnapshot.builder()
132            .name(METRIC_NAME_CACHE_EVICTION)
133            .help("Cache eviction totals, doesn't include manually removed entries");
134
135    final CounterSnapshot.Builder cacheLoadFailure =
136        CounterSnapshot.builder().name(METRIC_NAME_CACHE_LOAD_FAILURE).help("Cache load failures");
137
138    final CounterSnapshot.Builder cacheLoadTotal =
139        CounterSnapshot.builder()
140            .name(METRIC_NAME_CACHE_LOADS)
141            .help("Cache loads: both success and failures");
142
143    final GaugeSnapshot.Builder cacheSize =
144        GaugeSnapshot.builder().name(METRIC_NAME_CACHE_SIZE).help("Cache size");
145
146    final SummarySnapshot.Builder cacheLoadSummary =
147        SummarySnapshot.builder()
148            .name(METRIC_NAME_CACHE_LOAD_DURATION_SECONDS)
149            .help("Cache load duration: both success and failures");
150
151    for (final Map.Entry<String, Cache<?, ?>> c : children.entrySet()) {
152      final List<String> cacheName = Collections.singletonList(c.getKey());
153      final Labels labels = Labels.of(labelNames, cacheName);
154
155      final CacheStats stats = c.getValue().stats();
156
157      cacheHitTotal.dataPoint(
158          CounterSnapshot.CounterDataPointSnapshot.builder()
159              .labels(labels)
160              .value(stats.hitCount())
161              .build());
162
163      cacheMissTotal.dataPoint(
164          CounterSnapshot.CounterDataPointSnapshot.builder()
165              .labels(labels)
166              .value(stats.missCount())
167              .build());
168
169      cacheRequestsTotal.dataPoint(
170          CounterSnapshot.CounterDataPointSnapshot.builder()
171              .labels(labels)
172              .value(stats.requestCount())
173              .build());
174
175      cacheEvictionTotal.dataPoint(
176          CounterSnapshot.CounterDataPointSnapshot.builder()
177              .labels(labels)
178              .value(stats.evictionCount())
179              .build());
180
181      cacheSize.dataPoint(
182          GaugeSnapshot.GaugeDataPointSnapshot.builder()
183              .labels(labels)
184              .value(c.getValue().size())
185              .build());
186
187      if (c.getValue() instanceof LoadingCache) {
188        cacheLoadFailure.dataPoint(
189            CounterSnapshot.CounterDataPointSnapshot.builder()
190                .labels(labels)
191                .value(stats.loadExceptionCount())
192                .build());
193
194        cacheLoadTotal.dataPoint(
195            CounterSnapshot.CounterDataPointSnapshot.builder()
196                .labels(labels)
197                .value(stats.loadCount())
198                .build());
199
200        cacheLoadSummary.dataPoint(
201            SummarySnapshot.SummaryDataPointSnapshot.builder()
202                .labels(labels)
203                .count(stats.loadCount())
204                .sum(stats.totalLoadTime() / NANOSECONDS_PER_SECOND)
205                .build());
206      }
207    }
208
209    metricSnapshotsBuilder.metricSnapshot(cacheHitTotal.build());
210    metricSnapshotsBuilder.metricSnapshot(cacheMissTotal.build());
211    metricSnapshotsBuilder.metricSnapshot(cacheRequestsTotal.build());
212    metricSnapshotsBuilder.metricSnapshot(cacheEvictionTotal.build());
213    metricSnapshotsBuilder.metricSnapshot(cacheLoadFailure.build());
214    metricSnapshotsBuilder.metricSnapshot(cacheLoadTotal.build());
215    metricSnapshotsBuilder.metricSnapshot(cacheSize.build());
216    metricSnapshotsBuilder.metricSnapshot(cacheLoadSummary.build());
217
218    return metricSnapshotsBuilder.build();
219  }
220
221  /**
222   * @deprecated Use {@link #getMetricFamilyDescriptors()} instead.
223   */
224  @Override
225  @Deprecated
226  @SuppressWarnings("InlineMeSuggester")
227  public List<String> getPrometheusNames() {
228    return ALL_METRIC_NAMES;
229  }
230}