Micrometer Cache Instrumentations

Micrometer supports binding metrics to a variety of popular caching libraries. Each implementation supports basic features, such as cache hits versus misses, from which you can derive basic information about the cache hit ratio over a period of time. Micrometer uses a function-tracking counter to monitor such things as hits and misses, giving you a notion not only of hits and misses over the total life of the cache (the basic metric exposed from Guava’s CacheStats, for example) but hits and misses inside a given interval.

Example

To demonstrate the features of cache monitoring, we start with a simple program that uses reactor-netty to read the entirety of Mary Shelley’s Frankenstein and put each word in the cache if it has not yet been seen:

// read all of Frankenstein
HttpClient.create("www.gutenberg.org")
    .get("/cache/epub/84/pg84.txt")
    .flatMapMany(res -> res.addHandler(wordDecoder()).receive().asString())
    .delayElements(Duration.ofMillis(10)) // one word per 10 ms
    .filter(word -> !word.isEmpty())
    .doOnNext(word -> {
        if (cache.getIfPresent(word) == null)
            cache.put(word, 1);
    })
    .blockLast();

The following image shows the hits versus misses on a cache that has been tuned to hold a maximum of 10,000 entries:

Hits vs. misses
Figure 1. Hits vs. misses, viewed in Prometheus
rate(book_guava_requests_total[10s])

By dividing the hits by the sum of all get operations (regardless of whether or not each one was a hit or a miss), we can arrive at a notion of the upper bound for the hit ratio for reading Frankenstein with only 10,000 words:

Hit ratio
Figure 2. Hit ratio, viewed by Prometheus
sum(rate(book_guava_requests_total{result="hit"}[1m])) / sum(rate(book_guava_requests_total[1m]))

In a real-world scenario, we tune caches according to how we evaluate the tradeoff between storage and load efficiency. You could create an alert based on some upper bound for the rate at which misses occur or on a lower bound for the hit ratio. Setting an upper bound on the miss ratio is better than a lower bound on the hit ratio. For both ratios, an absence of any activity drops the value to 0. The following image shows the miss ratio when it exceeds 10%:

Miss ratio (alerted)
Figure 3. Alerting when the miss ratio exceeds 10%

Cache implementations

Micrometer can instrument various cache implementations:

Caffeine

// Setting up instrumentation
LoadingCache<String, String> cache = Caffeine.newBuilder().recordStats().build(key -> "");

CaffeineCacheMetrics<String, String, Cache<String, String>> metrics = new CaffeineCacheMetrics<>(cache, "testCache",
        expectedTag);


// Binding manually
MeterRegistry registry = new SimpleMeterRegistry();
metrics.bindTo(registry);

// Binding through a static method
MeterRegistry meterRegistry = new SimpleMeterRegistry();
CaffeineCacheMetrics.monitor(meterRegistry, cache, "testCache", expectedTag);

Ehcache 2

// Setting up instrumentation
static Cache cache;

EhCache2Metrics metrics = new EhCache2Metrics(cache, expectedTag);


// Binding manually
MeterRegistry registry = new SimpleMeterRegistry();
metrics.bindTo(registry);

// Binding through a static method
MeterRegistry meterRegistry = new SimpleMeterRegistry();
EhCache2Metrics.monitor(meterRegistry, cache, expectedTag);

Guava

// Setting up instrumentation
LoadingCache<String, String> cache = CacheBuilder.newBuilder().recordStats().build(new CacheLoader<>() {
    public String load(String key) {
        return "";
    }
});

GuavaCacheMetrics<String, String, Cache<String, String>> metrics = new GuavaCacheMetrics<>(cache, "testCache",
        expectedTag);


// Binding manually
MeterRegistry registry = new SimpleMeterRegistry();
metrics.bindTo(registry);

// Binding through a static method
MeterRegistry meterRegistry = new SimpleMeterRegistry();
GuavaCacheMetrics.monitor(meterRegistry, cache, "testCache", expectedTag);

Hazelcast

// Setting up instrumentation
static IMap<String, String> cache;

HazelcastCacheMetrics metrics = new HazelcastCacheMetrics(cache, expectedTag);


// Binding manually
MeterRegistry meterRegistry = new SimpleMeterRegistry();
metrics.bindTo(meterRegistry);

// Binding through a static method
MeterRegistry meterRegistry = new SimpleMeterRegistry();
HazelcastCacheMetrics.monitor(meterRegistry, cache, expectedTag);

JCache

// Setting up instrumentation
Cache<String, String> cache;

JCacheMetrics<String, String, Cache<String, String>> metrics;


metrics = new JCacheMetrics<>(cache, expectedTag);

// Binding manually
MeterRegistry meterRegistry = new SimpleMeterRegistry();
metrics.bindTo(meterRegistry);

// Binding through a static method
MeterRegistry meterRegistry = new SimpleMeterRegistry();
JCacheMetrics.monitor(meterRegistry, cache, expectedTag);