From ec06ab96ed207e565268ae66f825ab2590fdcefb Mon Sep 17 00:00:00 2001 From: Jean Hominal Date: Mon, 20 Jan 2025 12:55:17 +0100 Subject: [PATCH] Caffeine instrumentation: add weighted size metric (#1251) * Change caffeine_cache_eviction_weight from Gauge to Counter Signed-off-by: Jean Hominal * Add caffeine_cache_weighted_size gauge metric to caffeine-instrumentation Signed-off-by: Jean Hominal * Add Builder pattern for caffeine CacheMetricsCollector Signed-off-by: Jean Hominal * Add option collectEvictionWeightAsCounter to collect caffeine_cache_eviction_weight as an incremental counter Signed-off-by: Jean Hominal * Add option collectWeightedSize to enable collection of caffeine_cache_weighted_size Signed-off-by: Jean Hominal --------- Signed-off-by: Jean Hominal --- .../caffeine/CacheMetricsCollector.java | 102 ++++++++++- .../caffeine/CacheMetricsCollectorTest.java | 162 ++++++++++++++++-- 2 files changed, 247 insertions(+), 17 deletions(-) diff --git a/prometheus-metrics-instrumentation-caffeine/src/main/java/io/prometheus/metrics/instrumentation/caffeine/CacheMetricsCollector.java b/prometheus-metrics-instrumentation-caffeine/src/main/java/io/prometheus/metrics/instrumentation/caffeine/CacheMetricsCollector.java index c5c9fef3c..a1b8782a3 100644 --- a/prometheus-metrics-instrumentation-caffeine/src/main/java/io/prometheus/metrics/instrumentation/caffeine/CacheMetricsCollector.java +++ b/prometheus-metrics-instrumentation-caffeine/src/main/java/io/prometheus/metrics/instrumentation/caffeine/CacheMetricsCollector.java @@ -3,6 +3,7 @@ import com.github.benmanes.caffeine.cache.AsyncCache; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.LoadingCache; +import com.github.benmanes.caffeine.cache.Policy; import com.github.benmanes.caffeine.cache.stats.CacheStats; import io.prometheus.metrics.model.registry.MultiCollector; import io.prometheus.metrics.model.snapshots.CounterSnapshot; @@ -14,8 +15,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; /** * Collect metrics from Caffeine's com.github.benmanes.caffeine.cache.Cache. @@ -25,7 +28,7 @@ *
{@code
  * // Note that `recordStats()` is required to gather non-zero statistics
  * Cache cache = Caffeine.newBuilder().recordStats().build();
- * CacheMetricsCollector cacheMetrics = new CacheMetricsCollector();
+ * CacheMetricsCollector cacheMetrics = CacheMetricsCollector.builder().build();
  * PrometheusRegistry.defaultRegistry.register(cacheMetrics);
  * cacheMetrics.addCache("mycache", cache);
  *
@@ -63,6 +66,7 @@ public class CacheMetricsCollector implements MultiCollector {
   private static final String METRIC_NAME_CACHE_LOAD_FAILURE = "caffeine_cache_load_failure";
   private static final String METRIC_NAME_CACHE_LOADS = "caffeine_cache_loads";
   private static final String METRIC_NAME_CACHE_ESTIMATED_SIZE = "caffeine_cache_estimated_size";
+  private static final String METRIC_NAME_CACHE_WEIGHTED_SIZE = "caffeine_cache_weighted_size";
   private static final String METRIC_NAME_CACHE_LOAD_DURATION_SECONDS =
       "caffeine_cache_load_duration_seconds";
 
@@ -77,9 +81,38 @@ public class CacheMetricsCollector implements MultiCollector {
               METRIC_NAME_CACHE_LOAD_FAILURE,
               METRIC_NAME_CACHE_LOADS,
               METRIC_NAME_CACHE_ESTIMATED_SIZE,
+              METRIC_NAME_CACHE_WEIGHTED_SIZE,
               METRIC_NAME_CACHE_LOAD_DURATION_SECONDS));
 
   protected final ConcurrentMap> children = new ConcurrentHashMap<>();
+  private final boolean collectEvictionWeightAsCounter;
+  private final boolean collectWeightedSize;
+
+  /**
+   * Instantiates a {@link CacheMetricsCollector}, with the legacy parameters.
+   *
+   * 

The use of this constructor is discouraged, in favor of a Builder pattern {@link #builder()} + * + *

Note that the {@link #builder()} API has different default values than this deprecated + * constructor. + */ + @Deprecated + public CacheMetricsCollector() { + this(false, false); + } + + /** + * Instantiate a {@link CacheMetricsCollector} + * + * @param collectEvictionWeightAsCounter If true, {@code caffeine_cache_eviction_weight} will be + * observed as an incrementing counter instead of a gauge. + * @param collectWeightedSize If true, {@code caffeine_cache_weighted_size} will be observed. + */ + protected CacheMetricsCollector( + boolean collectEvictionWeightAsCounter, boolean collectWeightedSize) { + this.collectEvictionWeightAsCounter = collectEvictionWeightAsCounter; + this.collectWeightedSize = collectWeightedSize; + } /** * Add or replace the cache with the given name. @@ -146,10 +179,14 @@ public MetricSnapshots collect() { .name(METRIC_NAME_CACHE_EVICTION) .help("Cache eviction totals, doesn't include manually removed entries"); - final GaugeSnapshot.Builder cacheEvictionWeight = + final CounterSnapshot.Builder cacheEvictionWeight = + CounterSnapshot.builder() + .name(METRIC_NAME_CACHE_EVICTION_WEIGHT) + .help("Weight of evicted cache entries, doesn't include manually removed entries"); + final GaugeSnapshot.Builder cacheEvictionWeightLegacyGauge = GaugeSnapshot.builder() .name(METRIC_NAME_CACHE_EVICTION_WEIGHT) - .help("Cache eviction weight"); + .help("Weight of evicted cache entries, doesn't include manually removed entries"); final CounterSnapshot.Builder cacheLoadFailure = CounterSnapshot.builder().name(METRIC_NAME_CACHE_LOAD_FAILURE).help("Cache load failures"); @@ -162,6 +199,11 @@ public MetricSnapshots collect() { final GaugeSnapshot.Builder cacheSize = GaugeSnapshot.builder().name(METRIC_NAME_CACHE_ESTIMATED_SIZE).help("Estimated cache size"); + final GaugeSnapshot.Builder cacheWeightedSize = + GaugeSnapshot.builder() + .name(METRIC_NAME_CACHE_WEIGHTED_SIZE) + .help("Approximate accumulated weight of cache entries"); + final SummarySnapshot.Builder cacheLoadSummary = SummarySnapshot.builder() .name(METRIC_NAME_CACHE_LOAD_DURATION_SECONDS) @@ -175,6 +217,11 @@ public MetricSnapshots collect() { try { cacheEvictionWeight.dataPoint( + CounterSnapshot.CounterDataPointSnapshot.builder() + .labels(labels) + .value(stats.evictionWeight()) + .build()); + cacheEvictionWeightLegacyGauge.dataPoint( GaugeSnapshot.GaugeDataPointSnapshot.builder() .labels(labels) .value(stats.evictionWeight()) @@ -183,6 +230,17 @@ public MetricSnapshots collect() { // EvictionWeight metric is unavailable, newer version of Caffeine is needed. } + if (collectWeightedSize) { + final Optional> eviction = c.getValue().policy().eviction(); + if (eviction.isPresent() && eviction.get().weightedSize().isPresent()) { + cacheWeightedSize.dataPoint( + GaugeSnapshot.GaugeDataPointSnapshot.builder() + .labels(labels) + .value(eviction.get().weightedSize().getAsLong()) + .build()); + } + } + cacheHitTotal.dataPoint( CounterSnapshot.CounterDataPointSnapshot.builder() .labels(labels) @@ -235,12 +293,19 @@ public MetricSnapshots collect() { } } + if (collectWeightedSize) { + metricSnapshotsBuilder.metricSnapshot(cacheWeightedSize.build()); + } + return metricSnapshotsBuilder .metricSnapshot(cacheHitTotal.build()) .metricSnapshot(cacheMissTotal.build()) .metricSnapshot(cacheRequestsTotal.build()) .metricSnapshot(cacheEvictionTotal.build()) - .metricSnapshot(cacheEvictionWeight.build()) + .metricSnapshot( + collectEvictionWeightAsCounter + ? cacheEvictionWeight.build() + : cacheEvictionWeightLegacyGauge.build()) .metricSnapshot(cacheLoadFailure.build()) .metricSnapshot(cacheLoadTotal.build()) .metricSnapshot(cacheSize.build()) @@ -250,6 +315,35 @@ public MetricSnapshots collect() { @Override public List getPrometheusNames() { + if (!collectWeightedSize) { + return ALL_METRIC_NAMES.stream() + .filter(s -> !METRIC_NAME_CACHE_WEIGHTED_SIZE.equals(s)) + .collect(Collectors.toList()); + } return ALL_METRIC_NAMES; } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private boolean collectEvictionWeightAsCounter = true; + private boolean collectWeightedSize = true; + + public Builder collectEvictionWeightAsCounter(boolean collectEvictionWeightAsCounter) { + this.collectEvictionWeightAsCounter = collectEvictionWeightAsCounter; + return this; + } + + public Builder collectWeightedSize(boolean collectWeightedSize) { + this.collectWeightedSize = collectWeightedSize; + return this; + } + + public CacheMetricsCollector build() { + return new CacheMetricsCollector(collectEvictionWeightAsCounter, collectWeightedSize); + } + } } diff --git a/prometheus-metrics-instrumentation-caffeine/src/test/java/io/prometheus/metrics/instrumentation/caffeine/CacheMetricsCollectorTest.java b/prometheus-metrics-instrumentation-caffeine/src/test/java/io/prometheus/metrics/instrumentation/caffeine/CacheMetricsCollectorTest.java index 4ffb1d794..ac5ec9e2a 100644 --- a/prometheus-metrics-instrumentation-caffeine/src/test/java/io/prometheus/metrics/instrumentation/caffeine/CacheMetricsCollectorTest.java +++ b/prometheus-metrics-instrumentation-caffeine/src/test/java/io/prometheus/metrics/instrumentation/caffeine/CacheMetricsCollectorTest.java @@ -13,6 +13,7 @@ import io.prometheus.metrics.model.registry.PrometheusRegistry; import io.prometheus.metrics.model.snapshots.CounterSnapshot; import io.prometheus.metrics.model.snapshots.DataPointSnapshot; +import io.prometheus.metrics.model.snapshots.GaugeSnapshot; import io.prometheus.metrics.model.snapshots.Labels; import io.prometheus.metrics.model.snapshots.MetricSnapshots; import io.prometheus.metrics.model.snapshots.SummarySnapshot; @@ -22,17 +23,112 @@ import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; @SuppressWarnings("CheckReturnValue") class CacheMetricsCollectorTest { + // This enum was added to simplify test parametrization on argument options. + public enum Options { + LEGACY(false, false), + COLLECT_EVICTION_WEIGHT_AS_COUNTER(true, false), + COLLECT_WEIGHTED_SIZE(false, true), + BUILDER_DEFAULT(true, true); + + private final boolean collectEvictionWeightAsCounter; + private final boolean collectWeightedSize; + + Options(boolean collectEvictionWeightAsCounter, boolean collectWeightedSize) { + this.collectEvictionWeightAsCounter = collectEvictionWeightAsCounter; + this.collectWeightedSize = collectWeightedSize; + } + } - @Test - public void cacheExposesMetricsForHitMissAndEviction() { + @ParameterizedTest + @EnumSource + public void cacheExposesMetricsForHitMissAndEviction(Options options) { // Run cleanup in same thread, to remove async behavior with evictions final Cache cache = Caffeine.newBuilder().maximumSize(2).recordStats().executor(Runnable::run).build(); - final CacheMetricsCollector collector = new CacheMetricsCollector(); + final CacheMetricsCollector collector = + CacheMetricsCollector.builder() + .collectEvictionWeightAsCounter(options.collectEvictionWeightAsCounter) + .collectWeightedSize(options.collectWeightedSize) + .build(); + collector.addCache("users", cache); + + final PrometheusRegistry registry = new PrometheusRegistry(); + registry.register(collector); + + cache.getIfPresent("user1"); + cache.getIfPresent("user1"); + cache.put("user1", "First User"); + cache.getIfPresent("user1"); + + // Add to cache to trigger eviction. + cache.put("user2", "Second User"); + cache.put("user3", "Third User"); + cache.put("user4", "Fourth User"); + + assertCounterMetric(registry, "caffeine_cache_hit", "users", 1.0); + assertCounterMetric(registry, "caffeine_cache_miss", "users", 2.0); + assertCounterMetric(registry, "caffeine_cache_requests", "users", 3.0); + assertCounterMetric(registry, "caffeine_cache_eviction", "users", 2.0); + String openMetricEvictionWeightExpectedText; + if (options.collectEvictionWeightAsCounter) { + assertCounterMetric(registry, "caffeine_cache_eviction_weight", "users", 2.0); + openMetricEvictionWeightExpectedText = + "# TYPE caffeine_cache_eviction_weight counter\n" + + "# HELP caffeine_cache_eviction_weight Weight of evicted cache entries, doesn't include manually removed entries\n" + + "caffeine_cache_eviction_weight_total{cache=\"users\"} 2.0\n"; + } else { + assertGaugeMetric(registry, "caffeine_cache_eviction_weight", "users", 2.0); + openMetricEvictionWeightExpectedText = + "# TYPE caffeine_cache_eviction_weight gauge\n" + + "# HELP caffeine_cache_eviction_weight Weight of evicted cache entries, doesn't include manually removed entries\n" + + "caffeine_cache_eviction_weight{cache=\"users\"} 2.0\n"; + } + + final String expected = + "# TYPE caffeine_cache_estimated_size gauge\n" + + "# HELP caffeine_cache_estimated_size Estimated cache size\n" + + "caffeine_cache_estimated_size{cache=\"users\"} 2.0\n" + + "# TYPE caffeine_cache_eviction counter\n" + + "# HELP caffeine_cache_eviction Cache eviction totals, doesn't include manually removed entries\n" + + "caffeine_cache_eviction_total{cache=\"users\"} 2.0\n" + + openMetricEvictionWeightExpectedText + + "# TYPE caffeine_cache_hit counter\n" + + "# HELP caffeine_cache_hit Cache hit totals\n" + + "caffeine_cache_hit_total{cache=\"users\"} 1.0\n" + + "# TYPE caffeine_cache_miss counter\n" + + "# HELP caffeine_cache_miss Cache miss totals\n" + + "caffeine_cache_miss_total{cache=\"users\"} 2.0\n" + + "# TYPE caffeine_cache_requests counter\n" + + "# HELP caffeine_cache_requests Cache request totals, hits + misses\n" + + "caffeine_cache_requests_total{cache=\"users\"} 3.0\n" + + "# EOF\n"; + + assertThat(convertToOpenMetricsFormat(registry)).isEqualTo(expected); + } + + @ParameterizedTest + @EnumSource + public void weightedCacheExposesMetricsForHitMissAndEvictionWeightedSize(Options options) { + // Run cleanup in same thread, to remove async behavior with evictions + final Cache cache = + Caffeine.newBuilder() + .weigher((String k, String v) -> k.length() + v.length()) + .maximumWeight(35) + .recordStats() + .executor(Runnable::run) + .build(); + + final CacheMetricsCollector collector = + CacheMetricsCollector.builder() + .collectEvictionWeightAsCounter(options.collectEvictionWeightAsCounter) + .collectWeightedSize(options.collectWeightedSize) + .build(); collector.addCache("users", cache); final PrometheusRegistry registry = new PrometheusRegistry(); @@ -52,6 +148,29 @@ public void cacheExposesMetricsForHitMissAndEviction() { assertCounterMetric(registry, "caffeine_cache_miss", "users", 2.0); assertCounterMetric(registry, "caffeine_cache_requests", "users", 3.0); assertCounterMetric(registry, "caffeine_cache_eviction", "users", 2.0); + String openMetricEvictionWeightExpectedText; + if (options.collectEvictionWeightAsCounter) { + assertCounterMetric(registry, "caffeine_cache_eviction_weight", "users", 31.0); + openMetricEvictionWeightExpectedText = + "# TYPE caffeine_cache_eviction_weight counter\n" + + "# HELP caffeine_cache_eviction_weight Weight of evicted cache entries, doesn't include manually removed entries\n" + + "caffeine_cache_eviction_weight_total{cache=\"users\"} 31.0\n"; + } else { + assertGaugeMetric(registry, "caffeine_cache_eviction_weight", "users", 31.0); + openMetricEvictionWeightExpectedText = + "# TYPE caffeine_cache_eviction_weight gauge\n" + + "# HELP caffeine_cache_eviction_weight Weight of evicted cache entries, doesn't include manually removed entries\n" + + "caffeine_cache_eviction_weight{cache=\"users\"} 31.0\n"; + } + String openMetricWeightedSizeExpectedText; + if (options.collectWeightedSize) { + openMetricWeightedSizeExpectedText = + "# TYPE caffeine_cache_weighted_size gauge\n" + + "# HELP caffeine_cache_weighted_size Approximate accumulated weight of cache entries\n" + + "caffeine_cache_weighted_size{cache=\"users\"} 31.0\n"; + } else { + openMetricWeightedSizeExpectedText = ""; + } final String expected = "# TYPE caffeine_cache_estimated_size gauge\n" @@ -60,9 +179,7 @@ public void cacheExposesMetricsForHitMissAndEviction() { + "# TYPE caffeine_cache_eviction counter\n" + "# HELP caffeine_cache_eviction Cache eviction totals, doesn't include manually removed entries\n" + "caffeine_cache_eviction_total{cache=\"users\"} 2.0\n" - + "# TYPE caffeine_cache_eviction_weight gauge\n" - + "# HELP caffeine_cache_eviction_weight Cache eviction weight\n" - + "caffeine_cache_eviction_weight{cache=\"users\"} 2.0\n" + + openMetricEvictionWeightExpectedText + "# TYPE caffeine_cache_hit counter\n" + "# HELP caffeine_cache_hit Cache hit totals\n" + "caffeine_cache_hit_total{cache=\"users\"} 1.0\n" @@ -72,6 +189,7 @@ public void cacheExposesMetricsForHitMissAndEviction() { + "# TYPE caffeine_cache_requests counter\n" + "# HELP caffeine_cache_requests Cache request totals, hits + misses\n" + "caffeine_cache_requests_total{cache=\"users\"} 3.0\n" + + openMetricWeightedSizeExpectedText + "# EOF\n"; assertThat(convertToOpenMetricsFormat(registry)).isEqualTo(expected); @@ -87,7 +205,7 @@ public void loadingCacheExposesMetricsForLoadsAndExceptions() throws Exception { .thenReturn("Third User"); final LoadingCache cache = Caffeine.newBuilder().recordStats().build(loader); - final CacheMetricsCollector collector = new CacheMetricsCollector(); + final CacheMetricsCollector collector = CacheMetricsCollector.builder().build(); collector.addCache("loadingusers", cache); @@ -117,9 +235,14 @@ public void loadingCacheExposesMetricsForLoadsAndExceptions() throws Exception { assertThat(loadDuration.getSum()).isGreaterThan(0); } - @Test - public void getPrometheusNamesHasSameSizeAsMetricSizeWhenScraping() { - final CacheMetricsCollector collector = new CacheMetricsCollector(); + @ParameterizedTest + @EnumSource + public void getPrometheusNamesHasSameSizeAsMetricSizeWhenScraping(Options options) { + final CacheMetricsCollector collector = + CacheMetricsCollector.builder() + .collectEvictionWeightAsCounter(options.collectEvictionWeightAsCounter) + .collectWeightedSize(options.collectWeightedSize) + .build(); final PrometheusRegistry registry = new PrometheusRegistry(); registry.register(collector); @@ -130,9 +253,14 @@ public void getPrometheusNamesHasSameSizeAsMetricSizeWhenScraping() { assertThat(prometheusNames).hasSize(metricSnapshots.size()); } - @Test - public void collectedMetricNamesAreKnownPrometheusNames() { - final CacheMetricsCollector collector = new CacheMetricsCollector(); + @ParameterizedTest + @EnumSource + public void collectedMetricNamesAreKnownPrometheusNames(Options options) { + final CacheMetricsCollector collector = + CacheMetricsCollector.builder() + .collectEvictionWeightAsCounter(options.collectEvictionWeightAsCounter) + .collectWeightedSize(options.collectWeightedSize) + .build(); final PrometheusRegistry registry = new PrometheusRegistry(); registry.register(collector); @@ -153,6 +281,14 @@ private void assertCounterMetric( assertThat(dataPointSnapshot.getValue()).isEqualTo(value); } + private void assertGaugeMetric( + PrometheusRegistry registry, String name, String cacheName, double value) { + final GaugeSnapshot.GaugeDataPointSnapshot dataPointSnapshot = + (GaugeSnapshot.GaugeDataPointSnapshot) getDataPointSnapshot(registry, name, cacheName); + + assertThat(dataPointSnapshot.getValue()).isEqualTo(value); + } + private DataPointSnapshot getDataPointSnapshot( PrometheusRegistry registry, String name, String cacheName) { final Labels labels = Labels.of(new String[] {"cache"}, new String[] {cacheName});