Search code examples
prometheusguavaehcachespring-boot-actuatorspring-micrometer

EhCache metrics do not appear in Prometheus if Guava cache is bound to MeterRegistry


Implicitly created EhCache caches

I have a Spring component which declares some caches using a file ehcache.xml with the following kind of configuration:

<?xml version="1.0" encoding="UTF-8"?>
<eh:config
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:eh="http://www.ehcache.org/v3"
        xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.3.xsd">

    <eh:cache alias="Cache1">
        <eh:expiry>
            <eh:ttl unit="minutes">1</eh:ttl>
        </eh:expiry>
        <eh:resources>
            <eh:heap>10000</eh:heap>
        </eh:resources>
    </eh:cache>

<!-- [...] -->

</eh:config>

which are then used similarly to what follows:

import org.springframework.cache.annotation.Cacheable;

public class ThingDoer {
    @Cacheable("Cache1")
    public Integer doSomethingCached(int value) {
        return value + 3;
    }

    // ...
}

With this, I can see the different caches' metrics appear in Prometheus:

bash> 2>&1 curl -v --silent 'http://localhost:9000/actuator/prometheus' | grep cache_gets | sort

cache_gets_total{cache="Cache1",cache_manager="cacheManager",hostname="localhost",name="Cache1",result="hit",} 0.0
cache_gets_total{cache="Cache1",cache_manager="cacheManager",hostname="localhost",name="Cache1",result="miss",} 0.0
cache_gets_total{cache="Cache2",cache_manager="cacheManager",hostname="localhost",name="Cache2",result="hit",} 0.0
cache_gets_total{cache="Cache2",cache_manager="cacheManager",hostname="localhost",name="Cache2",result="miss",} 0.0
cache_gets_total{cache="Cache3",cache_manager="cacheManager",hostname="localhost",name="Cache3",result="hit",} 0.0
cache_gets_total{cache="Cache3",cache_manager="cacheManager",hostname="localhost",name="Cache3",result="miss",} 0.0
[...]

All of the caches I defined in my file ehcache.xml are exposing their metrics automagically.

Adding a homemade cache

Now, I created a homemade cache that performs complex operations when queried, and used the com.google.common.cache.LoadingCache interface for this, because it is meant to replace another instance of such a cache, with as few changes as possible to the rest of the code. I therefore created a custom cache, with the following signature:

import com.google.common.cache.AbstractLoadingCache;

public class CustomCache extends AbstractLoadingCache<String, Object> {
    public CustomCache(Function<String, Object> valueLoader) {
        // ...
    }

    // ...
}

It properly declares a stats() method that returns a com.google.common.cache.CacheStats containing the stats of the cache. The parameter valueLoader of its constructor is used to load a new value when it's missing from the cache.

Now, here is a sketch of how this cache is used:

import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;

public class CustomThingDoer {
    private final CustomCache customCache;

    public CustomThingDoer() {
        this.customCache = new CustomCache(this::loadNewObject);
    }

    public Optional<Object> doSomethingCached(String value) {
        try {
            return Optional.of(customCache.get(value));
        } catch (ExecutionException e) {
            System.out.println("Something wrong happened");
            return Optional.empty();
        }
    }

    private Object loadNewObject(String value) {
        // Do something non-static
    }
}

Since the cache is meant to be used in places where its methods are called directly (i.e. there is no @Cacheable annotation to use this cache, rather calls to get are used), and since the method to load missing values depends on the instance of the class in which it is defined (i.e. the method in question is a fetch into a database which code depends on things defined in the instance of CustomThingDoer), I wish to register it manually into the MeterRegistry.

To do so, I pass a bean of MeterRegistry to my CustomThingDoer in the constructor, and perform the following:

GuavaCacheMetrics.monitor(meterRegistry, this.customCache, "MyCustomCache");

Disappearing cache metrics

But now that this is done, this is what cache metrics I get in Prometheus:

bash> 2>&1 curl -v --silent 'http://localhost:9000/actuator/prometheus' | grep cache_gets | sort

cache_gets_total{cache="MyCustomCache",hostname="localhost",result="hit",} 0.0
cache_gets_total{cache="MyCustomCache",hostname="localhost",result="miss",} 0.0

Now, only the metrics of my custom cache appear. Furthermore, the field cache_manager is not present.

My suspicion is that since I register my cache manually, some predicate is broken with the automagic, and it never adds the automatically generated EhCache caches to the MeterRegistry.

I want to have both my custom cache's metrics as well as the implicitly created EhCache caches' metrics. If possible, I would like not to have to change the interface used (i.e. `com.google.common.cache.LoadingCache) as to make the minimal amount of changes on the code (and, honestly, to the unit tests). Does someone know what the issue might be, or what solution I may have?


Solution

  • It seems to be linked to a long-lasting bug in Micrometer: https://github.com/micrometer-metrics/micrometer/issues/877.

    It was fixed a few years ago, but it still has side-effects, among which the necessity to have the same set of tag keys for each metric name, otherwise some are not taken into account. https://github.com/micrometer-metrics/micrometer/issues/877#issuecomment-944894069