SLING-11197: fix NaN value returned by cache_hit_rate metric (#27)

diff --git a/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java b/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java
index 49e795b..9bba5a8 100644
--- a/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java
+++ b/src/main/java/org/apache/sling/graphql/core/servlet/GraphQLServlet.java
@@ -196,8 +196,11 @@
         cacheMisses = metricsService.counter(METRIC_NS + "." + servletRegistrationProperties + ".cache_misses");
         requestsServed = metricsService.counter(METRIC_NS + "." + servletRegistrationProperties + ".requests_total");
         gaugeCacheHitRate = METRIC_NS + "." + servletRegistrationProperties + ".cache_hit_rate";
-        metricRegistry.register(gaugeCacheHitRate,
-                (Gauge<Float>) () -> (cacheHits.getCount() / (float) (cacheHits.getCount() + cacheMisses.getCount())));
+        metricRegistry.register(gaugeCacheHitRate, (Gauge<Float>) () -> {
+            float hitCount = cacheHits.getCount();
+            float missCount = cacheMisses.getCount();
+            return hitCount > 0 || missCount > 0 ? hitCount / (hitCount + missCount) : 0.0f;
+        });
         requestTimer = metricsService.timer(METRIC_NS + "." + servletRegistrationProperties + ".requests_timer");
     }
 
diff --git a/src/test/java/org/apache/sling/graphql/core/servlet/GraphQLServletTest.java b/src/test/java/org/apache/sling/graphql/core/servlet/GraphQLServletTest.java
index a7a2f52..1fe9d84 100644
--- a/src/test/java/org/apache/sling/graphql/core/servlet/GraphQLServletTest.java
+++ b/src/test/java/org/apache/sling/graphql/core/servlet/GraphQLServletTest.java
@@ -21,7 +21,6 @@
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Map;
-import java.util.stream.Stream;
 
 import javax.servlet.Servlet;
 
@@ -42,14 +41,20 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import com.codahale.metrics.Gauge;
 import com.codahale.metrics.MetricRegistry;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 @RunWith(MockitoJUnitRunner.class)
@@ -62,17 +67,22 @@
     private static final String TEST_QUERY = "{\"query\": \"{ currentResource { resourceType name } }\" }";
     private Resource resource;
 
+    @Mock
+    private MetricsService metricsService;
+    @Spy
+    private MetricRegistry metricRegistry = new MetricRegistry();
+    @Mock
+    private Counter counter;
+    @Mock
+    private Timer timer;
+
     @Before
     public void setUp() {
-        MetricsService metricsService = mock(MetricsService.class);
-        when(metricsService.counter(any(String.class))).thenReturn(mock(Counter.class));
-
-        Timer timer = mock(Timer.class);
         when(timer.time()).thenReturn(mock(Timer.Context.class));
+        when(metricsService.counter(any(String.class))).thenReturn(counter);
         when(metricsService.timer(any(String.class))).thenReturn(timer);
         context.registerService(MetricsService.class, metricsService);
 
-        MetricRegistry metricRegistry = mock(MetricRegistry.class);
         context.registerService(MetricRegistry.class, metricRegistry, "name", "sling");
 
         QueryExecutor queryExecutor = mock(QueryExecutor.class);
@@ -132,6 +142,38 @@
         assertEquals("Persisted queries are disabled.", response.getStatusMessage());
     }
 
+    @Test
+    public void testMetricsRegistered() {
+        context.registerInjectActivateService(new SimpleGraphQLCacheProvider());
+        context.registerInjectActivateService(new GraphQLServlet(), ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES, TEST_RESOURCE_TYPE,
+            "persistedQueries.suffix", "");
+
+        String expectedMetricPrefix = "org.apache.sling.graphql.core.servlet.GraphQLServlet.rt:" + TEST_RESOURCE_TYPE + ".m:GET.e:gql";
+
+        verify(metricsService).counter(expectedMetricPrefix + ".cache_hits");
+        verify(metricsService).counter(expectedMetricPrefix + ".requests_total");
+        verify(metricsService).timer(expectedMetricPrefix + ".requests_timer");
+        verify(metricRegistry).register(eq(expectedMetricPrefix + ".cache_hit_rate"), any(Gauge.class));
+    }
+
+    @Test
+    public void testCacheHitRatioMetric () {
+        context.registerInjectActivateService(new SimpleGraphQLCacheProvider());
+        context.registerInjectActivateService(new GraphQLServlet(), ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES, TEST_RESOURCE_TYPE,
+            "persistedQueries.suffix", "/persisted");
+
+        // test resource type, default method, default extension
+        String expectedMetric = "org.apache.sling.graphql.core.servlet.GraphQLServlet.rt:" + TEST_RESOURCE_TYPE + ".m:GET.e:gql.cache_hit_rate";
+
+        assertTrue(metricRegistry.getGauges().containsKey(expectedMetric));
+        assertEquals(0.0f, metricRegistry.getGauges().get(expectedMetric).getValue());
+
+        // increments both the hit and miss metric
+        when(counter.getCount()).thenReturn(1L);
+
+        assertEquals(0.5f, metricRegistry.getGauges().get(expectedMetric).getValue());
+    }
+
     private void assertPostWithBody(String contentType, int expectedStatus) throws IOException {
         context.registerInjectActivateService(new SimpleGraphQLCacheProvider());
         context.registerInjectActivateService(new GraphQLServlet(), ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES, TEST_RESOURCE_TYPE,