diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration.java index 76796747c7f..e756fc201cb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/cache/CacheMeterBinderProvidersConfiguration.java @@ -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(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index 7bb8fe658ec..538dfb459ff 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -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") diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProvider.java new file mode 100644 index 00000000000..a31aea3807e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProvider.java @@ -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 { + + @Override + public MeterBinder getMeterBinder(RedisCache cache, Iterable tags) { + return new RedisCacheMetrics(cache, tags); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetrics.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetrics.java new file mode 100644 index 00000000000..8ede30d9d8c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetrics.java @@ -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 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); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProviderTests.java new file mode 100644 index 00000000000..05656026860 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMeterBinderProviderTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetricsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetricsTests.java new file mode 100644 index 00000000000..38d6af150cf --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/cache/RedisCacheMetricsTests.java @@ -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 withCacheMetrics( + BiConsumer 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 { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java index 4ef13b702fd..6fc07e76b5c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java @@ -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; + } } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java index 5856716c809..5e14f7daa13 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java @@ -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()); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index 9d1d7989a02..80c0dc3136f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -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.