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;
012
013import java.util.Arrays;
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 * <p>
022 * <pre>{@code
023 *
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().register();
027 * cacheMetrics.addCache("mycache", cache);
028 *
029 * }</pre>
030 *
031 * Exposed metrics are labeled with the provided cache name.
032 *
033 * With the example above, sample metric names would be:
034 * <pre>
035 *     guava_cache_hit_total{cache="mycache"} 10.0
036 *     guava_cache_miss_total{cache="mycache"} 3.0
037 *     guava_cache_requests_total{cache="mycache"} 13.0
038 *     guava_cache_eviction_total{cache="mycache"} 1.0
039 *     guava_cache_size{cache="mycache"} 5.0
040 * </pre>
041 *
042 * Additionally, if the cache includes a loader, the following metrics would be provided:
043 * <pre>
044 *     guava_cache_load_failure_total{cache="mycache"} 2.0
045 *     guava_cache_loads_total{cache="mycache"} 7.0
046 *     guava_cache_load_duration_seconds_count{cache="mycache"} 7.0
047 *     guava_cache_load_duration_seconds_sum{cache="mycache"} 0.0034
048 * </pre>
049 *
050 */
051public class CacheMetricsCollector implements MultiCollector {
052
053    private static final double NANOSECONDS_PER_SECOND = 1_000_000_000.0;
054
055    protected final ConcurrentMap<String, Cache> children = new ConcurrentHashMap<>();
056
057    /**
058     * Add or replace the cache with the given name.
059     * <p>
060     * Any references any previous cache with this name is invalidated.
061     *
062     * @param cacheName The name of the cache, will be the metrics label value
063     * @param cache The cache being monitored
064     */
065    public void addCache(String cacheName, Cache cache) {
066        children.put(cacheName, cache);
067    }
068
069    /**
070     * Remove the cache with the given name.
071     * <p>
072     * Any references to the cache are invalidated.
073     *
074     * @param cacheName cache to be removed
075     */
076    public Cache removeCache(String cacheName) {
077        return children.remove(cacheName);
078    }
079
080    /**
081     * Remove all caches.
082     * <p>
083     * Any references to all caches are invalidated.
084     */
085    public void clear(){
086        children.clear();
087    }
088
089    @Override
090    public MetricSnapshots collect() {
091        final MetricSnapshots.Builder metricSnapshotsBuilder = MetricSnapshots.builder();
092        final List<String> labelNames = Arrays.asList("cache");
093
094        final CounterSnapshot.Builder cacheHitTotal = CounterSnapshot.builder()
095            .name("guava_cache_hit")
096            .help("Cache hit totals");
097
098        final CounterSnapshot.Builder cacheMissTotal = CounterSnapshot.builder()
099            .name("guava_cache_miss")
100            .help("Cache miss totals");
101
102        final CounterSnapshot.Builder cacheRequestsTotal = CounterSnapshot.builder()
103            .name("guava_cache_requests")
104            .help("Cache request totals");
105
106        final CounterSnapshot.Builder cacheEvictionTotal = CounterSnapshot.builder()
107            .name("guava_cache_eviction")
108            .help("Cache eviction totals, doesn't include manually removed entries");
109
110        final CounterSnapshot.Builder cacheLoadFailure = CounterSnapshot.builder()
111            .name("guava_cache_load_failure")
112            .help("Cache load failures");
113
114        final CounterSnapshot.Builder cacheLoadTotal = CounterSnapshot.builder()
115            .name("guava_cache_loads")
116            .help("Cache loads: both success and failures");
117
118        final GaugeSnapshot.Builder cacheSize = GaugeSnapshot.builder()
119            .name("guava_cache_size")
120            .help("Cache size");
121
122        final SummarySnapshot.Builder cacheLoadSummary = 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 = Arrays.asList(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
139            cacheMissTotal.dataPoint(
140                CounterSnapshot.CounterDataPointSnapshot.builder()
141                    .labels(labels)
142                    .value(stats.missCount())
143                    .build()
144            );
145
146            cacheRequestsTotal.dataPoint(
147                CounterSnapshot.CounterDataPointSnapshot.builder()
148                    .labels(labels)
149                    .value(stats.requestCount())
150                    .build()
151            );
152
153            cacheEvictionTotal.dataPoint(
154                CounterSnapshot.CounterDataPointSnapshot.builder()
155                    .labels(labels)
156                    .value(stats.evictionCount())
157                    .build()
158            );
159
160            cacheSize.dataPoint(
161                GaugeSnapshot.GaugeDataPointSnapshot.builder()
162                    .labels(labels)
163                    .value(c.getValue().size())
164                    .build()
165            );
166
167            if (c.getValue() instanceof LoadingCache) {
168                cacheLoadFailure.dataPoint(
169                    CounterSnapshot.CounterDataPointSnapshot.builder()
170                        .labels(labels)
171                        .value(stats.loadExceptionCount())
172                        .build()
173                );
174
175                cacheLoadTotal.dataPoint(
176                    CounterSnapshot.CounterDataPointSnapshot.builder()
177                        .labels(labels)
178                        .value(stats.loadCount())
179                        .build()
180                );
181
182                cacheLoadSummary.dataPoint(
183                    SummarySnapshot.SummaryDataPointSnapshot.builder()
184                        .labels(labels)
185                        .count(stats.loadCount())
186                        .sum(stats.totalLoadTime() / NANOSECONDS_PER_SECOND)
187                        .build()
188                );
189            }
190        }
191
192        metricSnapshotsBuilder.metricSnapshot(cacheHitTotal.build());
193        metricSnapshotsBuilder.metricSnapshot(cacheMissTotal.build());
194        metricSnapshotsBuilder.metricSnapshot(cacheRequestsTotal.build());
195        metricSnapshotsBuilder.metricSnapshot(cacheEvictionTotal.build());
196        metricSnapshotsBuilder.metricSnapshot(cacheLoadFailure.build());
197        metricSnapshotsBuilder.metricSnapshot(cacheLoadTotal.build());
198        metricSnapshotsBuilder.metricSnapshot(cacheSize.build());
199        metricSnapshotsBuilder.metricSnapshot(cacheLoadSummary.build());
200
201        return metricSnapshotsBuilder.build();
202    }
203}