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:
parent
a099cd9420
commit
34c4c3f235
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.EhCache2CacheMeterBinderProvider;
|
||||||
import org.springframework.boot.actuate.metrics.cache.HazelcastCacheMeterBinderProvider;
|
import org.springframework.boot.actuate.metrics.cache.HazelcastCacheMeterBinderProvider;
|
||||||
import org.springframework.boot.actuate.metrics.cache.JCacheCacheMeterBinderProvider;
|
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.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.cache.caffeine.CaffeineCache;
|
import org.springframework.cache.caffeine.CaffeineCache;
|
||||||
import org.springframework.cache.ehcache.EhCacheCache;
|
import org.springframework.cache.ehcache.EhCacheCache;
|
||||||
import org.springframework.cache.jcache.JCacheCache;
|
import org.springframework.cache.jcache.JCacheCache;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.cache.RedisCache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure {@link CacheMeterBinderProvider} beans.
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ dependencies {
|
||||||
testImplementation("org.skyscreamer:jsonassert")
|
testImplementation("org.skyscreamer:jsonassert")
|
||||||
testImplementation("org.springframework:spring-test")
|
testImplementation("org.springframework:spring-test")
|
||||||
testImplementation("com.squareup.okhttp3:mockwebserver")
|
testImplementation("com.squareup.okhttp3:mockwebserver")
|
||||||
|
testImplementation("org.testcontainers:junit-jupiter")
|
||||||
|
|
||||||
testRuntimeOnly("io.projectreactor.netty:reactor-netty-http")
|
testRuntimeOnly("io.projectreactor.netty:reactor-netty-http")
|
||||||
testRuntimeOnly("javax.xml.bind:jaxb-api")
|
testRuntimeOnly("javax.xml.bind:jaxb-api")
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -257,6 +257,11 @@ public class CacheProperties {
|
||||||
*/
|
*/
|
||||||
private boolean useKeyPrefix = true;
|
private boolean useKeyPrefix = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to enable cache statistics.
|
||||||
|
*/
|
||||||
|
private boolean enableStatistics;
|
||||||
|
|
||||||
public Duration getTimeToLive() {
|
public Duration getTimeToLive() {
|
||||||
return this.timeToLive;
|
return this.timeToLive;
|
||||||
}
|
}
|
||||||
|
|
@ -289,6 +294,13 @@ public class CacheProperties {
|
||||||
this.useKeyPrefix = useKeyPrefix;
|
this.useKeyPrefix = useKeyPrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isEnableStatistics() {
|
||||||
|
return this.enableStatistics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnableStatistics(boolean enableStatistics) {
|
||||||
|
this.enableStatistics = enableStatistics;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,9 @@ class RedisCacheConfiguration {
|
||||||
if (!cacheNames.isEmpty()) {
|
if (!cacheNames.isEmpty()) {
|
||||||
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
|
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
|
||||||
}
|
}
|
||||||
|
if (cacheProperties.getRedis().isEnableStatistics()) {
|
||||||
|
builder.enableStatistics();
|
||||||
|
}
|
||||||
redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||||
return cacheManagerCustomizers.customize(builder.build());
|
return cacheManagerCustomizers.customize(builder.build());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2303,6 +2303,7 @@ The following cache libraries are supported:
|
||||||
* EhCache 2
|
* EhCache 2
|
||||||
* Hazelcast
|
* Hazelcast
|
||||||
* Any compliant JCache (JSR-107) implementation
|
* 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.
|
Metrics are tagged by the name of the cache and by the name of the `CacheManager` that is derived from the bean name.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue