From bb65f8a195419596229e0c2e6a2cd72c7466dbd6 Mon Sep 17 00:00:00 2001 From: Oleksii Bondar Date: Wed, 2 Jan 2019 12:51:14 -0800 Subject: [PATCH] Revise cache metrics implementations + tests (#1107) --- .../io/micrometer/core/instrument/Tags.java | 1 - .../binder/cache/CacheMeterBinder.java | 1 - .../binder/cache/CaffeineCacheMetrics.java | 8 +- .../binder/cache/EhCache2Metrics.java | 5 +- .../binder/cache/GuavaCacheMetrics.java | 4 +- .../binder/cache/JCacheMetrics.java | 20 +- .../cache/AbstractCacheMetricsTest.java | 55 +++++ .../cache/CaffeineCacheMetricsTest.java | 114 ++++++++++ .../binder/cache/EhCache2MetricsTest.java | 197 ++++++++++++++++ .../binder/cache/GuavaCacheMetricsTest.java | 115 ++++++++++ .../cache/HazelcastCacheMetricsTest.java | 184 +++++++++++++++ .../binder/cache/JCacheMetricsTest.java | 213 ++++++++++++++++++ 12 files changed, 898 insertions(+), 19 deletions(-) create mode 100644 micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/AbstractCacheMetricsTest.java create mode 100644 micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetricsTest.java create mode 100644 micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/EhCache2MetricsTest.java create mode 100644 micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetricsTest.java create mode 100644 micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/HazelcastCacheMetricsTest.java create mode 100644 micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/JCacheMetricsTest.java diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java index bcc9491a2a..f572249ceb 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/Tags.java @@ -218,7 +218,6 @@ public static Tags of(@Nullable Iterable tags) { } else if (tags instanceof Tags) { return (Tags) tags; } else if (tags instanceof Collection) { - @SuppressWarnings("unchecked") Collection tagsCollection = (Collection) tags; return new Tags(tagsCollection.toArray(new Tag[0])); } else { diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinder.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinder.java index 0a8020acb5..bfe19477d3 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinder.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinder.java @@ -23,7 +23,6 @@ import java.lang.ref.WeakReference; - /** * A common base class for cache metrics that ensures that all caches are instrumented * with the same basic set of metrics while allowing for additional detail that is specific diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetrics.java index 7c80408676..1fd841aaed 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetrics.java @@ -67,7 +67,7 @@ public CaffeineCacheMetrics(Cache cache, String cacheName, Iterable t * @param The cache type. * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. */ - public static C monitor(MeterRegistry registry, C cache, String cacheName, String... tags) { + public static > C monitor(MeterRegistry registry, C cache, String cacheName, String... tags) { return monitor(registry, cache, cacheName, Tags.of(tags)); } @@ -83,7 +83,7 @@ public static C monitor(MeterRegistry registry, C cache, Strin * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. * @see CacheStats */ - public static C monitor(MeterRegistry registry, C cache, String cacheName, Iterable tags) { + public static > C monitor(MeterRegistry registry, C cache, String cacheName, Iterable tags) { new CaffeineCacheMetrics(cache, cacheName, tags).bindTo(registry); return cache; } @@ -99,7 +99,7 @@ public static C monitor(MeterRegistry registry, C cache, Strin * @param The cache type. * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. */ - public static C monitor(MeterRegistry registry, C cache, String cacheName, String... tags) { + public static > C monitor(MeterRegistry registry, C cache, String cacheName, String... tags) { return monitor(registry, cache, cacheName, Tags.of(tags)); } @@ -115,7 +115,7 @@ public static C monitor(MeterRegistry registry, C * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. * @see CacheStats */ - public static C monitor(MeterRegistry registry, C cache, String cacheName, Iterable tags) { + public static > C monitor(MeterRegistry registry, C cache, String cacheName, Iterable tags) { monitor(registry, cache.synchronous(), cacheName, tags); return cache; } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/EhCache2Metrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/EhCache2Metrics.java index b4f013a81d..73586bfd64 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/EhCache2Metrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/EhCache2Metrics.java @@ -89,6 +89,7 @@ protected long putCount() { @Override protected void bindImplementationSpecificMetrics(MeterRegistry registry) { Gauge.builder("cache.remoteSize", stats, StatisticsGateway::getRemoteSize) + .tags(getTagsWithCacheName()) .description("The number of entries held remotely in this cache") .register(registry); @@ -102,7 +103,7 @@ protected void bindImplementationSpecificMetrics(MeterRegistry registry) { .description("Cache puts resulting in a new key/value pair") .register(registry); - FunctionCounter.builder("cache.puts.added", stats, StatisticsGateway::cachePutAddedCount) + FunctionCounter.builder("cache.puts.added", stats, StatisticsGateway::cachePutUpdatedCount) .tags(getTagsWithCacheName()).tags("result", "updated") .description("Cache puts resulting in an updated value") .register(registry); @@ -112,7 +113,7 @@ protected void bindImplementationSpecificMetrics(MeterRegistry registry) { rollbackTransactionMetrics(registry); recoveryTransactionMetrics(registry); - Gauge.builder("cache.local.offheap.size", stats, StatisticsGateway::getLocalOffHeapSize) + Gauge.builder("cache.local.offheap.size", stats, StatisticsGateway::getLocalOffHeapSizeInBytes) .tags(getTagsWithCacheName()) .description("Local off-heap size") .baseUnit("bytes") diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetrics.java index 3d3c2cda4d..81ffad7fd7 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetrics.java @@ -44,7 +44,7 @@ public class GuavaCacheMetrics extends CacheMeterBinder { * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. * @see com.google.common.cache.CacheStats */ - public static C monitor(MeterRegistry registry, C cache, String cacheName, String... tags) { + public static > C monitor(MeterRegistry registry, C cache, String cacheName, String... tags) { return monitor(registry, cache, cacheName, Tags.of(tags)); } @@ -60,7 +60,7 @@ public static C monitor(MeterRegistry registry, C cache, Strin * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. * @see com.google.common.cache.CacheStats */ - public static C monitor(MeterRegistry registry, C cache, String cacheName, Iterable tags) { + public static > C monitor(MeterRegistry registry, C cache, String cacheName, Iterable tags) { new GuavaCacheMetrics(cache, cacheName, tags).bindTo(registry); return cache; } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/JCacheMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/JCacheMetrics.java index 2a3acf49bb..6dfaf027f7 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/JCacheMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/JCacheMetrics.java @@ -129,17 +129,19 @@ protected void bindImplementationSpecificMetrics(MeterRegistry registry) { } private Long lookupStatistic(String name) { - try { - List mBeanServers = MBeanServerFactory.findMBeanServer(null); - for (MBeanServer mBeanServer : mBeanServers) { - try { - return (Long) mBeanServer.getAttribute(objectName, name); - } catch (AttributeNotFoundException | InstanceNotFoundException ex) { - // did not find MBean, try the next server + if (objectName != null) { + try { + List mBeanServers = MBeanServerFactory.findMBeanServer(null); + for (MBeanServer mBeanServer : mBeanServers) { + try { + return (Long) mBeanServer.getAttribute(objectName, name); + } catch (AttributeNotFoundException | InstanceNotFoundException ex) { + // did not find MBean, try the next server + } } + } catch (MBeanException | ReflectionException ex) { + throw new IllegalStateException(ex); } - } catch (MBeanException | ReflectionException ex) { - throw new IllegalStateException(ex); } // didn't find the MBean in any servers diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/AbstractCacheMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/AbstractCacheMetricsTest.java new file mode 100644 index 0000000000..cc0a246425 --- /dev/null +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/AbstractCacheMetricsTest.java @@ -0,0 +1,55 @@ +/** + * Copyright 2018 Pivotal Software, Inc. + *

+ * 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 + *

+ * http://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 io.micrometer.core.instrument.binder.cache; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.search.RequiredSearch; + +/** + * @author Oleksii Bondar + */ +abstract class AbstractCacheMetricsTest { + + protected Tags expectedTag = Tags.of("app", "test"); + + /** + * Verifies base metrics presence + */ + protected void verifyCommonCacheMetrics(MeterRegistry meterRegistry, CacheMeterBinder meterBinder) { + meterRegistry.get("cache.puts").tags(expectedTag).functionCounter(); + meterRegistry.get("cache.gets").tags(expectedTag).tag("result", "hit").functionCounter(); + + if (meterBinder.size() != null) { + meterRegistry.get("cache.size").tags(expectedTag).gauge(); + } + if (meterBinder.missCount() != null) { + meterRegistry.get("cache.gets").tags(expectedTag).tag("result", "miss").functionCounter(); + } + if (meterBinder.evictionCount() != null) { + meterRegistry.get("cache.evictions").tags(expectedTag).functionCounter(); + } + } + + protected RequiredSearch fetch(MeterRegistry meterRegistry, String name) { + return meterRegistry.get(name).tags(expectedTag); + } + + protected RequiredSearch fetch(MeterRegistry meterRegistry, String name, Iterable tags) { + return fetch(meterRegistry, name).tags(tags); + } +} diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetricsTest.java new file mode 100644 index 0000000000..7bb6be3f09 --- /dev/null +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetricsTest.java @@ -0,0 +1,114 @@ +/** + * Copyright 2018 Pivotal Software, Inc. + *

+ * 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 + *

+ * http://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 io.micrometer.core.instrument.binder.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.github.benmanes.caffeine.cache.stats.CacheStats; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.TimeGauge; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CaffeineCacheMetrics}. + * + * @author Oleksii Bondar + */ +class CaffeineCacheMetricsTest extends AbstractCacheMetricsTest { + + private LoadingCache cache = Caffeine.newBuilder().build(new CacheLoader() { + public String load(String key) throws Exception { + return ""; + }; + }); + private CaffeineCacheMetrics metrics = new CaffeineCacheMetrics(cache, "testCache", expectedTag); + + @Test + void reportExpectedGeneralMetrics() { + MeterRegistry registry = new SimpleMeterRegistry(); + metrics.bindTo(registry); + + verifyCommonCacheMetrics(registry, metrics); + + Gauge evictionWeight = fetch(registry, "cache.eviction.weight").gauge(); + CacheStats stats = cache.stats(); + assertThat(evictionWeight.value()).isEqualTo(stats.evictionCount()); + + // specific to LoadingCache instance + TimeGauge loadDuration = fetch(registry, "cache.load.duration").timeGauge(); + assertThat(loadDuration.value()).isEqualTo(stats.totalLoadTime()); + + FunctionCounter successfulLoad = fetch(registry, "cache.load", Tags.of("result", "success")).functionCounter(); + assertThat(successfulLoad.count()).isEqualTo(stats.loadSuccessCount()); + + FunctionCounter failedLoad = fetch(registry, "cache.load", Tags.of("result", "failure")).functionCounter(); + assertThat(failedLoad.count()).isEqualTo(stats.loadFailureCount()); + } + + @Test + void constructInstanceViaStaticMethodMonitor() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + CaffeineCacheMetrics.monitor(meterRegistry, cache, "testCache", expectedTag); + + meterRegistry.get("cache.eviction.weight").tags(expectedTag).gauge(); + } + + @Test + void doNotReportMetricsForNonLoadingCache() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + Cache cache = Caffeine.newBuilder().build(); + CaffeineCacheMetrics metrics = new CaffeineCacheMetrics(cache, "testCache", expectedTag); + metrics.bindTo(meterRegistry); + + assertThat(meterRegistry.find("cache.load.duration").timeGauge()).isNull(); + } + + @Test + void returnCacheSize() { + assertThat(metrics.size()).isEqualTo(cache.estimatedSize()); + } + + @Test + void returnHitCount() { + assertThat(metrics.hitCount()).isEqualTo(cache.stats().hitCount()); + } + + @Test + void returnMissCount() { + assertThat(metrics.missCount()).isEqualTo(cache.stats().missCount()); + } + + @Test + void returnEvictionCount() { + assertThat(metrics.evictionCount()).isEqualTo(cache.stats().evictionCount()); + } + + @Test + void returnPutCount() { + assertThat(metrics.putCount()).isEqualTo(cache.stats().loadCount()); + } + +} diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/EhCache2MetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/EhCache2MetricsTest.java new file mode 100644 index 0000000000..486811c1c2 --- /dev/null +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/EhCache2MetricsTest.java @@ -0,0 +1,197 @@ +/** + * Copyright 2018 Pivotal Software, Inc. + *

+ * 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 + *

+ * http://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 io.micrometer.core.instrument.binder.cache; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +import java.util.Random; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.statistics.StatisticsGateway; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link EhCache2Metrics}. + * + * @author Oleksii Bondar + */ +class EhCache2MetricsTest extends AbstractCacheMetricsTest { + + private static CacheManager cacheManager; + private static Cache cache; + + private EhCache2Metrics metrics = new EhCache2Metrics(cache, expectedTag); + + @Test + void reportMetrics() { + MeterRegistry registry = new SimpleMeterRegistry(); + metrics.bindTo(registry); + + verifyCommonCacheMetrics(registry, metrics); + + StatisticsGateway stats = cache.getStatistics(); + + Gauge remoteSize = fetch(registry, "cache.remoteSize").gauge(); + assertThat(remoteSize.value()).isEqualTo(stats.getRemoteSize()); + + FunctionCounter cacheRemovals = fetch(registry, "cache.removals").functionCounter(); + assertThat(cacheRemovals.count()).isEqualTo(stats.cacheRemoveCount()); + + String cacheAdded = "cache.puts.added"; + FunctionCounter putsAdded = fetch(registry, cacheAdded, Tags.of("result", "added")).functionCounter(); + assertThat(putsAdded.count()).isEqualTo(stats.cachePutAddedCount()); + + FunctionCounter putsUpdated = fetch(registry, cacheAdded, Tags.of("result", "updated")).functionCounter(); + assertThat(putsUpdated.count()).isEqualTo(stats.cachePutUpdatedCount()); + + Gauge offHeapSize = fetch(registry, "cache.local.offheap.size").gauge(); + assertThat(offHeapSize.value()).isEqualTo(stats.getLocalOffHeapSizeInBytes()); + + Gauge heapSize = fetch(registry, "cache.local.heap.size").gauge(); + assertThat(heapSize.value()).isEqualTo(stats.getLocalHeapSizeInBytes()); + + Gauge diskSize = fetch(registry, "cache.local.disk.size").gauge(); + assertThat(diskSize.value()).isEqualTo(stats.getLocalDiskSizeInBytes()); + + // miss metrics + String misses = "cache.misses"; + FunctionCounter expiredMisses = fetch(registry, misses, Tags.of("reason", "expired")).functionCounter(); + assertThat(expiredMisses.count()).isEqualTo(stats.cacheMissExpiredCount()); + + FunctionCounter notFoundMisses = fetch(registry, misses, Tags.of("reason", "notFound")).functionCounter(); + assertThat(notFoundMisses.count()).isEqualTo(stats.cacheMissNotFoundCount()); + + // commit transaction metrics + String xaCommits = "cache.xa.commits"; + FunctionCounter readOnlyCommits = fetch(registry, xaCommits, Tags.of("result", "readOnly")).functionCounter(); + assertThat(readOnlyCommits.count()).isEqualTo(stats.xaCommitReadOnlyCount()); + + FunctionCounter exceptionCommits = fetch(registry, xaCommits, Tags.of("result", "exception")).functionCounter(); + assertThat(exceptionCommits.count()).isEqualTo(stats.xaCommitExceptionCount()); + + FunctionCounter committedCommits = fetch(registry, xaCommits, Tags.of("result", "committed")).functionCounter(); + assertThat(committedCommits.count()).isEqualTo(stats.xaCommitCommittedCount()); + + // rollback transaction metrics + String xaRollbacks = "cache.xa.rollbacks"; + FunctionCounter exceptionRollback = fetch(registry, xaRollbacks, Tags.of("result", "exception")) + .functionCounter(); + assertThat(exceptionRollback.count()).isEqualTo(stats.xaRollbackExceptionCount()); + + FunctionCounter successRollback = fetch(registry, xaRollbacks, Tags.of("result", "success")).functionCounter(); + assertThat(successRollback.count()).isEqualTo(stats.xaRollbackSuccessCount()); + + // recovery transaction metrics + String xaRecoveries = "cache.xa.recoveries"; + FunctionCounter nothingRecovered = fetch(registry, xaRecoveries, Tags.of("result", "nothing")) + .functionCounter(); + assertThat(nothingRecovered.count()).isEqualTo(stats.xaRecoveryNothingCount()); + + FunctionCounter successRecoveries = fetch(registry, xaRecoveries, Tags.of("result", "success")) + .functionCounter(); + assertThat(successRecoveries.count()).isEqualTo(stats.xaRecoveryRecoveredCount()); + } + + @Test + void constructInstanceViaStaticMethodMonitor() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + EhCache2Metrics.monitor(meterRegistry, cache, expectedTag); + + meterRegistry.get("cache.remoteSize").tags(expectedTag).gauge(); + } + + @Test + void returnCacheSize() { + StatisticsGateway stats = cache.getStatistics(); + assertThat(metrics.size()).isEqualTo(stats.getSize()); + } + + @Test + void returnEvictionCount() { + StatisticsGateway stats = cache.getStatistics(); + assertThat(metrics.evictionCount()).isEqualTo(stats.cacheEvictedCount()); + } + + @Test + void returnHitCount() { + StatisticsGateway stats = cache.getStatistics(); + assertThat(metrics.hitCount()).isEqualTo(stats.cacheHitCount()); + } + + @Test + void returnMissCount() { + StatisticsGateway stats = cache.getStatistics(); + assertThat(metrics.missCount()).isEqualTo(stats.cacheMissCount()); + } + + @Test + void returnPutCount() { + StatisticsGateway stats = cache.getStatistics(); + assertThat(metrics.putCount()).isEqualTo(stats.cachePutCount()); + } + + @BeforeAll + static void setup() { + cacheManager = CacheManager.newInstance(); + cacheManager.addCache("testCache"); + cache = cacheManager.getCache("testCache"); + StatisticsGateway stats = mock(StatisticsGateway.class); + // generate non-negative random value to address false-positives + int valueBound = 100000; + Random random = new Random(); + when(stats.getSize()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheEvictedCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheHitCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheMissCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cachePutCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.getRemoteSize()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheRemoveCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cachePutAddedCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cachePutUpdatedCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.getLocalOffHeapSizeInBytes()).thenReturn((long) random.nextInt(valueBound)); + when(stats.getLocalHeapSizeInBytes()).thenReturn((long) random.nextInt(valueBound)); + when(stats.getLocalDiskSizeInBytes()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheMissExpiredCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheMissNotFoundCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaCommitCommittedCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaCommitExceptionCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaCommitReadOnlyCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaRollbackExceptionCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaRollbackSuccessCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaRecoveryRecoveredCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaRecoveryNothingCount()).thenReturn((long) random.nextInt(valueBound)); + ReflectionTestUtils.setField(cache, "statistics", stats); + } + + @AfterAll + static void cleanup() { + cacheManager.removeAllCaches(); + } + +} diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetricsTest.java new file mode 100644 index 0000000000..dd67ee4abd --- /dev/null +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetricsTest.java @@ -0,0 +1,115 @@ +/** + * Copyright 2018 Pivotal Software, Inc. + *

+ * 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 + *

+ * http://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 io.micrometer.core.instrument.binder.cache; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.CacheStats; +import com.google.common.cache.LoadingCache; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.TimeGauge; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GuavaCacheMetrics}. + * + * @author Oleksii Bondar + */ +class GuavaCacheMetricsTest extends AbstractCacheMetricsTest { + + private LoadingCache cache = CacheBuilder.newBuilder().build(new CacheLoader() { + public String load(String key) throws Exception { + return ""; + }; + }); + private GuavaCacheMetrics metrics = new GuavaCacheMetrics(cache, "testCache", expectedTag); + + @Test + void reportExpectedMetrics() { + MeterRegistry registry = new SimpleMeterRegistry(); + metrics.bindTo(registry); + + verifyCommonCacheMetrics(registry, metrics); + + // common metrics + Gauge cacheSize = fetch(registry, "cache.size").gauge(); + assertThat(cacheSize.value()).isEqualTo(cache.size()); + + FunctionCounter hitCount = fetch(registry, "cache.gets", Tags.of("result", "hit")).functionCounter(); + assertThat(hitCount.count()).isEqualTo(metrics.hitCount()); + + FunctionCounter missCount = fetch(registry, "cache.gets", Tags.of("result", "miss")).functionCounter(); + assertThat(missCount.count()).isEqualTo(metrics.missCount().doubleValue()); + + FunctionCounter cachePuts = fetch(registry, "cache.puts").functionCounter(); + assertThat(cachePuts.count()).isEqualTo(metrics.putCount()); + + FunctionCounter cacheEviction = fetch(registry, "cache.evictions").functionCounter(); + assertThat(cacheEviction.count()).isEqualTo(metrics.evictionCount().doubleValue()); + + CacheStats stats = cache.stats(); + TimeGauge loadDuration = fetch(registry, "cache.load.duration").timeGauge(); + assertThat(loadDuration.value()).isEqualTo(stats.totalLoadTime()); + + FunctionCounter successfulLoad = fetch(registry, "cache.load", Tags.of("result", "success")).functionCounter(); + assertThat(successfulLoad.count()).isEqualTo(stats.loadSuccessCount()); + + FunctionCounter failedLoad = fetch(registry, "cache.load", Tags.of("result", "failure")).functionCounter(); + assertThat(failedLoad.count()).isEqualTo(stats.loadExceptionCount()); + } + + @Test + void constructInstanceViaStaticMethodMonitor() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + GuavaCacheMetrics.monitor(meterRegistry, cache, "testCache", expectedTag); + + meterRegistry.get("cache.load.duration").tags(expectedTag).timeGauge(); + } + + @Test + void returnCacheSize() { + assertThat(metrics.size()).isEqualTo(cache.size()); + } + + @Test + void returnHitCount() { + assertThat(metrics.hitCount()).isEqualTo(cache.stats().hitCount()); + } + + @Test + void returnMissCount() { + assertThat(metrics.missCount()).isEqualTo(cache.stats().missCount()); + } + + @Test + void returnEvictionCount() { + assertThat(metrics.evictionCount()).isEqualTo(cache.stats().evictionCount()); + } + + @Test + void returnPutCount() { + assertThat(metrics.putCount()).isEqualTo(cache.stats().loadCount()); + } + +} diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/HazelcastCacheMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/HazelcastCacheMetricsTest.java new file mode 100644 index 0000000000..db487e3d5c --- /dev/null +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/HazelcastCacheMetricsTest.java @@ -0,0 +1,184 @@ +/** + * Copyright 2018 Pivotal Software, Inc. + *

+ * 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 + *

+ * http://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 io.micrometer.core.instrument.binder.cache; + +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.IMap; +import com.hazelcast.monitor.LocalMapStats; +import com.hazelcast.monitor.NearCacheStats; +import com.hazelcast.monitor.impl.LocalMapStatsImpl; +import com.hazelcast.monitor.impl.NearCacheStatsImpl; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.FunctionTimer; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link HazelcastCacheMetrics}. + * + * @author Oleksii Bondar + */ +class HazelcastCacheMetricsTest extends AbstractCacheMetricsTest { + + private static IMap cache; + + private HazelcastCacheMetrics metrics = new HazelcastCacheMetrics(cache, expectedTag); + + @Test + void reportMetrics() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + metrics.bindTo(meterRegistry); + + verifyCommonCacheMetrics(meterRegistry, metrics); + + LocalMapStats localMapStats = cache.getLocalMapStats(); + + Gauge backupEntries = fetch(meterRegistry, "cache.entries", Tags.of("ownership", "backup")).gauge(); + assertThat(backupEntries.value()).isEqualTo(localMapStats.getBackupEntryCount()); + + Gauge ownedEntries = fetch(meterRegistry, "cache.entries", Tags.of("ownership", "owned")).gauge(); + assertThat(ownedEntries.value()).isEqualTo(localMapStats.getOwnedEntryCount()); + + Gauge backupEntryMemory = fetch(meterRegistry, "cache.entry.memory", Tags.of("ownership", "backup")).gauge(); + assertThat(backupEntryMemory.value()).isEqualTo(localMapStats.getBackupEntryMemoryCost()); + + Gauge ownedEntryMemory = fetch(meterRegistry, "cache.entry.memory", Tags.of("ownership", "owned")).gauge(); + assertThat(ownedEntryMemory.value()).isEqualTo(localMapStats.getOwnedEntryMemoryCost()); + + FunctionCounter partitionGets = fetch(meterRegistry, "cache.partition.gets").functionCounter(); + assertThat(partitionGets.count()).isEqualTo(localMapStats.getGetOperationCount()); + + // near cache stats + NearCacheStats nearCacheStats = localMapStats.getNearCacheStats(); + Gauge hitCacheRequests = fetch(meterRegistry, "cache.near.requests", Tags.of("result", "hit")).gauge(); + assertThat(hitCacheRequests.value()).isEqualTo(nearCacheStats.getHits()); + + Gauge missCacheRequests = fetch(meterRegistry, "cache.near.requests", Tags.of("result", "miss")).gauge(); + assertThat(missCacheRequests.value()).isEqualTo(nearCacheStats.getMisses()); + + Gauge nearPersistance = fetch(meterRegistry, "cache.near.persistences").gauge(); + assertThat(nearPersistance.value()).isEqualTo(nearCacheStats.getPersistenceCount()); + + Gauge nearEvictions = fetch(meterRegistry, "cache.near.evictions").gauge(); + assertThat(nearEvictions.value()).isEqualTo(nearCacheStats.getEvictions()); + + // timings + TimeUnit timeUnit = TimeUnit.NANOSECONDS; + FunctionTimer getsLatency = fetch(meterRegistry, "cache.gets.latency").functionTimer(); + assertThat(getsLatency.count()).isEqualTo(localMapStats.getGetOperationCount()); + assertThat(getsLatency.totalTime(timeUnit)).isEqualTo(localMapStats.getTotalGetLatency()); + + FunctionTimer putsLatency = fetch(meterRegistry, "cache.puts.latency").functionTimer(); + assertThat(putsLatency.count()).isEqualTo(localMapStats.getPutOperationCount()); + assertThat(putsLatency.totalTime(timeUnit)).isEqualTo(localMapStats.getTotalPutLatency()); + + FunctionTimer removeLatency = fetch(meterRegistry, "cache.removals.latency").functionTimer(); + assertThat(removeLatency.count()).isEqualTo(localMapStats.getRemoveOperationCount()); + assertThat(removeLatency.totalTime(timeUnit)).isEqualTo(localMapStats.getTotalRemoveLatency()); + } + + @Test + void constructInstanceViaStaticMethodMonitor() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + HazelcastCacheMetrics.monitor(meterRegistry, cache, expectedTag); + + meterRegistry.get("cache.partition.gets").tags(expectedTag).functionCounter(); + } + + @Test + void doNotReportEvictionCountSinceNotImplemented() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + HazelcastCacheMetrics.monitor(meterRegistry, cache, expectedTag); + + assertThat(meterRegistry.find("cache.evictions").functionCounter()).isNull(); + } + + @Test + void doNotReportMissCountSinceNotImplemented() { + MeterRegistry registry = new SimpleMeterRegistry(); + HazelcastCacheMetrics.monitor(registry, cache, expectedTag); + + assertThat(registry.find("cache.gets").tags(Tags.of("result", "miss")).functionCounter()).isNull(); + } + + @Test + void returnCacheSize() { + assertThat(metrics.size()).isEqualTo(cache.size()); + } + + @Test + void returnNullForMissCount() { + assertThat(metrics.missCount()).isNull(); + } + + @Test + void returnNullForEvictionCount() { + assertThat(metrics.evictionCount()).isNull(); + } + + @Test + void returnHitCount() { + assertThat(metrics.hitCount()).isEqualTo(cache.getLocalMapStats().getHits()); + } + + @Test + void returnPutCount() { + assertThat(metrics.putCount()).isEqualTo(cache.getLocalMapStats().getPutOperationCount()); + } + + @BeforeAll + static void setup() { + cache = Hazelcast.newHazelcastInstance().getMap("mycache"); + NearCacheStats nearCacheStats = mock(NearCacheStatsImpl.class); + // generate non-negative random value to address false-positives + int valueBound = 100000; + Random random = new Random(); + when(nearCacheStats.getMisses()).thenReturn((long) random.nextInt(valueBound)); + when(nearCacheStats.getPersistenceCount()).thenReturn((long) random.nextInt(valueBound)); + when(nearCacheStats.getEvictions()).thenReturn((long) random.nextInt(valueBound)); + + LocalMapStatsImpl localMapStats = (LocalMapStatsImpl) cache.getLocalMapStats(); + localMapStats.setNearCacheStats(nearCacheStats); + + localMapStats.setBackupEntryCount(random.nextInt(valueBound)); + localMapStats.setBackupEntryMemoryCost(random.nextInt(valueBound)); + localMapStats.setOwnedEntryCount(random.nextInt(valueBound)); + localMapStats.setOwnedEntryMemoryCost(random.nextInt(valueBound)); + localMapStats.incrementGetLatencyNanos(random.nextInt(valueBound)); + localMapStats.incrementPutLatencyNanos(random.nextInt(valueBound)); + localMapStats.incrementRemoveLatencyNanos(random.nextInt(valueBound)); + } + + @AfterAll + static void cleanup() { + Hazelcast.shutdownAll(); + } + +} diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/JCacheMetricsTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/JCacheMetricsTest.java new file mode 100644 index 0000000000..49191972f3 --- /dev/null +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/cache/JCacheMetricsTest.java @@ -0,0 +1,213 @@ +/** + * Copyright 2018 Pivotal Software, Inc. + *

+ * 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 + *

+ * http://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 io.micrometer.core.instrument.binder.cache; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.DynamicMBean; +import javax.management.InvalidAttributeValueException; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +import java.net.URI; +import java.util.Random; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link CaffeineCacheMetrics}. + * + * @author Oleksii Bondar + */ +class JCacheMetricsTest extends AbstractCacheMetricsTest { + + @Mock + private Cache cache; + + @Mock + private CacheManager cacheManager; + + private JCacheMetrics metrics; + private MBeanServer mbeanServer; + private Long expectedAttributeValue = new Random().nextLong(); + + @BeforeEach + void setup() throws Exception { + MockitoAnnotations.initMocks(this); + when(cache.getCacheManager()).thenReturn(cacheManager); + when(cache.getName()).thenReturn("testCache"); + when(cacheManager.getURI()).thenReturn(new URI("http://localhost")); + metrics = new JCacheMetrics(cache, expectedTag); + + // emulate MBean server with MBean used for statistic lookup + mbeanServer = MBeanServerFactory.createMBeanServer(); + ObjectName objectName = new ObjectName("javax.cache:type=CacheStatistics"); + ReflectionTestUtils.setField(metrics, "objectName", objectName); + CacheMBeanStub mBean = new CacheMBeanStub(expectedAttributeValue); + mbeanServer.registerMBean(mBean, objectName); + } + + @AfterEach + void tearDown() { + if (mbeanServer != null) { + MBeanServerFactory.releaseMBeanServer(mbeanServer); + } + } + + @Test + void reportExpectedMetrics() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + metrics.bindTo(meterRegistry); + + verifyCommonCacheMetrics(meterRegistry, metrics); + + Gauge cacheRemovals = fetch(meterRegistry, "cache.removals").gauge(); + assertThat(cacheRemovals.value()).isEqualTo(expectedAttributeValue.doubleValue()); + } + + @Test + void constructInstanceViaStaticMethodMonitor() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + JCacheMetrics.monitor(meterRegistry, cache, expectedTag); + + meterRegistry.get("cache.removals").tags(expectedTag).gauge(); + } + + @Test + void constructInstanceViaStaticMethodMonitorWithVarArgTags() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + JCacheMetrics.monitor(meterRegistry, cache, "version", "1.0"); + + meterRegistry.get("cache.removals").tags(Tags.of("version", "1.0")).gauge(); + } + + @Test + void returnCacheSize() { + assertThat(metrics.size()).isNull(); + } + + @Test + void returnNullForMissCount() { + assertThat(metrics.missCount()).isEqualTo(expectedAttributeValue); + } + + @Test + void returnNullForEvictionCount() { + assertThat(metrics.evictionCount()).isEqualTo(expectedAttributeValue); + } + + @Test + void returnHitCount() { + assertThat(metrics.hitCount()).isEqualTo(expectedAttributeValue); + } + + @Test + void returnPutCount() { + assertThat(metrics.putCount()).isEqualTo(expectedAttributeValue); + } + + @Test + void defaultValueWhenNoMBeanAttributeFound() throws MalformedObjectNameException { + // change source MBean to emulate AttributeNotFoundException + ObjectName objectName = new ObjectName("javax.cache:type=CacheInformation"); + ReflectionTestUtils.setField(metrics, "objectName", objectName); + + assertThat(metrics.hitCount()).isEqualTo(0L); + } + + @Test + void defaultValueWhenObjectNameNotInitialized() throws MalformedObjectNameException { + // set cacheManager to null to emulate scenario when objectName not initialized + when(cache.getCacheManager()).thenReturn(null); + metrics = new JCacheMetrics(cache, expectedTag); + + assertThat(metrics.hitCount()).isEqualTo(0L); + } + + @Test + void doNotReportMetricWhenObjectNameNotInitialized() throws MalformedObjectNameException { + // set cacheManager to null to emulate scenario when objectName not initialized + when(cache.getCacheManager()).thenReturn(null); + metrics = new JCacheMetrics(cache, expectedTag); + MeterRegistry registry = new SimpleMeterRegistry(); + metrics.bindImplementationSpecificMetrics(registry); + + assertThat(registry.find("cache.removals").tags(expectedTag).functionCounter()).isNull(); + } + + private static class CacheMBeanStub implements DynamicMBean { + + private Long expectedAttributeValue; + + public CacheMBeanStub(Long attributeValue) { + this.expectedAttributeValue = attributeValue; + } + + @Override + public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException, ReflectionException { + return expectedAttributeValue; + } + + @Override + public void setAttribute(Attribute attribute) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException { + } + + @Override + public AttributeList getAttributes(String[] attributes) { + return null; + } + + @Override + public AttributeList setAttributes(AttributeList attributes) { + return null; + } + + @Override + public Object invoke(String actionName, + Object[] params, + String[] signature) throws MBeanException, ReflectionException { + return null; + } + + @Override + public MBeanInfo getMBeanInfo() { + return new MBeanInfo(CacheMBeanStub.class.getName(), "description", null, null, null, null); + } + + } + +}