Spring Bootでcacheにcaffeineを利用した際のmetricsをprometheusで取得する

## 検証version

spring boot 2.1.0.RELEASE

## Spring BootでCacheを利用する

まずCacheを利用するための設定をする。やることは

* `spring-boot-starter-cache` の依存を入れる
* Cacheableなmethodを書く。これはControllerに置かない。別のComponentに置く。
* ConfigurationクラスかApplicationクラスに`@EnableCaching`をつける

の3点

### `spring-boot-starter-cache` の依存を入れる

gradleならbuild.gradleのdependenciesに指定。webのsimple構成を作るため以下のように依存を定義する。

```groovy:build.gradle
dependencies {
	implementation('org.springframework.boot:spring-boot-starter-actuator')
	implementation('org.springframework.boot:spring-boot-starter-cache')
	implementation('org.springframework.boot:spring-boot-starter-web')
	compileOnly('org.projectlombok:lombok')
	testImplementation('org.springframework.boot:spring-boot-starter-test')
}
```

### Cacheableなmethodを定義する

適当にRepository classを用意して以下のように実装し、一度echoしたvalueはcacheから値を取り出すようにする。`@Cacheable`のvalueに設定した文字列はcacheNameである。

```java:EchoRepository
@Slf4j
@Repository
public class EchoRepository {

    @Cacheable("echo")
    public String echo(String value) {
        log.info("no cache: {}", value);
        return value;
    }
}
```

また、この処理を呼び出すRestControllerを実装

```java:EchoController
@RequiredArgsConstructor
@RestController
public class EchoController {

    private final EchoRepository repository;

    @GetMapping("echo")
    public String echo(@RequestParam  String value) {
        return repository.echo(value);
    }
}
```

### ConfigurationクラスかApplicationクラスに`@EnableCaching`をつける

とりあえずApplicationクラスにつけておく

```java:DemoApplication
@EnableCaching
@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}
```

### テスト

2つのparameterで2回ずつ呼び出し

```
$ curl http://localhost:8080/echo?value=test
test$ 
$ curl http://localhost:8080/echo?value=test
test$ 
$ curl http://localhost:8080/echo?value=test1
test1$ 
$ curl http://localhost:8080/echo?value=test1
test1$ 
```
log を確認

```
2018-10-31 23:41:26.824  INFO 3663 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 18 ms
2018-10-31 23:41:26.914  INFO 3663 --- [nio-8080-exec-1] c.k.demo.repository.EchoRepository       : no cache: test
2018-10-31 23:41:34.690  INFO 3663 --- [nio-8080-exec-3] c.k.demo.repository.EchoRepository       : no cache: test1
```

cacheされているようだ

## Caffeineを使う

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html

cache自体の実装は選択が可能である。デフォルトだと`ConcurrentHashMap`を使ったSimpleな実装が選択されるようだが、ここではCaffeineを使う。

利用するには、dependenciesに
`implementation('com.github.ben-manes.caffeine:caffeine')`
を追加すればよい

## Caffeineのmetricsを収集する

https://github.com/micrometer-metrics/micrometer/tree/master/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache

micrometerの実装に、Caffeineのmetricsを収集する実装がされている。これを利用したい。

### Spring Boot actuatorでprometheusのendpointを提供する

まず、dependenciesに
`implementation('io.micrometer:micrometer-registry-prometheus')`
を追加する。次に、`application.yml`に
`management.endpoints.web.exposure.include: prometheus`
を記載して、prometheusのendpointが叩けるようにし、試しにcurlでrequestしてみるとdefaultのmetricsやAutoConfigurationでregisterされるmetricsが取得できるようになる。
なお、actuatorのbase pathは`management.endpoints.web.base-path`で定義されていて、デフォルトが`/actuator`になっている

```
$ curl http://localhost:8080/actuator/prometheus 2>/dev/null | head
# HELP tomcat_global_request_seconds  
# TYPE tomcat_global_request_seconds summary
tomcat_global_request_seconds_count{name="http-nio-8080",} 4.0
tomcat_global_request_seconds_sum{name="http-nio-8080",} 0.597
# HELP tomcat_threads_config_max_threads  
# TYPE tomcat_threads_config_max_threads gauge
tomcat_threads_config_max_threads{name="http-nio-8080",} 200.0
# HELP tomcat_sessions_active_current_sessions  
# TYPE tomcat_sessions_active_current_sessions gauge
tomcat_sessions_active_current_sessions 0.0
```

### Caffeineのmetricsを収集するための設定追加

