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;
013
014import java.util.Arrays;
015import java.util.List;
016import java.util.Map;
017import java.util.concurrent.ConcurrentHashMap;
018import java.util.concurrent.ConcurrentMap;
019
020
021/**
022 * Collect metrics from Caffeine's com.github.benmanes.caffeine.cache.Cache.
023 * <p>
024 * <pre>{@code
025 *
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().register();
029 * cacheMetrics.addCache("mycache", cache);
030 *
031 * }</pre>
032 *
033 * Exposed metrics are labeled with the provided cache name.
034 *
035 * With the example above, sample metric names would be:
036 * <pre>
037 *     caffeine_cache_hit_total{cache="mycache"} 10.0
038 *     caffeine_cache_miss_total{cache="mycache"} 3.0
039 *     caffeine_cache_requests_total{cache="mycache"} 13.0
040 *     caffeine_cache_eviction_total{cache="mycache"} 1.0
041 *     caffeine_cache_estimated_size{cache="mycache"} 5.0
042 * </pre>
043 *
044 * Additionally, if the cache includes a loader, the following metrics would be provided:
045 * <pre>
046 *     caffeine_cache_load_failure_total{cache="mycache"} 2.0
047 *     caffeine_cache_loads_total{cache="mycache"} 7.0
048 *     caffeine_cache_load_duration_seconds_count{cache="mycache"} 7.0
049 *     caffeine_cache_load_duration_seconds_sum{cache="mycache"} 0.0034
050 * </pre>
051 *
052 */
053public class CacheMetricsCollector implements MultiCollector {
054    private static final double NANOSECONDS_PER_SECOND = 1_000_000_000.0;
055
056    protected final ConcurrentMap<String, Cache> children = new ConcurrentHashMap<String, Cache>();
057
058    /**
059     * Add or replace the cache with the given name.
060     * <p>
061     * Any references any previous cache with this name is invalidated.
062     *
063     * @param cacheName The name of the cache, will be the metrics label value
064     * @param cache The cache being monitored
065     */
066    public void addCache(String cacheName, Cache cache) {
067        children.put(cacheName, cache);
068    }
069
070    /**
071     * Add or replace the cache with the given name.
072     * <p>
073     * Any references any previous cache with this name is invalidated.
074     *
075     * @param cacheName The name of the cache, will be the metrics label value
076     * @param cache The cache being monitored
077     */
078    public void addCache(String cacheName, AsyncCache cache) {
079        children.put(cacheName, cache.synchronous());
080    }
081
082    /**
083     * Remove the cache with the given name.
084     * <p>
085     * Any references to the cache are invalidated.
086     *
087     * @param cacheName cache to be removed
088     */
089    public Cache removeCache(String cacheName) {
090        return children.remove(cacheName);
091    }
092
093    /**
094     * Remove all caches.
095     * <p>
096     * Any references to all caches are invalidated.
097     */
098    public void clear(){
099        children.clear();
100    }
101
102    @Override
103    public MetricSnapshots collect() {
104        final MetricSnapshots.Builder metricSnapshotsBuilder = MetricSnapshots.builder();
105        final List<String> labelNames = Arrays.asList("cache");
106
107        final CounterSnapshot.Builder cacheHitTotal = CounterSnapshot.builder()
108            .name("caffeine_cache_hit")
109            .help("Cache hit totals");
110
111        final CounterSnapshot.Builder cacheMissTotal = CounterSnapshot.builder()
112            .name("caffeine_cache_miss")
113            .help("Cache miss totals");
114
115        final CounterSnapshot.Builder cacheRequestsTotal = CounterSnapshot.builder()
116            .name("caffeine_cache_requests")
117            .help("Cache request totals, hits + misses");
118
119        final CounterSnapshot.Builder cacheEvictionTotal = CounterSnapshot.builder()
120            .name("caffeine_cache_eviction")
121            .help("Cache eviction totals, doesn't include manually removed entries");
122
123        final GaugeSnapshot.Builder cacheEvictionWeight = GaugeSnapshot.builder()
124            .name("caffeine_cache_eviction_weight")
125            .help("Cache eviction weight");
126
127        final CounterSnapshot.Builder cacheLoadFailure = CounterSnapshot.builder()
128            .name("caffeine_cache_load_failure")
129            .help("Cache load failures");
130
131        final CounterSnapshot.Builder cacheLoadTotal = CounterSnapshot.builder()
132            .name("caffeine_cache_loads")
133            .help("Cache loads: both success and failures");
134
135        final GaugeSnapshot.Builder cacheSize = GaugeSnapshot.builder()
136            .name("caffeine_cache_estimated_size")
137            .help("Estimated cache size");
138
139        final SummarySnapshot.Builder cacheLoadSummary = SummarySnapshot.builder()
140            .name("caffeine_cache_load_duration_seconds")
141            .help("Cache load duration: both success and failures");
142
143        for (final Map.Entry<String, Cache> c: children.entrySet()) {
144            final List<String> cacheName = Arrays.asList(c.getKey());
145            final Labels labels = Labels.of(labelNames, cacheName);
146
147            final CacheStats stats = c.getValue().stats();
148
149            try {
150                cacheEvictionWeight.dataPoint(
151                    GaugeSnapshot.GaugeDataPointSnapshot.builder()
152                        .labels(labels)
153                        .value(stats.evictionWeight())
154                        .build()
155                );
156            } catch (Exception e) {
157                // EvictionWeight metric is unavailable, newer version of Caffeine is needed.
158            }
159
160            cacheHitTotal.dataPoint(
161                CounterSnapshot.CounterDataPointSnapshot.builder()
162                    .labels(labels)
163                    .value(stats.hitCount())
164                    .build()
165            );
166
167            cacheMissTotal.dataPoint(
168                CounterSnapshot.CounterDataPointSnapshot.builder()
169                    .labels(labels)
170                    .value(stats.missCount())
171                    .build()
172            );
173
174            cacheRequestsTotal.dataPoint(
175                CounterSnapshot.CounterDataPointSnapshot.builder()
176                    .labels(labels)
177                    .value(stats.requestCount())
178                    .build()
179            );
180
181            cacheEvictionTotal.dataPoint(
182                CounterSnapshot.CounterDataPointSnapshot.builder()
183                    .labels(labels)
184                    .value(stats.evictionCount())
185                    .build()
186            );
187
188            cacheSize.dataPoint(
189                GaugeSnapshot.GaugeDataPointSnapshot.builder()
190                    .labels(labels)
191                    .value(c.getValue().estimatedSize())
192                    .build()
193            );
194
195            if (c.getValue() instanceof LoadingCache) {
196                cacheLoadFailure.dataPoint(
197                    CounterSnapshot.CounterDataPointSnapshot.builder()
198                        .labels(labels)
199                        .value(stats.loadFailureCount())
200                        .build()
201                );
202
203                cacheLoadTotal.dataPoint(
204                    CounterSnapshot.CounterDataPointSnapshot.builder()
205                        .labels(labels)
206                        .value(stats.loadCount())
207                        .build()
208                );
209
210                cacheLoadSummary.dataPoint(
211                    SummarySnapshot.SummaryDataPointSnapshot.builder()
212                        .labels(labels)
213                        .count(stats.loadCount())
214                        .sum(stats.totalLoadTime() / NANOSECONDS_PER_SECOND)
215                        .build()
216                );
217            }
218        }
219
220        return metricSnapshotsBuilder
221            .metricSnapshot(cacheHitTotal.build())
222            .metricSnapshot(cacheMissTotal.build())
223            .metricSnapshot(cacheRequestsTotal.build())
224            .metricSnapshot(cacheEvictionTotal.build())
225            .metricSnapshot(cacheEvictionWeight.build())
226            .metricSnapshot(cacheLoadFailure.build())
227            .metricSnapshot(cacheLoadTotal.build())
228            .metricSnapshot(cacheSize.build())
229            .metricSnapshot(cacheLoadSummary.build())
230            .build();
231    }
232}