From 48187ee6eca75de6747bc2778abd4244f3a18707 Mon Sep 17 00:00:00 2001 From: jenedgar Date: Tue, 28 Jul 2026 15:47:59 +0100 Subject: [PATCH 1/8] CCD-5958: cache methods --- .../gov/hmcts/ccd/SecurityConfiguration.java | 6 +- .../hmcts/ccd/config/CacheConfiguration.java | 3 +- .../CaffeineCacheRateMetricsBinder.java | 71 +++++++++++++++++++ src/main/resources/application.properties | 2 +- .../uk/gov/hmcts/ccd/ActuatorSecurityIT.java | 50 +++++++++++++ 5 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java diff --git a/src/main/java/uk/gov/hmcts/ccd/SecurityConfiguration.java b/src/main/java/uk/gov/hmcts/ccd/SecurityConfiguration.java index ea5f0ea452..adfe8bd44d 100644 --- a/src/main/java/uk/gov/hmcts/ccd/SecurityConfiguration.java +++ b/src/main/java/uk/gov/hmcts/ccd/SecurityConfiguration.java @@ -67,7 +67,9 @@ public class SecurityConfiguration { "/webjars/**", "/v2/api-docs", "/v2/api-docs/**", - "/testing-support/cleanup-case-type/**" + "/testing-support/cleanup-case-type/**", + "/metrics", + "/metrics/**" }; @Inject @@ -106,7 +108,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(csrf -> csrf.disable()) // NOSONAR - CSRF is disabled purposely .formLogin(fl -> fl.disable()) .logout(logout -> logout.disable()) - .authorizeHttpRequests(auth -> + .authorizeHttpRequests(auth -> auth.requestMatchers("/error") .permitAll() .anyRequest() diff --git a/src/main/java/uk/gov/hmcts/ccd/config/CacheConfiguration.java b/src/main/java/uk/gov/hmcts/ccd/config/CacheConfiguration.java index fa675b989a..7f6ce5d8d7 100644 --- a/src/main/java/uk/gov/hmcts/ccd/config/CacheConfiguration.java +++ b/src/main/java/uk/gov/hmcts/ccd/config/CacheConfiguration.java @@ -107,8 +107,9 @@ private CaffeineCache buildCache(String cacheName, int ttl, int maxIdle) { cacheBuilder.maximumSize(applicationParams.getDefaultCacheMaxSize()); log.debug("creating custom cache: name='{}'", cacheName); + // Build one stats-enabled cache instance and register the same instance with the cache manager. CaffeineCache caffeineCache = new CaffeineCache(cacheName, cacheBuilder.recordStats().build()); - caffeineCacheManager.registerCustomCache(cacheName, cacheBuilder.build()); + caffeineCacheManager.registerCustomCache(cacheName, caffeineCache.getNativeCache()); log.debug("registering custom cache: name='{}'", cacheName); return caffeineCache; diff --git a/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java b/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java new file mode 100644 index 0000000000..f63e12dfba --- /dev/null +++ b/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java @@ -0,0 +1,71 @@ +package uk.gov.hmcts.ccd.config; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.MeterBinder; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.stereotype.Component; + +@Component +public class CaffeineCacheRateMetricsBinder implements MeterBinder { + + private final CacheManager cacheManager; + + public CaffeineCacheRateMetricsBinder(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + @Override + public void bindTo(MeterRegistry registry) { + cacheManager.getCacheNames().forEach(cacheName -> { + Cache cache = cacheManager.getCache(cacheName); + if (cache instanceof CaffeineCache caffeineCache) { + com.github.benmanes.caffeine.cache.Cache nativeCache = caffeineCache.getNativeCache(); + + FunctionCounter.builder("cache.hits", nativeCache, c -> c.stats().hitCount()) + .description("The number of cache hits") + .tag("cache", cacheName) + .register(registry); + + FunctionCounter.builder("cache.misses", nativeCache, c -> c.stats().missCount()) + .description("The number of cache misses") + .tag("cache", cacheName) + .register(registry); + + FunctionCounter.builder("cache.loads", nativeCache, c -> c.stats().loadCount()) + .description("The number of cache loads") + .tag("cache", cacheName) + .register(registry); + + FunctionCounter.builder("cache.average", nativeCache, c -> c.stats().loadCount()) + .description("The number of cache average") + .tag("cache", cacheName) + .register(registry); + + FunctionCounter.builder("cache.of", nativeCache, c -> c.stats().loadCount()) + .description("The number of cache of") + .tag("cache", cacheName) + .register(registry); + + + FunctionCounter.builder("cache.eviction", nativeCache, c -> c.stats().loadCount()) + .description("The number of cache eviction") + .tag("cache", cacheName) + .register(registry); + + FunctionCounter.builder("cache.eviction", nativeCache, c -> c.stats().loadCount()) + .description("The number of cache eviction") + .tag("cache", cacheName) + .register(registry); + + Gauge.builder("cache.hitrate", nativeCache, c -> c.stats().hitRate()) + .description("The cache hit rate") + .tag("cache", cacheName) + .register(registry); + } + }); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 67c9733f40..8aa994dc33 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -124,7 +124,7 @@ ccd.defaultPrintType=CCD Print Type management.server.servlet.context-path=/ # server under root instead of /actuator/* management.endpoints.web.base-path=/ -management.endpoints.web.exposure.include=health,info +management.endpoints.web.exposure.include=health,info,metrics # Explicitly disable the loggers actuator endpoint to prevent runtime log-level manipulation management.endpoint.loggers.enabled=false diff --git a/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java b/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java index 78b2109068..953504a376 100644 --- a/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java +++ b/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java @@ -9,6 +9,7 @@ import org.springframework.web.context.WebApplicationContext; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; class ActuatorSecurityIT extends WireMockBaseTest { @@ -38,4 +39,53 @@ void shouldNotExposeLoggersEndpointAnonymously() throws Exception { assertTrue(status == 401 || status == 404, "Expected /loggers to be protected (401) or disabled (404), but got: " + status); } + + @Test + void shouldAllowAnonymousAccessToMetricsEndpoint() throws Exception { + mockMvc.perform(get("/metrics")) + .andExpect(status().isOk()); + } + + @Test + void shouldExposeCacheHitMissAndPutMetrics() throws Exception { + // Exercise a known cache to generate put/hit/miss stats. + var cache = cacheManager.getCache("userInfoCache"); + assertTrue(cache != null, "Expected userInfoCache to exist"); + + cache.put("metrics-test-key", "metrics-test-value"); + cache.get("metrics-test-key"); // hit + cache.get("metrics-test-miss"); // miss + + MvcResult getsMetricResult = mockMvc.perform(get("/metrics/cache.gets")) + .andExpect(status().isOk()) + .andReturn(); + String getsMetricResponse = getsMetricResult.getResponse().getContentAsString(); + assertTrue(getsMetricResponse.contains("\"result\""), + "Expected cache.gets metric to include hit/miss result tags"); + assertTrue(getsMetricResponse.contains("hit") || getsMetricResponse.contains("miss"), + "Expected cache.gets metric to include hit or miss tags"); + + mockMvc.perform(get("/metrics/cache.hits")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/metrics/cache.misses")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/metrics/cache.loads")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/metrics/cache.eviction")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/metrics/cache.average")) + .andExpect(status().isOk()); + mockMvc.perform(get("/metrics/cache.of")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/metrics/cache.hitrate")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/metrics/cache.puts")) + .andExpect(status().isOk()); + } } From cf492e35164c1a65f3578585e1fb0a70a33625c3 Mon Sep 17 00:00:00 2001 From: jenedgar Date: Wed, 29 Jul 2026 16:53:51 +0100 Subject: [PATCH 2/8] CCD-5958: adding applicationinsights and updating messages --- lib/applicationinsights.json | 11 ++++ .../hmcts/ccd/config/CacheConfiguration.java | 1 - .../CaffeineCacheRateMetricsBinder.java | 52 ++++++++++++------- .../uk/gov/hmcts/ccd/ActuatorSecurityIT.java | 20 ++++--- 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/lib/applicationinsights.json b/lib/applicationinsights.json index 88ff5ee711..b75a1068fa 100644 --- a/lib/applicationinsights.json +++ b/lib/applicationinsights.json @@ -16,6 +16,17 @@ } ], "percentage": 1 + }, + { + "telemetryType": "request", + "attributes": [ + { + "key": "http.url", + "value": "https?://[^/]+/metrics.*", + "matchType": "regexp" + } + ], + "percentage": 100 } ] } diff --git a/src/main/java/uk/gov/hmcts/ccd/config/CacheConfiguration.java b/src/main/java/uk/gov/hmcts/ccd/config/CacheConfiguration.java index 7f6ce5d8d7..92c4275c7e 100644 --- a/src/main/java/uk/gov/hmcts/ccd/config/CacheConfiguration.java +++ b/src/main/java/uk/gov/hmcts/ccd/config/CacheConfiguration.java @@ -107,7 +107,6 @@ private CaffeineCache buildCache(String cacheName, int ttl, int maxIdle) { cacheBuilder.maximumSize(applicationParams.getDefaultCacheMaxSize()); log.debug("creating custom cache: name='{}'", cacheName); - // Build one stats-enabled cache instance and register the same instance with the cache manager. CaffeineCache caffeineCache = new CaffeineCache(cacheName, cacheBuilder.recordStats().build()); caffeineCacheManager.registerCustomCache(cacheName, caffeineCache.getNativeCache()); log.debug("registering custom cache: name='{}'", cacheName); diff --git a/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java b/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java index f63e12dfba..a34b0d8796 100644 --- a/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java +++ b/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java @@ -12,6 +12,7 @@ @Component public class CaffeineCacheRateMetricsBinder implements MeterBinder { + public static final String CACHE = "cache"; private final CacheManager cacheManager; public CaffeineCacheRateMetricsBinder(CacheManager cacheManager) { @@ -26,44 +27,55 @@ public void bindTo(MeterRegistry registry) { com.github.benmanes.caffeine.cache.Cache nativeCache = caffeineCache.getNativeCache(); FunctionCounter.builder("cache.hits", nativeCache, c -> c.stats().hitCount()) - .description("The number of cache hits") - .tag("cache", cacheName) + .description("The number of cache Hit Count") + .tag(CACHE, cacheName) .register(registry); + FunctionCounter.builder("cache.hits.rate", nativeCache, c -> c.stats().hitRate()) + .description("The number of cache Hit Rate") + .tag(CACHE, cacheName) + .register(registry); + + FunctionCounter.builder("cache.misses", nativeCache, c -> c.stats().missCount()) - .description("The number of cache misses") - .tag("cache", cacheName) + .description("The number of cache Miss Count") + .tag(CACHE, cacheName) .register(registry); - FunctionCounter.builder("cache.loads", nativeCache, c -> c.stats().loadCount()) - .description("The number of cache loads") - .tag("cache", cacheName) + FunctionCounter.builder("cache.misses.rate", nativeCache, c -> c.stats().missRate()) + .description("The number of cache Miss Rate") + .tag(CACHE, cacheName) .register(registry); - FunctionCounter.builder("cache.average", nativeCache, c -> c.stats().loadCount()) - .description("The number of cache average") - .tag("cache", cacheName) + FunctionCounter.builder("cache.load.count", nativeCache, c -> c.stats().loadCount()) + .description("The number of cache Load Count") + .tag(CACHE, cacheName) .register(registry); - FunctionCounter.builder("cache.of", nativeCache, c -> c.stats().loadCount()) - .description("The number of cache of") - .tag("cache", cacheName) + FunctionCounter.builder("cache.average.load.penalty", nativeCache, c -> c.stats().averageLoadPenalty()) + .description("The number of cache Average Load Penalty") + .tag(CACHE, cacheName) .register(registry); + FunctionCounter.builder("cache.eviction.count", nativeCache, c -> c.stats().evictionCount()) + .description("The number of cache Eviction Count") + .tag(CACHE, cacheName) + .register(registry); - FunctionCounter.builder("cache.eviction", nativeCache, c -> c.stats().loadCount()) - .description("The number of cache eviction") - .tag("cache", cacheName) + FunctionCounter.builder("cache.eviction.weight", nativeCache, c -> c.stats().evictionWeight()) + .description("The number of cache Eviction Weight") + .tag(CACHE, cacheName) .register(registry); - FunctionCounter.builder("cache.eviction", nativeCache, c -> c.stats().loadCount()) - .description("The number of cache eviction") - .tag("cache", cacheName) + FunctionCounter.builder("cache.request.count", nativeCache, c -> c.stats().requestCount()) + .description("The number of cache Request Count") + .tag(CACHE, cacheName) .register(registry); + Gauge.builder("cache.hitrate", nativeCache, c -> c.stats().hitRate()) .description("The cache hit rate") - .tag("cache", cacheName) + .tag(CACHE, cacheName) .register(registry); } }); diff --git a/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java b/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java index 953504a376..5903354915 100644 --- a/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java +++ b/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java @@ -68,24 +68,32 @@ void shouldExposeCacheHitMissAndPutMetrics() throws Exception { mockMvc.perform(get("/metrics/cache.hits")) .andExpect(status().isOk()); + mockMvc.perform(get("/metrics/cache.hits.rate")) + .andExpect(status().isOk()); + mockMvc.perform(get("/metrics/cache.misses")) .andExpect(status().isOk()); - mockMvc.perform(get("/metrics/cache.loads")) + mockMvc.perform(get("/metrics/cache.misses.rate")) .andExpect(status().isOk()); - mockMvc.perform(get("/metrics/cache.eviction")) + mockMvc.perform(get("/metrics/cache.load.count")) .andExpect(status().isOk()); - mockMvc.perform(get("/metrics/cache.average")) + mockMvc.perform(get("/metrics/cache.average.load.penalty")) .andExpect(status().isOk()); - mockMvc.perform(get("/metrics/cache.of")) + + mockMvc.perform(get("/metrics/cache.eviction.count")) .andExpect(status().isOk()); - mockMvc.perform(get("/metrics/cache.hitrate")) + mockMvc.perform(get("/metrics/cache.eviction.weight")) .andExpect(status().isOk()); - mockMvc.perform(get("/metrics/cache.puts")) + mockMvc.perform(get("/metrics/cache.request.count")) .andExpect(status().isOk()); + + mockMvc.perform(get("/metrics/cache.hitrate")) + .andExpect(status().isOk()); + } } From de6959a2d8f3dddb10b4fc789588f9348d2c5abe Mon Sep 17 00:00:00 2001 From: jenedgar Date: Thu, 30 Jul 2026 11:25:22 +0100 Subject: [PATCH 3/8] CCD-5958: Updating messages and inserting put --- .../CaffeineCacheRateMetricsBinder.java | 23 +++++++++---------- .../uk/gov/hmcts/ccd/ActuatorSecurityIT.java | 5 +++- .../CaffeineCacheRateMetricsBinderTest.java | 0 3 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java diff --git a/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java b/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java index a34b0d8796..7eae56d657 100644 --- a/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java +++ b/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java @@ -1,9 +1,9 @@ package uk.gov.hmcts.ccd.config; import io.micrometer.core.instrument.FunctionCounter; -import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; +import org.jspecify.annotations.NonNull; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.caffeine.CaffeineCache; @@ -20,7 +20,7 @@ public CaffeineCacheRateMetricsBinder(CacheManager cacheManager) { } @Override - public void bindTo(MeterRegistry registry) { + public void bindTo(@NonNull MeterRegistry registry) { cacheManager.getCacheNames().forEach(cacheName -> { Cache cache = cacheManager.getCache(cacheName); if (cache instanceof CaffeineCache caffeineCache) { @@ -36,47 +36,46 @@ public void bindTo(MeterRegistry registry) { .tag(CACHE, cacheName) .register(registry); - FunctionCounter.builder("cache.misses", nativeCache, c -> c.stats().missCount()) .description("The number of cache Miss Count") .tag(CACHE, cacheName) .register(registry); FunctionCounter.builder("cache.misses.rate", nativeCache, c -> c.stats().missRate()) - .description("The number of cache Miss Rate") + .description("The number of cache miss rate") .tag(CACHE, cacheName) .register(registry); FunctionCounter.builder("cache.load.count", nativeCache, c -> c.stats().loadCount()) - .description("The number of cache Load Count") + .description("The number of cache load count") .tag(CACHE, cacheName) .register(registry); FunctionCounter.builder("cache.average.load.penalty", nativeCache, c -> c.stats().averageLoadPenalty()) - .description("The number of cache Average Load Penalty") + .description("The number of cache average load penalty") .tag(CACHE, cacheName) .register(registry); FunctionCounter.builder("cache.eviction.count", nativeCache, c -> c.stats().evictionCount()) - .description("The number of cache Eviction Count") + .description("The number of cache eviction count") .tag(CACHE, cacheName) .register(registry); FunctionCounter.builder("cache.eviction.weight", nativeCache, c -> c.stats().evictionWeight()) - .description("The number of cache Eviction Weight") + .description("The number of cache eviction weight") .tag(CACHE, cacheName) .register(registry); FunctionCounter.builder("cache.request.count", nativeCache, c -> c.stats().requestCount()) - .description("The number of cache Request Count") + .description("The number of cache request count") .tag(CACHE, cacheName) .register(registry); - - Gauge.builder("cache.hitrate", nativeCache, c -> c.stats().hitRate()) - .description("The cache hit rate") + FunctionCounter.builder("cache.puts", nativeCache, c -> c.stats().loadSuccessCount()) + .description("The number of cache puts (load success count)") .tag(CACHE, cacheName) .register(registry); + } }); } diff --git a/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java b/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java index 5903354915..96999af51a 100644 --- a/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java +++ b/src/test/java/uk/gov/hmcts/ccd/ActuatorSecurityIT.java @@ -92,7 +92,10 @@ void shouldExposeCacheHitMissAndPutMetrics() throws Exception { mockMvc.perform(get("/metrics/cache.request.count")) .andExpect(status().isOk()); - mockMvc.perform(get("/metrics/cache.hitrate")) + mockMvc.perform(get("/metrics/cache.puts")) + .andExpect(status().isOk()); + + mockMvc.perform(get("/metrics/cache.size")) .andExpect(status().isOk()); } diff --git a/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java b/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java new file mode 100644 index 0000000000..e69de29bb2 From 9d1686fea6a8a4f5a9ae661b09145675280b05a5 Mon Sep 17 00:00:00 2001 From: jenedgar Date: Thu, 30 Jul 2026 14:36:42 +0100 Subject: [PATCH 4/8] CCD-5958: updated applicationinsights to include tab info --- lib/applicationinsights.json | 2 +- .../CaffeineCacheRateMetricsBinder.java | 5 ++ .../CaffeineCacheRateMetricsBinderTest.java | 80 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/lib/applicationinsights.json b/lib/applicationinsights.json index b75a1068fa..e0128939f3 100644 --- a/lib/applicationinsights.json +++ b/lib/applicationinsights.json @@ -22,7 +22,7 @@ "attributes": [ { "key": "http.url", - "value": "https?://[^/]+/metrics.*", + "value": "https?://[^/]+/metrics/[^?]+\\?tag=cache:[^&]+$", "matchType": "regexp" } ], diff --git a/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java b/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java index 7eae56d657..55a5378389 100644 --- a/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java +++ b/src/main/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinder.java @@ -26,6 +26,11 @@ public void bindTo(@NonNull MeterRegistry registry) { if (cache instanceof CaffeineCache caffeineCache) { com.github.benmanes.caffeine.cache.Cache nativeCache = caffeineCache.getNativeCache(); + FunctionCounter.builder("cache.hits", nativeCache, c -> c.stats().hitCount()) + .description("The number of cache Hit Count") + .tag(CACHE, cacheName) + .register(registry); + FunctionCounter.builder("cache.hits", nativeCache, c -> c.stats().hitCount()) .description("The number of cache Hit Count") .tag(CACHE, cacheName) diff --git a/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java b/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java index e69de29bb2..5104472232 100644 --- a/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java @@ -0,0 +1,80 @@ +package uk.gov.hmcts.ccd.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.cache.Cache; +import org.springframework.cache.caffeine.CaffeineCacheManager; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayName("CaffeineCacheRateMetricsBinder") +class CaffeineCacheRateMetricsBinderTest { + + @Test + @DisplayName("should register both per-cache and overall metrics") + void shouldRegisterPerCacheAndOverallMetrics() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCaffeine(Caffeine.newBuilder().recordStats()); + cacheManager.setCacheNames(List.of("cacheOne", "cacheTwo")); + + Cache cacheOne = cacheManager.getCache("cacheOne"); + Cache cacheTwo = cacheManager.getCache("cacheTwo"); + + cacheOne.put("k1", "v1"); + cacheOne.get("k1"); + cacheOne.get("missing"); + + cacheTwo.put("k2", "v2"); + cacheTwo.get("k2"); + cacheTwo.get("missing"); + + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + new CaffeineCacheRateMetricsBinder(cacheManager).bindTo(registry); + + double hitsCacheOne = registry.get("cache.hits").tag("cache", "cacheOne").functionCounter().count(); + double hitsCacheTwo = registry.get("cache.hits").tag("cache", "cacheTwo").functionCounter().count(); + double hitsOverall = untaggedCounterValue(registry, "cache.hits"); + + double missesCacheOne = registry.get("cache.misses").tag("cache", "cacheOne").functionCounter().count(); + double missesCacheTwo = registry.get("cache.misses").tag("cache", "cacheTwo").functionCounter().count(); + double missesOverall = untaggedCounterValue(registry, "cache.misses"); + + double putsCacheOne = registry.get("cache.puts").tag("cache", "cacheOne").functionCounter().count(); + double putsCacheTwo = registry.get("cache.puts").tag("cache", "cacheTwo").functionCounter().count(); + double putsOverall = untaggedCounterValue(registry, "cache.puts"); + + double requestsOverall = untaggedCounterValue(registry, "cache.request.count"); + double hitRateOverall = untaggedGaugeValue(registry, "cache.hits.rate"); + + assertAll( + () -> assertEquals(hitsCacheOne + hitsCacheTwo, hitsOverall, 0.0001D), + () -> assertEquals(missesCacheOne + missesCacheTwo, missesOverall, 0.0001D), + () -> assertEquals(putsCacheOne + putsCacheTwo, putsOverall, 0.0001D), + () -> assertEquals(hitsOverall / requestsOverall, hitRateOverall, 0.0001D) + ); + } + + private double untaggedCounterValue(SimpleMeterRegistry registry, String meterName) { + return registry.getMeters().stream() + .filter(m -> meterName.equals(m.getId().getName()) && m.getId().getTags().isEmpty()) + .findFirst() + .map(m -> ((FunctionCounter) m).count()) + .orElseThrow(); + } + + private double untaggedGaugeValue(SimpleMeterRegistry registry, String meterName) { + return registry.getMeters().stream() + .filter(m -> meterName.equals(m.getId().getName()) && m.getId().getTags().isEmpty()) + .findFirst() + .map(m -> ((Gauge) m).value()) + .orElseThrow(); + } +} + From 02833e9e7a3e4c547761d6056817e82d9f7866a1 Mon Sep 17 00:00:00 2001 From: jenedgar Date: Fri, 31 Jul 2026 09:39:23 +0100 Subject: [PATCH 5/8] CCD-5958: updated tests --- .../CaffeineCacheRateMetricsBinderTest.java | 116 +++++++++++------- 1 file changed, 75 insertions(+), 41 deletions(-) diff --git a/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java b/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java index 5104472232..de96bba5b2 100644 --- a/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java @@ -1,80 +1,114 @@ package uk.gov.hmcts.ccd.config; import com.github.benmanes.caffeine.cache.Caffeine; -import io.micrometer.core.instrument.FunctionCounter; -import io.micrometer.core.instrument.Gauge; +import com.github.benmanes.caffeine.cache.stats.CacheStats; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.cache.caffeine.CaffeineCacheManager; import java.util.List; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @DisplayName("CaffeineCacheRateMetricsBinder") class CaffeineCacheRateMetricsBinderTest { + public static final String CACHE_ONE = "cacheOne"; + public static final String CACHE_TWO = "cacheTwo"; + public static final String CACHE = "cache"; + private static final double DELTA = 0.000001D; + + private CaffeineCacheRateMetricsBinder classUnderTest; + private CaffeineCacheManager cacheManager; + + @BeforeEach + void setUp() { + cacheManager = new CaffeineCacheManager(); + classUnderTest = new CaffeineCacheRateMetricsBinder(cacheManager); + } + @Test - @DisplayName("should register both per-cache and overall metrics") - void shouldRegisterPerCacheAndOverallMetrics() { - CaffeineCacheManager cacheManager = new CaffeineCacheManager(); - cacheManager.setCaffeine(Caffeine.newBuilder().recordStats()); - cacheManager.setCacheNames(List.of("cacheOne", "cacheTwo")); + @DisplayName("should register all supported metrics") + void shouldRegisterAllMetrics() { + cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(1).recordStats()); + cacheManager.setCacheNames(List.of(CACHE_ONE, CACHE_TWO)); - Cache cacheOne = cacheManager.getCache("cacheOne"); - Cache cacheTwo = cacheManager.getCache("cacheTwo"); + CaffeineCache cacheOne = (CaffeineCache) cacheManager.getCache(CACHE_ONE); + CaffeineCache cacheTwo = (CaffeineCache) cacheManager.getCache(CACHE_TWO); cacheOne.put("k1", "v1"); cacheOne.get("k1"); cacheOne.get("missing"); + cacheOne.get("load", () -> "loaded"); + cacheOne.get("load"); + cacheOne.put("evict-1", "value-1"); + cacheOne.put("evict-2", "value-2"); cacheTwo.put("k2", "v2"); cacheTwo.get("k2"); cacheTwo.get("missing"); + cacheTwo.get("load", () -> "loaded"); + cacheTwo.get("load"); + cacheTwo.put("evict-3", "value-3"); + cacheTwo.put("evict-4", "value-4"); SimpleMeterRegistry registry = new SimpleMeterRegistry(); - new CaffeineCacheRateMetricsBinder(cacheManager).bindTo(registry); - - double hitsCacheOne = registry.get("cache.hits").tag("cache", "cacheOne").functionCounter().count(); - double hitsCacheTwo = registry.get("cache.hits").tag("cache", "cacheTwo").functionCounter().count(); - double hitsOverall = untaggedCounterValue(registry, "cache.hits"); - - double missesCacheOne = registry.get("cache.misses").tag("cache", "cacheOne").functionCounter().count(); - double missesCacheTwo = registry.get("cache.misses").tag("cache", "cacheTwo").functionCounter().count(); - double missesOverall = untaggedCounterValue(registry, "cache.misses"); + classUnderTest.bindTo(registry); - double putsCacheOne = registry.get("cache.puts").tag("cache", "cacheOne").functionCounter().count(); - double putsCacheTwo = registry.get("cache.puts").tag("cache", "cacheTwo").functionCounter().count(); - double putsOverall = untaggedCounterValue(registry, "cache.puts"); - - double requestsOverall = untaggedCounterValue(registry, "cache.request.count"); - double hitRateOverall = untaggedGaugeValue(registry, "cache.hits.rate"); + CacheStats cacheOneStats = cacheOne.getNativeCache().stats(); + CacheStats cacheTwoStats = cacheTwo.getNativeCache().stats(); assertAll( - () -> assertEquals(hitsCacheOne + hitsCacheTwo, hitsOverall, 0.0001D), - () -> assertEquals(missesCacheOne + missesCacheTwo, missesOverall, 0.0001D), - () -> assertEquals(putsCacheOne + putsCacheTwo, putsOverall, 0.0001D), - () -> assertEquals(hitsOverall / requestsOverall, hitRateOverall, 0.0001D) + () -> assertEquals(cacheOneStats.hitCount(), metricValue(registry, "cache.hits", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.hitCount(), metricValue(registry, "cache.hits", CACHE_TWO), DELTA), + () -> assertEquals(cacheOneStats.hitRate(), metricValue(registry, "cache.hits.rate", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.hitRate(), metricValue(registry, "cache.hits.rate", CACHE_TWO), DELTA), + () -> assertEquals(cacheOneStats.missCount(), metricValue(registry, "cache.misses", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.missCount(), metricValue(registry, "cache.misses", CACHE_TWO), DELTA), + () -> assertEquals(cacheOneStats.missRate(), metricValue(registry, "cache.misses.rate", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.missRate(), metricValue(registry, "cache.misses.rate", CACHE_TWO), DELTA), + () -> assertEquals(cacheOneStats.loadCount(), metricValue(registry, "cache.load.count", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.loadCount(), metricValue(registry, "cache.load.count", CACHE_TWO), DELTA), + () -> assertEquals(cacheOneStats.averageLoadPenalty(), + metricValue(registry, "cache.average.load.penalty", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.averageLoadPenalty(), + metricValue(registry, "cache.average.load.penalty", CACHE_TWO), DELTA), + () -> assertEquals(cacheOneStats.evictionCount(), metricValue(registry, "cache.eviction.count", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.evictionCount(), metricValue(registry, "cache.eviction.count", CACHE_TWO), DELTA), + () -> assertEquals(cacheOneStats.evictionWeight(), metricValue(registry, "cache.eviction.weight", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.evictionWeight(), metricValue(registry, "cache.eviction.weight", CACHE_TWO), DELTA), + () -> assertEquals(cacheOneStats.requestCount(), metricValue(registry, "cache.request.count", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.requestCount(), metricValue(registry, "cache.request.count", CACHE_TWO), DELTA), + () -> assertEquals(cacheOneStats.loadSuccessCount(), metricValue(registry, "cache.puts", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.loadSuccessCount(), metricValue(registry, "cache.puts", CACHE_TWO), DELTA), + // Duplicate cache.hits registration should still resolve to a single meter per cache tag. + () -> assertEquals(20, registry.getMeters().size()) ); } - private double untaggedCounterValue(SimpleMeterRegistry registry, String meterName) { - return registry.getMeters().stream() - .filter(m -> meterName.equals(m.getId().getName()) && m.getId().getTags().isEmpty()) - .findFirst() - .map(m -> ((FunctionCounter) m).count()) - .orElseThrow(); + @Test + @DisplayName("should ignore non caffeine caches") + void shouldIgnoreNonCaffeineCaches() { + CacheManager cacheManager = mock(CacheManager.class); + when(cacheManager.getCacheNames()).thenReturn(List.of("simple")); + when(cacheManager.getCache("simple")).thenReturn(mock(Cache.class)); + + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + new CaffeineCacheRateMetricsBinder(cacheManager).bindTo(registry); + + assertTrue(registry.getMeters().isEmpty()); } - private double untaggedGaugeValue(SimpleMeterRegistry registry, String meterName) { - return registry.getMeters().stream() - .filter(m -> meterName.equals(m.getId().getName()) && m.getId().getTags().isEmpty()) - .findFirst() - .map(m -> ((Gauge) m).value()) - .orElseThrow(); + private double metricValue(SimpleMeterRegistry registry, String name, String cacheName) { + return registry.get(name).tag(CACHE, cacheName).functionCounter().count(); } } - From 6ce14265f161b5338dce123648068d6bc65616b8 Mon Sep 17 00:00:00 2001 From: jenedgar Date: Fri, 31 Jul 2026 09:39:23 +0100 Subject: [PATCH 6/8] CCD-5958: updated tests # Conflicts: # src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java --- .../CaffeineCacheRateMetricsBinderTest.java | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java b/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java index de96bba5b2..eab22cc113 100644 --- a/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java @@ -6,8 +6,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; + import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.cache.caffeine.CaffeineCacheManager; @@ -15,9 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; + @DisplayName("CaffeineCacheRateMetricsBinder") class CaffeineCacheRateMetricsBinderTest { @@ -42,8 +39,8 @@ void shouldRegisterAllMetrics() { cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(1).recordStats()); cacheManager.setCacheNames(List.of(CACHE_ONE, CACHE_TWO)); - CaffeineCache cacheOne = (CaffeineCache) cacheManager.getCache(CACHE_ONE); - CaffeineCache cacheTwo = (CaffeineCache) cacheManager.getCache(CACHE_TWO); + final CaffeineCache cacheOne = (CaffeineCache) cacheManager.getCache(CACHE_ONE); + final CaffeineCache cacheTwo = (CaffeineCache) cacheManager.getCache(CACHE_TWO); cacheOne.put("k1", "v1"); cacheOne.get("k1"); @@ -95,19 +92,6 @@ void shouldRegisterAllMetrics() { ); } - @Test - @DisplayName("should ignore non caffeine caches") - void shouldIgnoreNonCaffeineCaches() { - CacheManager cacheManager = mock(CacheManager.class); - when(cacheManager.getCacheNames()).thenReturn(List.of("simple")); - when(cacheManager.getCache("simple")).thenReturn(mock(Cache.class)); - - SimpleMeterRegistry registry = new SimpleMeterRegistry(); - new CaffeineCacheRateMetricsBinder(cacheManager).bindTo(registry); - - assertTrue(registry.getMeters().isEmpty()); - } - private double metricValue(SimpleMeterRegistry registry, String name, String cacheName) { return registry.get(name).tag(CACHE, cacheName).functionCounter().count(); } From 51e921e1c68177aea8c4b1bbd8df00cb6058bedb Mon Sep 17 00:00:00 2001 From: jenedgar Date: Fri, 31 Jul 2026 09:39:23 +0100 Subject: [PATCH 7/8] CCD-5958: updated tests --- .../CaffeineCacheRateMetricsBinderTest.java | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java b/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java index eab22cc113..2c81a759c2 100644 --- a/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/config/CaffeineCacheRateMetricsBinderTest.java @@ -77,16 +77,32 @@ void shouldRegisterAllMetrics() { () -> assertEquals(cacheTwoStats.loadCount(), metricValue(registry, "cache.load.count", CACHE_TWO), DELTA), () -> assertEquals(cacheOneStats.averageLoadPenalty(), metricValue(registry, "cache.average.load.penalty", CACHE_ONE), DELTA), + () -> assertEquals(cacheTwoStats.averageLoadPenalty(), metricValue(registry, "cache.average.load.penalty", CACHE_TWO), DELTA), - () -> assertEquals(cacheOneStats.evictionCount(), metricValue(registry, "cache.eviction.count", CACHE_ONE), DELTA), - () -> assertEquals(cacheTwoStats.evictionCount(), metricValue(registry, "cache.eviction.count", CACHE_TWO), DELTA), - () -> assertEquals(cacheOneStats.evictionWeight(), metricValue(registry, "cache.eviction.weight", CACHE_ONE), DELTA), - () -> assertEquals(cacheTwoStats.evictionWeight(), metricValue(registry, "cache.eviction.weight", CACHE_TWO), DELTA), - () -> assertEquals(cacheOneStats.requestCount(), metricValue(registry, "cache.request.count", CACHE_ONE), DELTA), - () -> assertEquals(cacheTwoStats.requestCount(), metricValue(registry, "cache.request.count", CACHE_TWO), DELTA), - () -> assertEquals(cacheOneStats.loadSuccessCount(), metricValue(registry, "cache.puts", CACHE_ONE), DELTA), - () -> assertEquals(cacheTwoStats.loadSuccessCount(), metricValue(registry, "cache.puts", CACHE_TWO), DELTA), + + () -> assertEquals(cacheOneStats.evictionCount(), + metricValue(registry, "cache.eviction.count", CACHE_ONE), DELTA), + + () -> assertEquals(cacheTwoStats.evictionCount(), + metricValue(registry, "cache.eviction.count", CACHE_TWO), DELTA), + + () -> assertEquals(cacheOneStats.evictionWeight(), + metricValue(registry, "cache.eviction.weight", CACHE_ONE), DELTA), + + () -> assertEquals(cacheTwoStats.evictionWeight(), + metricValue(registry, "cache.eviction.weight", CACHE_TWO), DELTA), + () -> assertEquals(cacheOneStats.requestCount(), + metricValue(registry, "cache.request.count", CACHE_ONE), DELTA), + + () -> assertEquals(cacheTwoStats.requestCount(), + metricValue(registry, "cache.request.count", CACHE_TWO), DELTA), + + () -> assertEquals(cacheOneStats.loadSuccessCount(), + metricValue(registry, "cache.puts", CACHE_ONE), DELTA), + + () -> assertEquals(cacheTwoStats.loadSuccessCount(), + metricValue(registry, "cache.puts", CACHE_TWO), DELTA), // Duplicate cache.hits registration should still resolve to a single meter per cache tag. () -> assertEquals(20, registry.getMeters().size()) ); From f04bb2a1a04f69bcc44462c7c840136924d948fe Mon Sep 17 00:00:00 2001 From: jenedgar Date: Fri, 31 Jul 2026 14:58:54 +0100 Subject: [PATCH 8/8] Trigger CI