You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2020/09/21 15:00:32 UTC

[sling-org-apache-sling-graphql-core] branch master updated: SLING-9744 - Add metrics for the persisted queries

This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-core.git


The following commit(s) were added to refs/heads/master by this push:
     new d9eb854  SLING-9744 - Add metrics for the persisted queries
d9eb854 is described below

commit d9eb85499e82e25025b73d0730d41b9eb2486b9b
Author: Radu Cotescu <co...@adobe.com>
AuthorDate: Mon Sep 21 16:59:41 2020 +0200

    SLING-9744 - Add metrics for the persisted queries
    
    * added metrics and documented them
---
 docs/metrics.md                                    |  60 ++++++++++
 pom.xml                                            |  12 ++
 .../core/cache/SimpleGraphQLCacheProvider.java     |  72 +++++++++---
 .../sling/graphql/core/servlet/GraphQLServlet.java | 129 +++++++++++++++------
 4 files changed, 225 insertions(+), 48 deletions(-)

diff --git a/docs/metrics.md b/docs/metrics.md
new file mode 100644
index 0000000..318752a
--- /dev/null
+++ b/docs/metrics.md
@@ -0,0 +1,60 @@
+Apache Sling GraphQL Core - Metrics
+----
+
+This page documents the metrics the Apache Sling GraphQL Core bundle exposes to the Metrics Registry.
+
+## `org.apache.sling.graphql.core.cache.SimpleGraphQLCacheProvider`
+
+<dl>
+
+<dt>org.apache.sling.graphql.core.cache.SimpleGraphQLCacheProvider.evictions</dt>
+<dd>
+    the number of cache evictions for <a href="../README.md#caching-persisted-queries-api">persisted queries</a>
+</dd>
+
+<dt>org.apache.sling.graphql.core.cache.SimpleGraphQLCacheProvider.capacity</dt>
+<dd>
+    the maximum number of entries the cache can store
+</dd>
+
+<dt>org.apache.sling.graphql.core.cache.SimpleGraphQLCacheProvider.elements<dt>
+<dd>
+    the current number of elements the cache stores
+</dd>
+
+<dt>org.apache.sling.graphql.core.cache.SimpleGraphQLCacheProvider.maxMemory</dt>
+<dd>
+    the maximum amount of memory the stored queries can consume (in bytes)
+</dd>
+
+<dt>org.apache.sling.graphql.core.cache.SimpleGraphQLCacheProvider.currentMemory</dt>
+<dd>
+    the current amount of memory used to store the persisted queries (in bytes); this is calculated through an approximation of the amount of bytes each entry requires
+</dd>
+
+</dl>
+
+## `org.apache.sling.graphql.core.servlet.GraphQLServlet`
+
+For each service instance of the `org.apache.sling.graphql.core.servlet.GraphQLServlet` servlet, the following additional
+metrics are available, where the `<qualifier>` is a string using the
+`[rt=list of servlet resource types][m=list of servlet methods][s=list of servlet selectors][e=list of servlet extensions]` pattern:
+
+<dl>
+
+<dt>org.apache.sling.graphql.core.servlet.GraphQLServlet.&lt;qualifier&gt;.cache_hits</dt>
+<dd>the number of times a persisted query was retrieved from the cache</dd>
+
+<dt>org.apache.sling.graphql.core.servlet.GraphQLServlet.&lt;qualifier&gt;.cache_misses</dt>
+<dd>the number of times a persisted query was not found in the cache</dd>
+
+<dt>org.apache.sling.graphql.core.servlet.GraphQLServlet.&lt;qualifier&gt;.cache_hit_rate</dt>
+<dd>the cache hit rate: a float number between 0 and 1.0; multiply the number by 100 to get the percentage</dd>
+
+<dt>org.apache.sling.graphql.core.servlet.GraphQLServlet.&lt;qualifier&gt;.total_requests</dt>
+<dd>total number of requests served by this servlet</dd>
+
+<dt>org.apache.sling.graphql.core.servlet.GraphQLServlet.&lt;qualifier&gt;.requests_timer</dt>
+<dd>request timing metrics for this servlet</dd>
+
+</dl>
diff --git a/pom.xml b/pom.xml
index 6f36856..eb79816 100644
--- a/pom.xml
+++ b/pom.xml
@@ -158,6 +158,18 @@
     </dependency>
     <dependency>
       <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.commons.metrics</artifactId>
+      <version>1.2.8</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.dropwizard.metrics</groupId>
+      <artifactId>metrics-core</artifactId>
+      <version>3.2.6</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sling</groupId>
       <artifactId>org.apache.sling.resource.presence</artifactId>
       <version>0.0.2</version>
       <scope>test</scope>
