Expose cache metrics for Redis

This commit adds support for Redis cache metrics. Users can opt-in for
statistics using the "spring.cache.redis.enable-statistics" property.

Closes gh-22701
This commit is contained in:
Stephane Nicoll 2020-10-07 17:20:53 +02:00
parent a099cd9420
commit 34c4c3f235
9 changed files with 335 additions and 1 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -26,12 +26,14 @@ import org.springframework.boot.actuate.metrics.cache.CaffeineCacheMeterBinderPr
import org.springframework.boot.actuate.metrics.cache.EhCache2CacheMeterBinderProvider;
import org.springframework.boot.actuate.metrics.cache.HazelcastCacheMeterBinderProvider;
import org.springframework.boot.actuate.metrics.cache.JCacheCacheMeterBinderProvider;
import org.springframework.boot.actuate.metrics.cache.RedisCacheMeterBinderProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.ehcache.EhCacheCache;
import org.springframework.cache.jcache.JCacheCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCache;
/**
* Configure {@link CacheMeterBinderProvider} beans.
@ -86,4 +88,15 @@ class CacheMeterBinderProvidersConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisCache.class)
static class RedisCacheMeterBinderProviderConfiguration {
@Bean
RedisCacheMeterBinderProvider redisCacheMeterBinderProvider() {
return new RedisCacheMeterBinderProvider();
}
}
}

View File

@ -83,6 +83,7 @@ dependencies {
testImplementation("org.skyscreamer:jsonassert")
testImplementation("org.springframework:spring-test")
testImplementation("com.squareup.okhttp3:mockwebserver")
testImplementation("org.testcontainers:junit-jupiter")
testRuntimeOnly("io.projectreactor.netty:reactor-netty-http")
testRuntimeOnly("javax.xml.bind:jaxb-api")

View File

@ -0,0 +1,37 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.cache;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.data.redis.cache.RedisCache;
/**
* {@link CacheMeterBinderProvider} implementation for Redis.
*
* @author Stephane Nicoll
* @since 2.4.0
*/
public class RedisCacheMeterBinderProvider implements CacheMeterBinderProvider<RedisCache> {
@Override
public MeterBinder getMeterBinder(RedisCache cache, Iterable<Tag> tags) {
return new RedisCacheMetrics(cache, tags);
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.cache;
import java.util.concurrent.TimeUnit;
import io.micrometer.core.instrument.FunctionCounter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.TimeGauge;
import io.micrometer.core.instrument.binder.cache.CacheMeterBinder;
import org.springframework.data.redis.cache.RedisCache;
/**
* {@link CacheMeterBinder} for {@link RedisCache}.
*
* @author Stephane Nicoll
* @since 2.4.0
*/
public class RedisCacheMetrics extends CacheMeterBinder {
private final RedisCache cache;
public RedisCacheMetrics(RedisCache cache, Iterable<Tag> tags) {
super(cache, cache.getName(), tags);
this.cache = cache;
}
@Override
protected Long size() {
return null;
}
@Override
protected long hitCount() {
return this.cache.getStatistics().getHits();
}
@Override
protected Long missCount() {
return this.cache.getStatistics().getMisses();
}
@Override
protected Long evictionCount() {
return null;
}
@Override
protected long putCount() {
return this.cache.getStatistics().getPuts();
}
@Override
protected void bindImplementationSpecificMetrics(MeterRegistry registry) {
FunctionCounter.builder("cache.removals", this.cache, (cache) -> cache.getStatistics().getDeletes())
.tags(getTagsWithCacheName()).description("Cache removals").register(registry);
FunctionCounter.builder("cache.gets", this.cache, (cache) -> cache.getStatistics().getPending())
.tags(getTagsWithCacheName()).tag("result", "pending").description("The number of pending requests")
.register(registry);
TimeGauge
.builder("cache.lock.duration", this.cache, TimeUnit.NANOSECONDS,
(cache) -> cache.getStatistics().getLockWaitDuration(TimeUnit.NANOSECONDS))
.tags(getTagsWithCacheName()).description("The time the cache has spent waiting on a lock")
.register(registry);
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.cache;
import java.util.Collections;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.cache.RedisCache;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RedisCacheMeterBinderProvider}.
*
* @author Stephane Nicoll
*/
class RedisCacheMeterBinderProviderTests {
@Test
void redisCacheProvider() {
RedisCache cache = mock(RedisCache.class);
given(cache.getName()).willReturn("test");
MeterBinder meterBinder = new RedisCacheMeterBinderProvider().getMeterBinder(cache, Collections.emptyList());
assertThat(meterBinder).isInstanceOf(RedisCacheMetrics.class);
}
}

View File

@ -0,0 +1,139 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.metrics.cache;
import java.util.UUID;
import java.util.function.BiConsumer;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.testsupport.testcontainers.RedisContainer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheManager;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RedisCacheMetrics}.
*
* @author Stephane Nicoll
*/
@Testcontainers(disabledWithoutDocker = true)
class RedisCacheMetricsTests {
@Container
static final RedisContainer redis = new RedisContainer();
private static final Tags TAGS = Tags.of("app", "test").and("cache", "test");
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class, CacheAutoConfiguration.class))
.withUserConfiguration(CachingConfiguration.class).withPropertyValues(
"spring.redis.host=" + redis.getHost(), "spring.redis.port=" + redis.getFirstMappedPort(),
"spring.cache.type=redis", "spring.cache.redis.enable-statistics=true");
@Test
void cacheStatisticsAreExposed() {
this.contextRunner.run(withCacheMetrics((cache, meterRegistry) -> {
assertThat(meterRegistry.find("cache.size").tags(TAGS).functionCounter()).isNull();
assertThat(meterRegistry.find("cache.gets").tags(TAGS.and("result", "hit")).functionCounter()).isNotNull();
assertThat(meterRegistry.find("cache.gets").tags(TAGS.and("result", "miss")).functionCounter()).isNotNull();
assertThat(meterRegistry.find("cache.gets").tags(TAGS.and("result", "pending")).functionCounter())
.isNotNull();
assertThat(meterRegistry.find("cache.evictions").tags(TAGS).functionCounter()).isNull();
assertThat(meterRegistry.find("cache.puts").tags(TAGS).functionCounter()).isNotNull();
assertThat(meterRegistry.find("cache.removals").tags(TAGS).functionCounter()).isNotNull();
assertThat(meterRegistry.find("cache.lock.duration").tags(TAGS).timeGauge()).isNotNull();
}));
}
@Test
void cacheHitsAreExposed() {
this.contextRunner.run(withCacheMetrics((cache, meterRegistry) -> {
String key = UUID.randomUUID().toString();
cache.put(key, "test");
cache.get(key);
cache.get(key);
assertThat(meterRegistry.get("cache.gets").tags(TAGS.and("result", "hit")).functionCounter().count())
.isEqualTo(2.0d);
}));
}
@Test
void cacheMissesAreExposed() {
this.contextRunner.run(withCacheMetrics((cache, meterRegistry) -> {
String key = UUID.randomUUID().toString();
cache.get(key);
cache.get(key);
cache.get(key);
assertThat(meterRegistry.get("cache.gets").tags(TAGS.and("result", "miss")).functionCounter().count())
.isEqualTo(3.0d);
}));
}
@Test
void cacheMetricsMatchCacheStatistics() {
this.contextRunner.run((context) -> {
RedisCache cache = getTestCache(context);
RedisCacheMetrics cacheMetrics = new RedisCacheMetrics(cache, TAGS);
assertThat(cacheMetrics.hitCount()).isEqualTo(cache.getStatistics().getHits());
assertThat(cacheMetrics.missCount()).isEqualTo(cache.getStatistics().getMisses());
assertThat(cacheMetrics.putCount()).isEqualTo(cache.getStatistics().getPuts());
assertThat(cacheMetrics.size()).isNull();
assertThat(cacheMetrics.evictionCount()).isNull();
});
}
private ContextConsumer<AssertableApplicationContext> withCacheMetrics(
BiConsumer<RedisCache, MeterRegistry> stats) {
return (context) -> {
RedisCache cache = getTestCache(context);
SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry();
new RedisCacheMetrics(cache, Tags.of("app", "test")).bindTo(meterRegistry);
stats.accept(cache, meterRegistry);
};
}
private RedisCache getTestCache(AssertableApplicationContext context) {
assertThat(context).hasSingleBean(RedisCacheManager.class);
RedisCacheManager cacheManager = context.getBean(RedisCacheManager.class);
RedisCache cache = (RedisCache) cacheManager.getCache("test");
assertThat(cache).isNotNull();
return cache;
}
@Configuration(proxyBeanMethods = false)
@EnableCaching
static class CachingConfiguration {
}
}

View File

@ -257,6 +257,11 @@ public class CacheProperties {
*/
private boolean useKeyPrefix = true;
/**
* Whether to enable cache statistics.
*/
private boolean enableStatistics;
public Duration getTimeToLive() {
return this.timeToLive;
}
@ -289,6 +294,13 @@ public class CacheProperties {
this.useKeyPrefix = useKeyPrefix;
}
public boolean isEnableStatistics() {
return this.enableStatistics;
}
public void setEnableStatistics(boolean enableStatistics) {
this.enableStatistics = enableStatistics;
}
}
}

View File

@ -63,6 +63,9 @@ class RedisCacheConfiguration {
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
if (cacheProperties.getRedis().isEnableStatistics()) {
builder.enableStatistics();
}
redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return cacheManagerCustomizers.customize(builder.build());
}

View File

@ -2303,6 +2303,7 @@ The following cache libraries are supported:
* EhCache 2
* Hazelcast
* Any compliant JCache (JSR-107) implementation
* Redis
Metrics are tagged by the name of the cache and by the name of the `CacheManager` that is derived from the bean name.