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

ab pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/main by this push:
     new 56cc230  SOLR-15564: Improve filtering expressions in /admin/metrics.
56cc230 is described below

commit 56cc23021df974230e8fe62c8af5f95a55fec34f
Author: Andrzej Bialecki <[email protected]>
AuthorDate: Mon Aug 2 17:45:19 2021 +0200

    SOLR-15564: Improve filtering expressions in /admin/metrics.
---
 solr/CHANGES.txt                                   |   2 +
 .../apache/solr/handler/admin/MetricsHandler.java  | 101 ++++++++++++++++++++-
 .../solr/handler/admin/MetricsHandlerTest.java     |  73 +++++++++++++++
 solr/solr-ref-guide/src/metrics-reporting.adoc     |  33 ++++++-
 4 files changed, 203 insertions(+), 6 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 4519edb..84502c8 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -385,6 +385,8 @@ Improvements
 
 * SOLR-15570: Include fields declared in the schema in table metadata (SQL) 
even if they are empty (Timothy Potter)
 
+* SOLR-15564: Improve filtering expressions in /admin/metrics. (ab)
+
 Optimizations
 ---------------------
 * SOLR-15433: Replace transient core cache LRU by Caffeine cache. (Bruno 
Roustant)
diff --git 
a/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java 
b/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java
index 0aa917a..52d90f9 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java
@@ -21,9 +21,11 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.function.BiConsumer;
 import java.util.function.Predicate;
 import java.util.regex.Pattern;
@@ -66,11 +68,12 @@ public class MetricsHandler extends RequestHandlerBase 
implements PermissionName
   public static final String REGISTRY_PARAM = "registry";
   public static final String GROUP_PARAM = "group";
   public static final String KEY_PARAM = "key";
+  public static final String EXPR_PARAM = "expr";
   public static final String TYPE_PARAM = "type";
 
   public static final String ALL = "all";
 
-  private static final Pattern KEY_REGEX = Pattern.compile("(?<!" + 
Pattern.quote("\\") + ")" + Pattern.quote(":"));
+  private static final Pattern KEY_SPLIT_REGEX = Pattern.compile("(?<!" + 
Pattern.quote("\\") + ")" + Pattern.quote(":"));
   private final CoreContainer cc;
   private final Map<String, String> injectedSysProps = 
CommonTestInjection.injectAdditionalProps();
   private final boolean enabled;
@@ -109,7 +112,7 @@ public class MetricsHandler extends RequestHandlerBase 
implements PermissionName
     handleRequest(req.getParams(), (k, v) -> rsp.add(k, v));
   }
   