diff --git a/src/main/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProvider.java b/src/main/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProvider.java
index b2def0f..7fe1f59 100644
--- a/src/main/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProvider.java
+++ b/src/main/java/org/apache/sling/graphql/core/cache/SimpleGraphQLCacheProvider.java
@@ -21,21 +21,29 @@ package org.apache.sling.graphql.core.cache;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.commons.metrics.Counter;
+import org.apache.sling.commons.metrics.MetricsService;
 import org.apache.sling.graphql.api.cache.GraphQLCacheProvider;
 import org.apache.sling.graphql.core.engine.SlingGraphQLException;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.osgi.framework.BundleContext;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.metatype.annotations.AttributeDefinition;
 import org.osgi.service.metatype.annotations.AttributeType;
 import org.osgi.service.metatype.annotations.Designate;
@@ -43,6 +51,9 @@ import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.MetricRegistry;
+
 @Component()
 @Designate(ocd = SimpleGraphQLCacheProvider.Config.class)
 public class SimpleGraphQLCacheProvider implements GraphQLCacheProvider {
@@ -73,29 +84,61 @@ public class SimpleGraphQLCacheProvider implements GraphQLCacheProvider {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(SimpleGraphQLCacheProvider.class);
 
+    @Reference
+    private MetricsService metricsService;
+
+    @Reference(target = "(name=sling)")
+    private MetricRegistry metricRegistry;
+
     private InMemoryLRUCache persistedQueriesCache;
     private Lock readLock;
     private Lock writeLock;
 
+    private Counter evictions;
+
+    private static final String METRIC_NS = SimpleGraphQLCacheProvider.class.getName();
+    private static final String GAUGE_CAPACITY = METRIC_NS + ".capacity";
+    private static final String GAUGE_ELEMENTS = METRIC_NS + ".elements";
+    private static final String GAUGE_MAX_MEMORY = METRIC_NS + ".maxMemory";
+    private static final String GAUGE_CURRENT_MEMORY = METRIC_NS + ".currentMemory";
+    private static final String COUNTER_EVICTIONS = METRIC_NS + ".evictions";
+    private static final Set<String> MANUALLY_REGISTERED_METRICS = new HashSet<>(Arrays.asList(GAUGE_CAPACITY, GAUGE_ELEMENTS,
+            GAUGE_MAX_MEMORY, GAUGE_CURRENT_MEMORY));
+
     @Activate
-    private void activate(Config config) {
+    private void activate(Config config, BundleContext bundleContext) {
         ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
         readLock = readWriteLock.readLock();
         writeLock = readWriteLock.writeLock();
-        int cacheSize = config.capacity();
-        if (cacheSize < 0) {
-            cacheSize = 0;
+        int capacity;
+        if (config.capacity() < 0) {
+            capacity = 0;
             LOGGER.debug("Cache capacity set to {}. Defaulting to 0.", config.capacity());
+        } else {
+            capacity = config.capacity();
         }
-        long maxSize = config.maxSize();
-        if (maxSize < 0) {
-            maxSize = 0;
-            LOGGER.debug("Cache size set to {}. Defaulting to 0.", config.maxSize());
+        long maxMemory;
+        if (config.maxSize() < 0) {
+            maxMemory = 0;
+            LOGGER.debug("Cache max memory set to {}. Defaulting to 0.", config.maxSize());
+        } else {
+            maxMemory = config.maxSize();
         }
-        persistedQueriesCache = new InMemoryLRUCache(cacheSize, maxSize);
-        LOGGER.debug("In-memory cache initialized: cacheSize={}, maxSize={}.", cacheSize, maxSize);
+        persistedQueriesCache = new InMemoryLRUCache(capacity, maxMemory);
+        LOGGER.debug("In-memory cache initialized: capacity={}, maxMemory={}.", capacity, maxMemory);
+        metricRegistry.register(GAUGE_CAPACITY, (Gauge<Integer>) () -> capacity);
+        metricRegistry.register(GAUGE_MAX_MEMORY, (Gauge<Long>) () -> maxMemory);
+        metricRegistry.register(GAUGE_CURRENT_MEMORY, (Gauge<Long>) () -> persistedQueriesCache.currentSizeInBytes);
+        metricRegistry.register(GAUGE_ELEMENTS, (Gauge<Integer>) () -> persistedQueriesCache.size());
+        evictions = metricsService.counter(COUNTER_EVICTIONS);
     }
 
+    @Deactivate
+    private void deactivate() {
+        for (String manuallyRegisteredMetric : MANUALLY_REGISTERED_METRICS) {
+            metricRegistry.remove(manuallyRegisteredMetric);
+        }
+    }
 
     @Override
     @Nullable
@@ -155,7 +198,7 @@ public class SimpleGraphQLCacheProvider implements GraphQLCacheProvider {
      * This implementation provides a simple LRU eviction based on either the number of entries or the memory used by the stored values.
      * Synchronization has to happen externally.
      */
-    private static class InMemoryLRUCache extends LinkedHashMap<String, String> {
+    private class InMemoryLRUCache extends LinkedHashMap<String, String> {
 
         private final int capacity;
         private final long maxSizeInBytes;
@@ -174,9 +217,10 @@ public class SimpleGraphQLCacheProvider implements GraphQLCacheProvider {
                 willRemove = size() > capacity;
             } else if (maxSizeInBytes > 0) {
                 willRemove = currentSizeInBytes > maxSizeInBytes;
-                if (willRemove) {
-                    currentSizeInBytes -=  getApproximateStringSizeInBytes(eldest.getValue());
-                }
+            }
+            if (willRemove) {
+                evictions.increment();
+                currentSizeInBytes -= getApproximateStringSizeInBytes(eldest.getValue());
             }
             return willRemove;
         }
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 640e5c0..2797906 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
@@ -33,6 +33,9 @@ import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.api.SlingHttpServletResponse;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.servlets.SlingAllMethodsServlet;
+import org.apache.sling.commons.metrics.Counter;
+import org.apache.sling.commons.metrics.MetricsService;
+import org.apache.sling.commons.metrics.Timer;
 import org.apache.sling.graphql.api.cache.GraphQLCacheProvider;
 import org.apache.sling.graphql.core.engine.GraphQLResourceQuery;
 import org.apache.sling.graphql.core.engine.SlingDataFetcherSelector;
@@ -43,12 +46,15 @@ import org.jetbrains.annotations.NotNull;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.metatype.annotations.AttributeDefinition;
 import org.osgi.service.metatype.annotations.AttributeType;
 import org.osgi.service.metatype.annotations.Designate;
 import org.osgi.service.metatype.annotations.ObjectClassDefinition;
 
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.MetricRegistry;
 import graphql.ExecutionResult;
 
 /** Servlet that can be activated to implement the standard
@@ -128,11 +134,26 @@ public class GraphQLServlet extends SlingAllMethodsServlet {
     @Reference
     private GraphQLCacheProvider cacheProvider;
 
+    @Reference
+    private MetricsService metricsService;
+
+    @Reference(target = "(name=sling)")
+    private MetricRegistry metricRegistry;
+
     private String suffixPersisted;
     private Pattern patternGetPersistedQuery;
     private int cacheControlMaxAge;
     private final JsonSerializer jsonSerializer = new JsonSerializer();
 
+    private Counter cacheHits;
+    private Counter cacheMisses;
+    private Counter requestsServed;
+    private Timer requestTimer;
+
+    private static final String METRIC_NS = GraphQLServlet.class.getName();
+    private String servletRegistrationProperties;
+    private String gaugeCacheHitRate;
+
     @Activate
     private void activate(Config config) {
         String[] extensions = config.sling_servlet_extensions();
@@ -157,60 +178,100 @@ public class GraphQLServlet extends SlingAllMethodsServlet {
             suffixPersisted = null;
             patternGetPersistedQuery = null;
         }
+        StringBuilder sb = new StringBuilder();
+        sb.append("[rt=").append(String.join(",", config.sling_servlet_resourceTypes())).append("]");
+        if (config.sling_servlet_methods().length > 0) {
+            sb.append("[m=").append(String.join(",", config.sling_servlet_methods())).append("]");
+        }
+        if (config.sling_servlet_selectors().length > 0) {
+            sb.append("[s=").append(String.join(",", config.sling_servlet_selectors())).append("]");
+        }
+        if (config.sling_servlet_extensions().length > 0) {
+            sb.append("[e=").append(String.join(",", config.sling_servlet_extensions())).append("]");
+        }
+        servletRegistrationProperties = sb.toString();
+        cacheHits = metricsService.counter(METRIC_NS + "." + servletRegistrationProperties + ".cache_hits");
+        cacheMisses = metricsService.counter(METRIC_NS + "." + servletRegistrationProperties + ".cache_misses");
+        requestsServed = metricsService.counter(METRIC_NS + "." + servletRegistrationProperties + ".total_requests");
+        gaugeCacheHitRate = METRIC_NS + "." + servletRegistrationProperties + ".cache_hit_rate";
+        metricRegistry.register(gaugeCacheHitRate,
+                (Gauge<Float>) () -> (float) (cacheHits.getCount() / (float) (cacheHits.getCount() + cacheMisses.getCount())));
+        requestTimer = metricsService.timer(METRIC_NS + "." + servletRegistrationProperties + ".requests_timer");
+    }
+
+    @Deactivate
+    private void deactivate() {
+        if (StringUtils.isNotEmpty(gaugeCacheHitRate)) {
+            metricRegistry.remove(gaugeCacheHitRate);
+        }
     }
 
     @Override
     public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
-        String suffix = request.getRequestPathInfo().getSuffix();
-        if (suffix != null) {
-            if (StringUtils.isNotEmpty(suffixPersisted) && suffix.startsWith(suffixPersisted)) {
-                Matcher matcher = patternGetPersistedQuery.matcher(suffix);
-                if (matcher.matches()) {
-                    String queryHash = matcher.group(1);
-                    String extension = matcher.group(2);
-                    String requestExtension = request.getRequestPathInfo().getExtension();
-                    if (requestExtension != null && requestExtension.equals(extension)) {
-                        if (StringUtils.isNotEmpty(queryHash)) {
-                            String query = cacheProvider.getQuery(queryHash, request.getResource().getResourceType(),
-                                    request.getRequestPathInfo().getSelectorString());
-                            if (query != null) {
-                                boolean isAuthenticated = request.getHeaders("Authorization").hasMoreElements();
-                                StringBuilder cacheControlValue = new StringBuilder("max-age=").append(cacheControlMaxAge);
-                                if (isAuthenticated) {
-                                    cacheControlValue.append(",private");
+        requestsServed.increment();
+        Timer.Context requestTimerContext = requestTimer.time();
+        try {
+            String suffix = request.getRequestPathInfo().getSuffix();
+            if (suffix != null) {
+                if (StringUtils.isNotEmpty(suffixPersisted) && suffix.startsWith(suffixPersisted)) {
+                    Matcher matcher = patternGetPersistedQuery.matcher(suffix);
+                    if (matcher.matches()) {
+                        String queryHash = matcher.group(1);
+                        String extension = matcher.group(2);
+                        String requestExtension = request.getRequestPathInfo().getExtension();
+                        if (requestExtension != null && requestExtension.equals(extension)) {
+                            if (StringUtils.isNotEmpty(queryHash)) {
+                                String query = cacheProvider.getQuery(queryHash, request.getResource().getResourceType(),
+                                        request.getRequestPathInfo().getSelectorString());
+                                if (query != null) {
+                                    boolean isAuthenticated = request.getHeaders("Authorization").hasMoreElements();
+                                    StringBuilder cacheControlValue = new StringBuilder("max-age=").append(cacheControlMaxAge);
+                                    if (isAuthenticated) {
+                                        cacheControlValue.append(",private");
+                                    }
+                                    response.addHeader("Cache-Control", cacheControlValue.toString());
+                                    execute(query, request, response);
+                                    cacheHits.increment();
+                                } else {
+                                    cacheMisses.increment();
+                                    response.sendError(HttpServletResponse.SC_NOT_FOUND, "Cannot find persisted query " + queryHash);
                                 }
-                                response.addHeader("Cache-Control", cacheControlValue.toString());
-                                execute(query, request, response);
-                            } else {
-                                response.sendError(HttpServletResponse.SC_NOT_FOUND, "Cannot find persisted query " + queryHash);
                             }
+                        } else {
+                            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "The persisted query's extension does not match the " +
+                                    "servlet extension.");
                         }
                     } else {
-                        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "The persisted query's extension does not match the " +
-                                "servlet extension.");
+                        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unexpected hash.");
                     }
                 } else {
-                    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unexpected hash.");
+                    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Persisted queries are disabled.");
                 }
             } else {
-                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Persisted queries are disabled.");
+                execute(request.getResource(), request, response);
             }
-        } else {
-            execute(request.getResource(), request, response);
+        } finally {
+            requestTimerContext.stop();
         }
     }
 
     @Override
     public void doPost(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) throws IOException {
-        String suffix = request.getRequestPathInfo().getSuffix();
-        if (suffix != null) {
-            if (StringUtils.isNotEmpty(suffixPersisted) && suffix.equals(suffixPersisted)) {
-                doPostPersistedQuery(request, response);
+        requestsServed.increment();
+        Timer.Context requestTimerContext = requestTimer.time();
+        try {
+            String suffix = request.getRequestPathInfo().getSuffix();
+            if (suffix != null) {
+                if (StringUtils.isNotEmpty(suffixPersisted) && suffix.equals(suffixPersisted)) {
+                    doPostPersistedQuery(request, response);
+                } else {
+                    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+                }
             } else {
-                response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+                execute(request.getResource(), request, response);
             }
-        } else {
-            execute(request.getResource(), request, response);
+        } finally {
+            requestTimerContext.stop();
         }
     }