`spring.cache.caffeine.spec`に`recordStats`のparameterを入れる
例えば `spring.cache.caffeine.spec: maximumSize=500,expireAfterAccess=600s,recordStats` をapplication.ymlに定義
metrics収集の面では`recordStats`だけあれば良いが、その他の設定可能な値は [CaffeineSpec.java](https://github.com/ben-manes/caffeine/blob/e8ff6d3261e7f5666d2b486352cc04b2874d70ed/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CaffeineSpec.java) あたりを参照

### これで取得され始めると思ったが...

先程実装した`/echo`を叩いた後に、prometheusのendpointを呼ぶ

```
$ curl http://localhost:8080/echo?value=test
test$ 
$ curl http://localhost:8080/actuator/prometheus 2>/dev/null | grep cache
# HELP tomcat_cache_access_total  
# TYPE tomcat_cache_access_total counter
tomcat_cache_access_total 0.0
# HELP tomcat_cache_hit_total  
# TYPE tomcat_cache_hit_total counter
tomcat_cache_hit_total 0.0
```
...

### `spring.cache.cache-names`の設定

どうも`CacheMetricsRegistrar`あたりの実装を見る感じだと、先に静的に利用しているcacheのcache nameを`application.yml`に書いておかないとMicrometerにregisterされないようになっているようだ
なので、cache関連の設定は、

```
spring.cache:
  cache-names: echo
  caffeine.spec: maximumSize=500,expireAfterAccess=600s,recordStats
```
のようにする。再度確認すると取得できた

```
]$ curl http://localhost:8080/actuator/prometheus 2>/dev/null | grep cache
# HELP cache_eviction_weight The sum of weights of evicted entries. This total does not include manual invalidations.
# TYPE cache_eviction_weight gauge
cache_eviction_weight{cache="echo",cacheManager="cacheManager",name="echo",} 0.0
# HELP cache_gets_total the number of times cache lookup methods have returned an uncached (newly loaded) value, or null
# TYPE cache_gets_total counter
cache_gets_total{cache="echo",cacheManager="cacheManager",name="echo",result="miss",} 1.0
cache_gets_total{cache="echo",cacheManager="cacheManager",name="echo",result="hit",} 0.0
# HELP tomcat_cache_hit_total  
# TYPE tomcat_cache_hit_total counter
tomcat_cache_hit_total 0.0
# HELP cache_size The number of entries in this cache. This may be an approximation, depending on the type of cache.
# TYPE cache_size gauge
cache_size{cache="echo",cacheManager="cacheManager",name="echo",} 1.0
# HELP cache_evictions_total cache evictions
# TYPE cache_evictions_total counter
cache_evictions_total{cache="echo",cacheManager="cacheManager",name="echo",} 0.0
# HELP cache_puts_total The number of entries added to the cache
# TYPE cache_puts_total counter
cache_puts_total{cache="echo",cacheManager="cacheManager",name="echo",} 0.0
# HELP tomcat_cache_access_total  
# TYPE tomcat_cache_access_total counter
tomcat_cache_access_total 0.0
```

### `@Cachable`で指定したcacheのmetricsを動的にregisterしたい

`spring.cache.cache-names`に`@Cachable`で指定したcache名をつらつらと書いていくのは、数が増えると漏れが発生しそうだし面倒に感じる。現状このような実装をしたい場合の解として、力技だが`CaffeineCacheManager`の派生クラスを作りCaffeineCacheがcreateされるときにMeterRegistryにregisterされるような実装にすれば可能(だがおすすめはあまりできない...)

```java:InstrumentedCaffeineCacheManager
@RequiredArgsConstructor
public class InstrumentedCaffeineCacheManager extends CaffeineCacheManager {

    private final MeterRegistry meterRegistry;

    @Override
    protected Cache<Object, Object> createNativeCaffeineCache(String name) {
        Cache<Object, Object> nativeCache = super.createNativeCaffeineCache(name);
        CaffeineCacheMetrics.monitor(meterRegistry, nativeCache, name, Collections.emptyList());
        return nativeCache;
    }
}
```
これをBeanとして登録。ただし、`CaffeineCacheConfiguration`では単にBean生成する他に処理があるのでcopy

```java:CacheConfig
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching // Application classからこちらに移動した
@Configuration
@RequiredArgsConstructor
public class CacheConfig {

    private final MeterRegistry meterRegistry;

    private final CacheProperties cacheProperties;

    // CaffeineCacheConfigurationの実装をcopy
    @Bean
    public CaffeineCacheManager cacheManager() {
        CaffeineCacheManager cacheManager = createCacheManager();
        List<String> cacheNames = cacheProperties.getCacheNames();
        if (!CollectionUtils.isEmpty(cacheNames)) {
            cacheManager.setCacheNames(cacheNames);
        }
        return cacheManager;
    }
    // ここで独自CaffeineCacheManagerを生成
    private CaffeineCacheManager createCacheManager() {
        CaffeineCacheManager cacheManager = new InstrumentedCaffeineCacheManager(meterRegistry);
        setCacheBuilder(cacheManager);
        return cacheManager;
    }

    private void setCacheBuilder(CaffeineCacheManager cacheManager) {
        String specification = cacheProperties.getCaffeine().getSpec();
        if (StringUtils.hasText(specification)) {
            cacheManager.setCacheSpecification(specification);
        }
    }
}
```
これで、`application.yml`に`spring.cache.cache-names`を書かなくても`@Cacheable`がついたcacheのmetricsが動的にregisterされるようになる