-  public void handleRequest(SolrParams params, BiConsumer<String, Object> 
consumer) throws Exception {
+  private void handleRequest(SolrParams params, BiConsumer<String, Object> 
consumer) throws Exception {
     if (!enabled) {
       consumer.accept("error", "metrics collection is disabled");
       return;
@@ -120,6 +123,11 @@ public class MetricsHandler extends RequestHandlerBase 
implements PermissionName
       handleKeyRequest(keys, consumer);
       return;
     }
+    String[] exprs = params.getParams(EXPR_PARAM);
+    if (exprs != null && exprs.length > 0) {
+      handleExprRequest(exprs, consumer);
+      return;
+    }
     MetricFilter mustMatchFilter = parseMustMatchFilter(params);
     Predicate<CharSequence> propertyFilter = parsePropertyFilter(params);
     List<MetricType> metricTypes = parseMetricTypes(params);
@@ -139,14 +147,95 @@ public class MetricsHandler extends RequestHandlerBase 
implements PermissionName
     consumer.accept("metrics", response);
   }
 
-  public void handleKeyRequest(String[] keys, BiConsumer<String, Object> 
consumer) {
+  private static class MetricsExpr {
+    Pattern registryRegex;
+    MetricFilter metricFilter;
+    Predicate<CharSequence> propertyFilter;
+  }
+
+  private void handleExprRequest(String[] exprs, BiConsumer<String, Object> 
consumer) {
+    SimpleOrderedMap<Object> result = new SimpleOrderedMap<>();
+    SimpleOrderedMap<Object> errors = new SimpleOrderedMap<>();
+    List<MetricsExpr> metricsExprs = new ArrayList<>();
+
+    for (String key : exprs) {
+      if (key == null || key.isEmpty()) {
+        continue;
+      }
+      String[] parts = KEY_SPLIT_REGEX.split(key);
+      if (parts.length < 2 || parts.length > 3) {
+        errors.add(key, "at least two and at most three colon-separated parts 
must be provided");
+        continue;
+      }
+      MetricsExpr me = new MetricsExpr();
+      me.registryRegex = Pattern.compile(unescape(parts[0]));
+      me.metricFilter = new SolrMetricManager.RegexFilter(unescape(parts[1]));
+      String propertyPart = parts.length > 2 ? unescape(parts[2]) : null;
+      if (propertyPart == null) {
+        me.propertyFilter = name -> true;
+      } else {
+        me.propertyFilter = new Predicate<>() {
+          final Pattern pattern = Pattern.compile(propertyPart);
+          @Override
+          public boolean test(CharSequence charSequence) {
+            return pattern.matcher(charSequence).matches();
+          }
+        };
+      }
+      metricsExprs.add(me);
+    }
+    // find matching registries first, to avoid scanning non-matching 
registries
+    Set<String> matchingRegistries = new TreeSet<>();
+    metricsExprs.forEach(me -> {
+      metricManager.registryNames().forEach(name -> {
+        if (me.registryRegex.matcher(name).matches()) {
+          matchingRegistries.add(name);
+        }
+      });
+    });
+    for (String registryName : matchingRegistries) {
+      MetricRegistry registry = metricManager.registry(registryName);
+      for (MetricsExpr me : metricsExprs) {
+        @SuppressWarnings("unchecked")
+        SimpleOrderedMap<Object> perRegistryResult = 
(SimpleOrderedMap<Object>) result.get(registryName);
+        final SimpleOrderedMap<Object> perRegistryTemp = new 
SimpleOrderedMap<>();
+        // skip processing if not a matching registry
+        if (!me.registryRegex.matcher(registryName).matches()) {
+          continue;
+        }
+        MetricUtils.toMaps(registry, 
Collections.singletonList(MetricFilter.ALL), me.metricFilter,
+            me.propertyFilter, false, false, true, false, (k, v) -> 
perRegistryTemp.add(k, v));
+        // extracted some metrics and there's no entry for this registry yet
+        if (perRegistryTemp.size() > 0) {
+          if (perRegistryResult == null) { // new results for this registry
+            result.add(registryName, perRegistryTemp);
+          } else {
+            // merge if needed
+            for (Iterator<Map.Entry<String, Object>> it = 
perRegistryTemp.iterator(); it.hasNext(); ) {
+              Map.Entry<String, Object> entry = it.next();
+              Object existing = perRegistryResult.get(entry.getKey());
+              if (existing == null) {
+                perRegistryResult.add(entry.getKey(), entry.getValue());
+              }
+            }
+          }
+        }
+      }
+    }
+    consumer.accept("metrics", result);
+    if (errors.size() > 0) {
+      consumer.accept("errors", errors);
+    }
+  }
+
+  private void handleKeyRequest(String[] keys, BiConsumer<String, Object> 
consumer) {
     SimpleOrderedMap<Object> result = new SimpleOrderedMap<>();
     SimpleOrderedMap<Object> errors = new SimpleOrderedMap<>();
     for (String key : keys) {
       if (key == null || key.isEmpty()) {
         continue;
       }
-      String[] parts = KEY_REGEX.split(key);
+      String[] parts = KEY_SPLIT_REGEX.split(key);
       if (parts.length < 2 || parts.length > 3) {
         errors.add(key, "at least two and at most three colon-separated parts 
must be provided");
         continue;
@@ -200,7 +289,9 @@ public class MetricsHandler extends RequestHandlerBase 
implements PermissionName
     for (int i = 0; i < s.length(); i++) {
       char c = s.charAt(i);
       if (c == '\\') {
-        continue;
+        if (i < s.length() - 1 && s.charAt(i + 1) == ':') {
+          continue;
+        }
       }
       sb.append(c);
     }
diff --git 
a/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
index 338c38f..adcc305 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
@@ -19,6 +19,7 @@ package org.apache.solr.handler.admin;
 
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import com.codahale.metrics.Counter;
@@ -350,6 +351,78 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
   }
 
   @Test
+  @SuppressWarnings("unchecked")
+  public void testExprMetrics() throws Exception {
+    MetricsHandler handler = new MetricsHandler(h.getCoreContainer());
+
+    String key1 = "solr\\.core\\..*:.*/select\\.request.*:.*Rate";
+    SolrQueryResponse resp = new SolrQueryResponse();
+    handler.handleRequestBody(req(CommonParams.QT, "/admin/metrics", 
CommonParams.WT, "json",
+        MetricsHandler.EXPR_PARAM, key1), resp);
+    // response structure is like in the case of non-key params
+    Object val = resp.getValues().findRecursive( "metrics", 
"solr.core.collection1", "QUERY./select.requestTimes");
+    assertNotNull(val);
+    assertTrue(val instanceof MapWriter);
+    Map<String, Object> map = new HashMap<>();
+    ((MapWriter) val).toMap(map);
+    assertEquals(map.toString(), 4, map.size()); // mean, 1, 5, 15
+    assertNotNull(map.toString(), map.get("meanRate"));
+    assertNotNull(map.toString(), map.get("1minRate"));
+    assertNotNull(map.toString(), map.get("5minRate"));
+    assertNotNull(map.toString(), map.get("15minRate"));
+    assertEquals(map.toString(), ((Number) map.get("1minRate")).doubleValue(), 
0.0, 0.0);
+    map.clear();
+
+    String key2 = "solr\\.core\\..*:.*/select\\.request.*";
+    resp = new SolrQueryResponse();
+    handler.handleRequestBody(req(CommonParams.QT, "/admin/metrics", 
CommonParams.WT, "json",
+        MetricsHandler.EXPR_PARAM, key2), resp);
+    // response structure is like in the case of non-key params
+    val = resp.getValues().findRecursive( "metrics", "solr.core.collection1");
+    assertNotNull(val);
+    Object v = ((SimpleOrderedMap<Object>) 
val).get("QUERY./select.requestTimes");
+    assertNotNull(v);
+    assertTrue(v instanceof MapWriter);
+    ((MapWriter) v).toMap(map);
+    assertEquals(map.toString(), 14, map.size());
+    assertNotNull(map.toString(), map.get("1minRate"));
+    assertEquals(map.toString(), ((Number) map.get("1minRate")).doubleValue(), 
0.0, 0.0);
+    map.clear();
+    // select requests counter
+    v = ((SimpleOrderedMap<Object>) val).get("QUERY./select.requests");
+    assertNotNull(v);
+    assertTrue(v instanceof Number);
+
+    // test multiple expressions producing overlapping metrics - should be no 
dupes
+
+    // this key matches also sub-metrics of /select, eg. /select.distrib, 
/select.local, ...
+    String key3 = "solr\\.core\\..*:.*/select.*\\.requestTimes:count";
+    resp = new SolrQueryResponse();
+    // ORDER OF PARAMS MATTERS HERE! see the refguide
+    handler.handleRequestBody(req(CommonParams.QT, "/admin/metrics", 
CommonParams.WT, "json",
+        MetricsHandler.EXPR_PARAM, key2, MetricsHandler.EXPR_PARAM, key1, 
MetricsHandler.EXPR_PARAM, key3), resp);
+    val = resp.getValues().findRecursive( "metrics", "solr.core.collection1");
+    assertNotNull(val);
+    // for requestTimes only the full set of values from the first expr should 
be present
+    assertNotNull(val);
+    SimpleOrderedMap<Object> values = (SimpleOrderedMap<Object>) val;
+    assertEquals(values.jsonStr(), 4, values.size());
+    List<Object> multipleVals = values.getAll("QUERY./select.requestTimes");
+    assertEquals(multipleVals.toString(), 1, multipleVals.size());
+    v = values.get("QUERY./select.local.requestTimes");
+    assertTrue(v instanceof MapWriter);
+    ((MapWriter) v).toMap(map);
+    assertEquals(map.toString(), 1, map.size());
+    assertTrue(map.toString(), map.containsKey("count"));
+    map.clear();
+    v = values.get("QUERY./select.distrib.requestTimes");
+    assertTrue(v instanceof MapWriter);
+    ((MapWriter) v).toMap(map);
+    assertEquals(map.toString(), 1, map.size());
+    assertTrue(map.toString(), map.containsKey("count"));
+  }
+
+  @Test
   public void testMetricsUnload() throws Exception {
 
     SolrCore core = 
h.getCoreContainer().getCore("collection1");//;.getRequestHandlers().put("/dumphandler",
 new DumpRequestHandler());
diff --git a/solr/solr-ref-guide/src/metrics-reporting.adoc 
b/solr/solr-ref-guide/src/metrics-reporting.adoc
index 8854be7..59326fa 100644
--- a/solr/solr-ref-guide/src/metrics-reporting.adoc
+++ b/solr/solr-ref-guide/src/metrics-reporting.adoc
@@ -763,7 +763,33 @@ Examples:
 * `key=solr.jvm:system.properties:user.name`
 
 +
-*NOTE: when this parameter is used, other selection methods listed above are 
ignored.*
+*NOTE: when this parameter is used, any other selection methods are ignored.*
+
+`expr`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: none
+|===
++
+Extended notation of the `key` selection criteria, which supports regular 
expressions for each of the
+parts supported by the `key` selector. This parameter can be specified 
multiple times to retrieve metrics that match
+any expression. The API guarantees that the output will consist only of unique 
metric names even if
+multiple expressions match the same metric name. Note: order of multiple 
`expr` parameters matters here
+- only the first value of the first matching expression will be recorded, 
subsequent values for the same metric name
+produced by matching other expressions will be skipped.
++
+Fully-qualified expression consists of at least two and at most three regex 
patterns separated by
+colons: a registry pattern, colon, a metric pattern, and then an optional 
colon and metric property pattern.
+Colons and other regex meta-characters in names and in regular expressions 
MUST be escaped using backslash (`\`) character.
+
+Examples:
+
+* `expr=solr\.core\..*:QUERY\..*\.requestTimes:max_ms`
+* `expr=solr\.jvm:system\.properties:user\..*`
+
++
+*NOTE: when this parameter is used, any other selection methods are ignored.*
 
 `compact`::
 +
@@ -845,3 +871,8 @@ Request only "user.name" property of "system.properties" 
metric from registry "s
 
 [source,text]
 
http://localhost:8983/solr/admin/metrics?wt=xml&key=solr.jvm:system.properties:user.name
+
+Request query rates (but not histograms) from any core in any collection in 
any QUERY handler:
+
+[source,text]
+http://localhost:8983/solr/admin/metrics?expr=solr\.core\..*:QUERY\..*\.requestTimes:.*Rate

Reply via email to