You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ds...@apache.org on 2022/09/02 16:26:04 UTC

[solr] branch branch_9x updated: SOLR-15007: Aggregated node level metrics for request handlers (#948)

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

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


The following commit(s) were added to refs/heads/branch_9x by this push:
     new 2e2473dc218 SOLR-15007: Aggregated node level metrics for request handlers (#948)
2e2473dc218 is described below

commit 2e2473dc2185af5731bc5a816a2e632d81ab7005
Author: Justin Sweeney <ju...@gmail.com>
AuthorDate: Fri Sep 2 00:58:07 2022 -0400

    SOLR-15007: Aggregated node level metrics for request handlers (#948)
    
    Useful when there are lots of cores.
    
    Co-authored-by: Justin Sweeney <ju...@fullstory.com>
---
 solr/CHANGES.txt                                   |   2 +
 .../apache/solr/handler/RequestHandlerBase.java    |  19 +-
 .../solr/metrics/DelegateRegistryCounter.java      |  74 ++++++++
 .../solr/metrics/DelegateRegistryHistogram.java    |  69 +++++++
 .../apache/solr/metrics/DelegateRegistryMeter.java |  82 ++++++++
 .../apache/solr/metrics/DelegateRegistryTimer.java | 130 +++++++++++++
 .../org/apache/solr/metrics/MetricSuppliers.java   |   2 +-
 .../SolrDelegateRegistryMetricsContext.java        |  84 ++++++++
 .../org/apache/solr/metrics/SolrMetricManager.java |   4 +
 .../cloud-aggregate-node-metrics/conf/schema.xml   |  29 +++
 .../conf/solrconfig.xml                            |  55 ++++++
 .../solr/handler/RequestHandlerMetricsTest.java    | 174 +++++++++++++++++
 .../solr/metrics/DelegateRegistryTimerTest.java    | 211 +++++++++++++++++++++
 .../deployment-guide/pages/metrics-reporting.adoc  |  16 ++
 14 files changed, 949 insertions(+), 2 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index d961883fec3..2a3d76270fd 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -25,6 +25,8 @@ New Features
 
 * SOLR-16282: CoreAdminHandler supports custom actions via solr.xml configuration. (Artem Abeleshev, Christine Poerschke)
 
+* SOLR-15007: Add ability to roll up core level metrics to be node level metrics for a RequestHandler via configuration. (Justin Sweeney, David Smiley)
+
 Improvements
 ---------------------
 * SOLR-15986: CommitUpdateCommand and SplitIndexCommand can write user commit metadata. (Bruno Roustant)
diff --git a/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java b/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
index f2ed4de9ef4..d7268212ea2 100644
--- a/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
+++ b/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
@@ -35,7 +35,9 @@ import org.apache.solr.core.MetricsConfig;
 import org.apache.solr.core.PluginBag;
 import org.apache.solr.core.PluginInfo;
 import org.apache.solr.core.SolrInfoBean;
+import org.apache.solr.metrics.SolrDelegateRegistryMetricsContext;
 import org.apache.solr.metrics.SolrMetricManager;
+import org.apache.solr.metrics.SolrMetricProducer;
 import org.apache.solr.metrics.SolrMetricsContext;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrRequestHandler;
@@ -60,6 +62,7 @@ public abstract class RequestHandlerBase
   protected SolrParams appends;
   protected SolrParams invariants;
   protected boolean httpCaching = true;
+  protected boolean aggregateNodeLevelMetricsEnabled = false;
 
   protected SolrMetricsContext solrMetricsContext;
   protected HandlerMetrics metrics = HandlerMetrics.NO_OP;
@@ -131,6 +134,11 @@ public abstract class RequestHandlerBase
     if (initArgs != null) {
       Object caching = initArgs.get("httpCaching");
       httpCaching = caching != null ? Boolean.parseBoolean(caching.toString()) : true;
+      Boolean aggregateNodeLevelMetricsEnabled =
+          initArgs.getBooleanArg("aggregateNodeLevelMetricsEnabled");
+      if (aggregateNodeLevelMetricsEnabled != null) {
+        this.aggregateNodeLevelMetricsEnabled = aggregateNodeLevelMetricsEnabled;
+      }
     }
   }
 
@@ -141,7 +149,16 @@ public abstract class RequestHandlerBase
 
   @Override
   public void initializeMetrics(SolrMetricsContext parentContext, String scope) {
-    this.solrMetricsContext = parentContext.getChildContext(this);
+    if (aggregateNodeLevelMetricsEnabled) {
+      this.solrMetricsContext =
+          new SolrDelegateRegistryMetricsContext(
+              parentContext.getMetricManager(),
+              parentContext.getRegistryName(),
+              SolrMetricProducer.getUniqueMetricTag(this, parentContext.getTag()),
+              SolrMetricManager.getRegistryName(SolrInfoBean.Group.node));
+    } else {
+      this.solrMetricsContext = parentContext.getChildContext(this);
+    }
     metrics = new HandlerMetrics(solrMetricsContext, getCategory().toString(), scope);
     solrMetricsContext.gauge(
         () -> handlerStart, true, "handlerStart", getCategory().toString(), scope);
diff --git a/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryCounter.java b/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryCounter.java
new file mode 100644
index 00000000000..c8cc2352818
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryCounter.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.metrics;
+
+import com.codahale.metrics.Counter;
+
+/**
+ * A counter implementation that is aware of both primary and delegate metrics belonging to
+ * different registries and able to update metrics in multiple registries
+ *
+ * @see SolrDelegateRegistryMetricsContext
+ */
+public class DelegateRegistryCounter extends Counter {
+
+  private final Counter primaryCounter;
+  private final Counter delegateCounter;
+
+  public DelegateRegistryCounter(Counter primaryCounter, Counter delegateCounter) {
+    this.primaryCounter = primaryCounter;
+    this.delegateCounter = delegateCounter;
+  }
+
+  @Override
+  public void inc() {
+    primaryCounter.inc();
+    delegateCounter.inc();
+  }
+
+  @Override
+  public void inc(long n) {
+    primaryCounter.inc(n);
+    delegateCounter.inc(n);
+  }
+
+  @Override
+  public void dec() {
+    primaryCounter.dec();
+    delegateCounter.dec();
+  }
+
+  @Override
+  public void dec(long n) {
+    primaryCounter.dec(n);
+    delegateCounter.dec(n);
+  }
+
+  @Override
+  public long getCount() {
+    return primaryCounter.getCount();
+  }
+
+  public Counter getPrimaryCounter() {
+    return primaryCounter;
+  }
+
+  public Counter getDelegateCounter() {
+    return delegateCounter;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryHistogram.java b/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryHistogram.java
new file mode 100644
index 00000000000..900641244ce
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryHistogram.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.metrics;
+
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Snapshot;
+
+/**
+ * A meter implementation that is aware of both primary and delegate metrics belonging to different
+ * registries and able to update metrics in multiple registries
+ *
+ * @see SolrDelegateRegistryMetricsContext
+ */
+public class DelegateRegistryHistogram extends Histogram {
+
+  private final Histogram primaryHistogram;
+  private final Histogram delegateHistogram;
+
+  public DelegateRegistryHistogram(Histogram primaryHistogram, Histogram delegateHistogram) {
+    super(null);
+    this.primaryHistogram = primaryHistogram;
+    this.delegateHistogram = delegateHistogram;
+  }
+
+  @Override
+  public void update(int value) {
+    primaryHistogram.update(value);
+    delegateHistogram.update(value);
+  }
+
+  @Override
+  public void update(long value) {
+    primaryHistogram.update(value);
+    delegateHistogram.update(value);
+  }
+
+  @Override
+  public long getCount() {
+    return primaryHistogram.getCount();
+  }
+
+  @Override
+  public Snapshot getSnapshot() {
+    return primaryHistogram.getSnapshot();
+  }
+
+  public Histogram getPrimaryHistogram() {
+    return primaryHistogram;
+  }
+
+  public Histogram getDelegateHistogram() {
+    return delegateHistogram;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryMeter.java b/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryMeter.java
new file mode 100644
index 00000000000..20903cc7f12
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryMeter.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.metrics;
+
+import com.codahale.metrics.Meter;
+
+/**
+ * A meter implementation that is aware of both primary and delegate metrics belonging to different
+ * registries and able to update metrics in multiple registries
+ *
+ * @see SolrDelegateRegistryMetricsContext
+ */
+public class DelegateRegistryMeter extends Meter {
+
+  private final Meter primaryMeter;
+  private final Meter delegateMeter;
+
+  public DelegateRegistryMeter(Meter primaryMeter, Meter delegateMeter) {
+    this.primaryMeter = primaryMeter;
+    this.delegateMeter = delegateMeter;
+  }
+
+  @Override
+  public void mark() {
+    primaryMeter.mark();
+    delegateMeter.mark();
+  }
+
+  @Override
+  public void mark(long n) {
+    primaryMeter.mark(n);
+    delegateMeter.mark(n);
+  }
+
+  @Override
+  public long getCount() {
+    return primaryMeter.getCount();
+  }
+
+  @Override
+  public double getFifteenMinuteRate() {
+    return primaryMeter.getFifteenMinuteRate();
+  }
+
+  @Override
+  public double getFiveMinuteRate() {
+    return primaryMeter.getFiveMinuteRate();
+  }
+
+  @Override
+  public double getMeanRate() {
+    return primaryMeter.getMeanRate();
+  }
+
+  @Override
+  public double getOneMinuteRate() {
+    return primaryMeter.getOneMinuteRate();
+  }
+
+  public Meter getPrimaryMeter() {
+    return primaryMeter;
+  }
+
+  public Meter getDelegateMeter() {
+    return delegateMeter;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryTimer.java b/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryTimer.java
new file mode 100644
index 00000000000..3519ee55cef
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/metrics/DelegateRegistryTimer.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.metrics;
+
+import com.codahale.metrics.Clock;
+import com.codahale.metrics.Snapshot;
+import com.codahale.metrics.Timer;
+import java.time.Duration;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+/**
+ * A timer implementation that is aware of both primary and delegate metrics belonging to different
+ * registries and able to update metrics in multiple registries
+ *
+ * @see SolrDelegateRegistryMetricsContext
+ */
+public class DelegateRegistryTimer extends Timer {
+
+  private final Timer primaryTimer;
+  private final Timer delegateTimer;
+  private final Clock clock;
+
+  public DelegateRegistryTimer(Clock clock, Timer primaryTimer, Timer delegateTimer) {
+    this.primaryTimer = primaryTimer;
+    this.delegateTimer = delegateTimer;
+    this.clock = clock;
+  }
+
+  @Override
+  public void update(long duration, TimeUnit unit) {
+    primaryTimer.update(duration, unit);
+    delegateTimer.update(duration, unit);
+  }
+
+  @Override
+  public void update(Duration duration) {
+    primaryTimer.update(duration);
+    delegateTimer.update(duration);
+  }
+
+  @Override
+  public <T> T time(Callable<T> event) throws Exception {
+    final long startTime = clock.getTick();
+    try {
+      return event.call();
+    } finally {
+      update(clock.getTick() - startTime, TimeUnit.NANOSECONDS);
+    }
+  }
+
+  @Override
+  public <T> T timeSupplier(Supplier<T> event) {
+    final long startTime = clock.getTick();
+    try {
+      return event.get();
+    } finally {
+      update(clock.getTick() - startTime, TimeUnit.NANOSECONDS);
+    }
+  }
+
+  @Override
+  public void time(Runnable event) {
+    final long startTime = clock.getTick();
+    try {
+      event.run();
+    } finally {
+      update(clock.getTick() - startTime, TimeUnit.NANOSECONDS);
+    }
+  }
+
+  @Override
+  public Context time() {
+    return super.time();
+  }
+
+  @Override
+  public long getCount() {
+    return primaryTimer.getCount();
+  }
+
+  @Override
+  public double getFifteenMinuteRate() {
+    return primaryTimer.getFifteenMinuteRate();
+  }
+
+  @Override
+  public double getFiveMinuteRate() {
+    return primaryTimer.getFiveMinuteRate();
+  }
+
+  @Override
+  public double getMeanRate() {
+    return primaryTimer.getMeanRate();
+  }
+
+  @Override
+  public double getOneMinuteRate() {
+    return primaryTimer.getOneMinuteRate();
+  }
+
+  @Override
+  public Snapshot getSnapshot() {
+    return primaryTimer.getSnapshot();
+  }
+
+  public Timer getPrimaryTimer() {
+    return primaryTimer;
+  }
+
+  public Timer getDelegateTimer() {
+    return delegateTimer;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/metrics/MetricSuppliers.java b/solr/core/src/java/org/apache/solr/metrics/MetricSuppliers.java
index f303bef6960..497be19d7f1 100644
--- a/solr/core/src/java/org/apache/solr/metrics/MetricSuppliers.java
+++ b/solr/core/src/java/org/apache/solr/metrics/MetricSuppliers.java
@@ -112,7 +112,7 @@ public class MetricSuppliers {
     }
   }
 
-  private static Clock getClock(PluginInfo info, String param) {
+  public static Clock getClock(PluginInfo info, String param) {
     if (info == null) {
       return Clock.defaultClock();
     }
diff --git a/solr/core/src/java/org/apache/solr/metrics/SolrDelegateRegistryMetricsContext.java b/solr/core/src/java/org/apache/solr/metrics/SolrDelegateRegistryMetricsContext.java
new file mode 100644
index 00000000000..bfab5f40b92
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrDelegateRegistryMetricsContext.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.metrics;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Timer;
+
+/**
+ * This class represents a metrics context that is delegate aware in that it is aware of multiple
+ * metric registries, a primary and a delegate. This enables creating metrics that are tracked at
+ * multiple levels, i.e. core-level and node-level. This class will create instances of new Timer,
+ * Meter, Counter, Histogram implementations that hold references to both primary and delegate
+ * implementations of corresponding classes. The DelegateRegistry* metric classes are just
+ * pass-through to two different implementations. As such the DelegateRegistry* metric classes do
+ * not hold any metric data themselves.
+ *
+ * @see org.apache.solr.metrics.SolrMetricsContext
+ */
+public class SolrDelegateRegistryMetricsContext extends SolrMetricsContext {
+
+  private final String delegateRegistry;
+
+  public SolrDelegateRegistryMetricsContext(
+      SolrMetricManager metricManager, String registry, String tag, String delegateRegistry) {
+    super(metricManager, registry, tag);
+    this.delegateRegistry = delegateRegistry;
+  }
+
+  @Override
+  public Meter meter(String metricName, String... metricPath) {
+    return new DelegateRegistryMeter(
+        super.meter(metricName, metricPath),
+        getMetricManager().meter(this, delegateRegistry, metricName, metricPath));
+  }
+
+  @Override
+  public Counter counter(String metricName, String... metricPath) {
+    return new DelegateRegistryCounter(
+        super.counter(metricName, metricPath),
+        getMetricManager().counter(this, delegateRegistry, metricName, metricPath));
+  }
+
+  @Override
+  public Timer timer(String metricName, String... metricPath) {
+    return new DelegateRegistryTimer(
+        MetricSuppliers.getClock(
+            getMetricManager().getMetricsConfig().getTimerSupplier(), MetricSuppliers.CLOCK),
+        super.timer(metricName, metricPath),
+        getMetricManager().timer(this, delegateRegistry, metricName, metricPath));
+  }
+
+  @Override
+  public Histogram histogram(String metricName, String... metricPath) {
+    return new DelegateRegistryHistogram(
+        super.histogram(metricName, metricPath),
+        getMetricManager().histogram(this, delegateRegistry, metricName, metricPath));
+  }
+
+  @Override
+  public SolrMetricsContext getChildContext(Object child) {
+    return new SolrDelegateRegistryMetricsContext(
+        getMetricManager(),
+        getRegistryName(),
+        SolrMetricProducer.getUniqueMetricTag(child, getTag()),
+        delegateRegistry);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java b/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
index 022de145224..bd63e9e8a22 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
@@ -1367,4 +1367,8 @@ public class SolrMetricManager {
       }
     }
   }
+
+  public MetricsConfig getMetricsConfig() {
+    return metricsConfig;
+  }
 }
diff --git a/solr/core/src/test-files/solr/configsets/cloud-aggregate-node-metrics/conf/schema.xml b/solr/core/src/test-files/solr/configsets/cloud-aggregate-node-metrics/conf/schema.xml
new file mode 100644
index 00000000000..4124feab0c3
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/cloud-aggregate-node-metrics/conf/schema.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+<schema name="minimal" version="1.1">
+  <fieldType name="string" class="solr.StrField"/>
+  <fieldType name="int" class="${solr.tests.IntegerFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
+  <fieldType name="long" class="${solr.tests.LongFieldType}" docValues="${solr.tests.numeric.dv}" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
+  <dynamicField name="*" type="string" indexed="true" stored="true"/>
+  <!-- for versioning -->
+  <field name="_version_" type="long" indexed="true" stored="true"/>
+  <field name="_root_" type="string" indexed="true" stored="true" multiValued="false" required="false"/>
+  <field name="id" type="string" indexed="true" stored="true"/>
+  <dynamicField name="*_s"  type="string"  indexed="true"  stored="true" />
+  <uniqueKey>id</uniqueKey>
+</schema>
diff --git a/solr/core/src/test-files/solr/configsets/cloud-aggregate-node-metrics/conf/solrconfig.xml b/solr/core/src/test-files/solr/configsets/cloud-aggregate-node-metrics/conf/solrconfig.xml
new file mode 100644
index 00000000000..f23456d060a
--- /dev/null
+++ b/solr/core/src/test-files/solr/configsets/cloud-aggregate-node-metrics/conf/solrconfig.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" ?>
+
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+
+<!-- Minimal solrconfig.xml with /select, /admin and /update only -->
+
+<config>
+
+  <dataDir>${solr.data.dir:}</dataDir>
+
+  <directoryFactory name="DirectoryFactory"
+                    class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+  <schemaFactory class="ClassicIndexSchemaFactory"/>
+
+  <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion>
+
+  <updateHandler class="solr.DirectUpdateHandler2">
+    <commitWithin>
+      <softCommit>${solr.commitwithin.softcommit:true}</softCommit>
+    </commitWithin>
+    <updateLog class="${solr.ulog:solr.UpdateLog}"></updateLog>
+  </updateHandler>
+
+  <requestHandler name="/update" class="solr.UpdateRequestHandler">
+    <str name="useParams">_UPDATE</str>
+    <bool name="aggregateNodeLevelMetricsEnabled">true</bool>
+  </requestHandler>
+
+  <requestHandler name="/select" class="solr.SearchHandler">
+    <lst name="defaults">
+      <str name="echoParams">explicit</str>
+      <str name="indent">true</str>
+      <str name="df">text</str>
+    </lst>
+    <bool name="aggregateNodeLevelMetricsEnabled">true</bool>
+  </requestHandler>
+  <indexConfig>
+    <mergeScheduler class="${solr.mscheduler:org.apache.lucene.index.ConcurrentMergeScheduler}"/>
+  </indexConfig>
+</config>
\ No newline at end of file
diff --git a/solr/core/src/test/org/apache/solr/handler/RequestHandlerMetricsTest.java b/solr/core/src/test/org/apache/solr/handler/RequestHandlerMetricsTest.java
new file mode 100644
index 00000000000..228ad8cc48b
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/RequestHandlerMetricsTest.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.handler;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.util.NamedList;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class RequestHandlerMetricsTest extends SolrCloudTestCase {
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    System.setProperty("metricsEnabled", "true");
+    configureCluster(1).addConfig("conf1", configset("cloud-aggregate-node-metrics")).configure();
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    cluster.deleteAllCollections();
+  }
+
+  @AfterClass
+  public static void afterClass() {
+    System.clearProperty("metricsEnabled");
+  }
+
+  @Test
+  @SuppressWarnings({"unchecked"})
+  public void testAggregateNodeLevelMetrics() throws SolrServerException, IOException {
+    String collection1 = "testRequestHandlerMetrics1";
+    String collection2 = "testRequestHandlerMetrics2";
+
+    CloudSolrClient cloudClient = cluster.getSolrClient();
+
+    CollectionAdminRequest.Create create =
+        CollectionAdminRequest.createCollection(collection1, "conf1", 1, 1);
+    cloudClient.request(create);
+    cluster.waitForActiveCollection(collection1, 1, 1);
+
+    create = CollectionAdminRequest.createCollection(collection2, "conf1", 1, 1);
+    cloudClient.request(create);
+    cluster.waitForActiveCollection(collection2, 1, 1);
+
+    SolrInputDocument solrInputDocument =
+        new SolrInputDocument("id", "10", "title", "test", "val_s1", "aaa");
+    cloudClient.add(collection1, solrInputDocument);
+    cloudClient.add(collection2, solrInputDocument);
+
+    SolrQuery solrQuery = new SolrQuery("*:*");
+    cloudClient.query(collection1, solrQuery);
+    cloudClient.query(collection2, solrQuery);
+
+    NamedList<Object> response =
+        cloudClient.request(new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/metrics", null));
+
+    NamedList<Object> metrics = (NamedList<Object>) response.get("metrics");
+
+    final double[] minQueryTime = {Double.MAX_VALUE};
+    final double[] maxQueryTime = {-1.0};
+    final double[] minUpdateTime = {Double.MAX_VALUE};
+    final double[] maxUpdateTime = {-1.0};
+    Set<NamedList<Object>> coreMetrics = new HashSet<>();
+    metrics.forEachKey(
+        (key) -> {
+          if (key.startsWith("solr.core.testRequestHandlerMetrics")) {
+            NamedList<Object> coreMetric = (NamedList<Object>) metrics.get(key);
+            coreMetrics.add(coreMetric);
+          }
+        });
+    assertEquals(2, coreMetrics.size());
+    coreMetrics.forEach(
+        metric -> {
+          assertEquals(
+              1L,
+              ((Map<String, Number>) metric.get("QUERY./select.requestTimes"))
+                  .get("count")
+                  .longValue());
+          minQueryTime[0] =
+              Math.min(
+                  minQueryTime[0],
+                  ((Map<String, Number>) metric.get("QUERY./select.requestTimes"))
+                      .get("min_ms")
+                      .doubleValue());
+          maxQueryTime[0] =
+              Math.max(
+                  maxQueryTime[0],
+                  ((Map<String, Number>) metric.get("QUERY./select.requestTimes"))
+                      .get("max_ms")
+                      .doubleValue());
+          assertEquals(
+              1L,
+              ((Map<String, Number>) metric.get("UPDATE./update.requestTimes"))
+                  .get("count")
+                  .longValue());
+          minUpdateTime[0] =
+              Math.min(
+                  minUpdateTime[0],
+                  ((Map<String, Number>) metric.get("UPDATE./update.requestTimes"))
+                      .get("min_ms")
+                      .doubleValue());
+          maxUpdateTime[0] =
+              Math.max(
+                  maxUpdateTime[0],
+                  ((Map<String, Number>) metric.get("UPDATE./update.requestTimes"))
+                      .get("max_ms")
+                      .doubleValue());
+        });
+
+    NamedList<Object> nodeMetrics = (NamedList<Object>) metrics.get("solr.node");
+    assertEquals(
+        2L,
+        ((Map<String, Number>) nodeMetrics.get("QUERY./select.requestTimes"))
+            .get("count")
+            .longValue());
+    assertEquals(
+        minQueryTime[0],
+        ((Map<String, Number>) nodeMetrics.get("QUERY./select.requestTimes"))
+            .get("min_ms")
+            .doubleValue(),
+        0.0);
+    assertEquals(
+        maxQueryTime[0],
+        ((Map<String, Number>) nodeMetrics.get("QUERY./select.requestTimes"))
+            .get("max_ms")
+            .doubleValue(),
+        0.0);
+    assertEquals(
+        2L,
+        ((Map<String, Number>) nodeMetrics.get("UPDATE./update.requestTimes"))
+            .get("count")
+            .longValue());
+    assertEquals(
+        minUpdateTime[0],
+        ((Map<String, Number>) nodeMetrics.get("UPDATE./update.requestTimes"))
+            .get("min_ms")
+            .doubleValue(),
+        0.0);
+    assertEquals(
+        maxUpdateTime[0],
+        ((Map<String, Number>) nodeMetrics.get("UPDATE./update.requestTimes"))
+            .get("max_ms")
+            .doubleValue(),
+        0.0);
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/metrics/DelegateRegistryTimerTest.java b/solr/core/src/test/org/apache/solr/metrics/DelegateRegistryTimerTest.java
new file mode 100644
index 00000000000..4a6f44453e6
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/metrics/DelegateRegistryTimerTest.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.solr.metrics;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.codahale.metrics.Clock;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import org.junit.Test;
+
+public class DelegateRegistryTimerTest {
+
+  MetricRegistry.MetricSupplier<Timer> timerSupplier =
+      new MetricSuppliers.DefaultTimerSupplier(null);
+
+  @Test
+  public void update() {
+    DelegateRegistryTimer delegateRegistryTimer =
+        new DelegateRegistryTimer(
+            Clock.defaultClock(), timerSupplier.newMetric(), timerSupplier.newMetric());
+    delegateRegistryTimer.update(Duration.ofNanos(100));
+    assertEquals(1, delegateRegistryTimer.getPrimaryTimer().getCount());
+    assertEquals(100.0, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(), 0.0);
+    assertEquals(100.0, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMedian(), 0.0);
+    assertEquals(100L, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMax());
+    assertEquals(100.0, delegateRegistryTimer.getSnapshot().getMean(), 0.0);
+    assertEquals(100.0, delegateRegistryTimer.getSnapshot().getMedian(), 0.0);
+    assertEquals(100L, delegateRegistryTimer.getSnapshot().getMax());
+    assertEquals(1, delegateRegistryTimer.getDelegateTimer().getCount());
+    assertEquals(100.0, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean(), 0.0);
+    assertEquals(100.0, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMedian(), 0.0);
+    assertEquals(100L, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMax());
+
+    delegateRegistryTimer.update(Duration.ofNanos(200));
+    delegateRegistryTimer.update(Duration.ofNanos(300));
+    assertEquals(3, delegateRegistryTimer.getPrimaryTimer().getCount());
+    assertEquals(200.0, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(), 0.0);
+    assertEquals(200.0, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMedian(), 0.0);
+    assertEquals(300L, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMax());
+    assertEquals(200.0, delegateRegistryTimer.getSnapshot().getMean(), 0.0);
+    assertEquals(200.0, delegateRegistryTimer.getSnapshot().getMedian(), 0.0);
+    assertEquals(300L, delegateRegistryTimer.getSnapshot().getMax());
+    assertEquals(3, delegateRegistryTimer.getDelegateTimer().getCount());
+    assertEquals(200.0, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean(), 0.0);
+    assertEquals(200.0, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMedian(), 0.0);
+    assertEquals(300L, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMax());
+  }
+
+  @Test
+  public void testUpdate() {
+    DelegateRegistryTimer delegateRegistryTimer =
+        new DelegateRegistryTimer(
+            Clock.defaultClock(), timerSupplier.newMetric(), timerSupplier.newMetric());
+    delegateRegistryTimer.update(100, TimeUnit.NANOSECONDS);
+    assertEquals(1, delegateRegistryTimer.getPrimaryTimer().getCount());
+    assertEquals(100, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(), 0.0);
+    assertEquals(100, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMedian(), 0.0);
+    assertEquals(100L, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMax());
+    assertEquals(100, delegateRegistryTimer.getSnapshot().getMean(), 0.0);
+    assertEquals(100, delegateRegistryTimer.getSnapshot().getMedian(), 0.0);
+    assertEquals(100L, delegateRegistryTimer.getSnapshot().getMax());
+    assertEquals(1, delegateRegistryTimer.getDelegateTimer().getCount());
+    assertEquals(100, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean(), 0.0);
+    assertEquals(100, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMedian(), 0.0);
+    assertEquals(100L, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMax());
+
+    delegateRegistryTimer.update(200, TimeUnit.NANOSECONDS);
+    delegateRegistryTimer.update(300, TimeUnit.NANOSECONDS);
+    assertEquals(3, delegateRegistryTimer.getPrimaryTimer().getCount());
+    assertEquals(200, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(), 0.0);
+    assertEquals(200, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMedian(), 0.0);
+    assertEquals(300L, delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMax());
+    assertEquals(200, delegateRegistryTimer.getSnapshot().getMean(), 0.0);
+    assertEquals(200, delegateRegistryTimer.getSnapshot().getMedian(), 0.0);
+    assertEquals(300L, delegateRegistryTimer.getSnapshot().getMax());
+    assertEquals(3, delegateRegistryTimer.getDelegateTimer().getCount());
+    assertEquals(200, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean(), 0.0);
+    assertEquals(200, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMedian(), 0.0);
+    assertEquals(300L, delegateRegistryTimer.getDelegateTimer().getSnapshot().getMax());
+  }
+
+  @Test
+  public void timeContext() throws InterruptedException {
+    DelegateRegistryTimer delegateRegistryTimer =
+        new DelegateRegistryTimer(
+            Clock.defaultClock(), timerSupplier.newMetric(), timerSupplier.newMetric());
+    Timer.Context time = delegateRegistryTimer.time();
+    Thread.sleep(100);
+    time.close();
+    assertTrue(delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean() > 100000);
+    assertTrue(delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean() > 100000);
+    assertEquals(
+        delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(),
+        delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean(),
+        0.0);
+    assertEquals(
+        delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(),
+        delegateRegistryTimer.getSnapshot().getMean(),
+        0.0);
+  }
+
+  @Test
+  public void timeSupplier() {
+    DelegateRegistryTimer delegateRegistryTimer =
+        new DelegateRegistryTimer(
+            Clock.defaultClock(), timerSupplier.newMetric(), timerSupplier.newMetric());
+    AtomicLong timeTaken = new AtomicLong();
+    Long supplierResult =
+        delegateRegistryTimer.timeSupplier(
+            () -> {
+              timeTaken.getAndSet(System.nanoTime());
+              for (int i = 0; i < 100; i++) {
+                // Just loop
+              }
+              timeTaken.getAndSet(System.nanoTime() - timeTaken.get());
+              return 1L;
+            });
+    assertEquals(Long.valueOf(1L), supplierResult);
+    assertEquals(1, delegateRegistryTimer.getPrimaryTimer().getCount());
+    assertEquals(1, delegateRegistryTimer.getDelegateTimer().getCount());
+    assertTrue(delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean() > timeTaken.get());
+    assertTrue(delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean() > timeTaken.get());
+    assertEquals(
+        delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(),
+        delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean(),
+        0.0);
+    assertEquals(
+        delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(),
+        delegateRegistryTimer.getSnapshot().getMean(),
+        0.0);
+  }
+
+  @Test
+  public void testTimeCallable() throws Exception {
+    DelegateRegistryTimer delegateRegistryTimer =
+        new DelegateRegistryTimer(
+            Clock.defaultClock(), timerSupplier.newMetric(), timerSupplier.newMetric());
+    AtomicLong timeTaken = new AtomicLong();
+    Long callableResult =
+        delegateRegistryTimer.time(
+            () -> {
+              timeTaken.getAndSet(System.nanoTime());
+              for (int i = 0; i < 100; i++) {
+                // Just loop
+              }
+              timeTaken.getAndSet(System.nanoTime() - timeTaken.get());
+              return 1L;
+            });
+    assertEquals(Long.valueOf(1L), callableResult);
+    assertEquals(1, delegateRegistryTimer.getPrimaryTimer().getCount());
+    assertEquals(1, delegateRegistryTimer.getDelegateTimer().getCount());
+    assertTrue(delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean() > timeTaken.get());
+    assertTrue(delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean() > timeTaken.get());
+    assertEquals(
+        delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(),
+        delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean(),
+        0.0);
+    assertEquals(
+        delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(),
+        delegateRegistryTimer.getSnapshot().getMean(),
+        0.0);
+  }
+
+  @Test
+  public void testTimeRunnable() {
+    DelegateRegistryTimer delegateRegistryTimer =
+        new DelegateRegistryTimer(
+            Clock.defaultClock(), timerSupplier.newMetric(), timerSupplier.newMetric());
+    AtomicLong timeTaken = new AtomicLong();
+    delegateRegistryTimer.time(
+        () -> {
+          timeTaken.getAndSet(System.nanoTime());
+          for (int i = 0; i < 100; i++) {
+            // Just loop
+          }
+          timeTaken.getAndSet(System.nanoTime() - timeTaken.get());
+        });
+    assertEquals(1, delegateRegistryTimer.getPrimaryTimer().getCount());
+    assertEquals(1, delegateRegistryTimer.getDelegateTimer().getCount());
+    assertTrue(delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean() > timeTaken.get());
+    assertTrue(delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean() > timeTaken.get());
+    assertEquals(
+        delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(),
+        delegateRegistryTimer.getDelegateTimer().getSnapshot().getMean(),
+        0.0);
+    assertEquals(
+        delegateRegistryTimer.getPrimaryTimer().getSnapshot().getMean(),
+        delegateRegistryTimer.getSnapshot().getMean(),
+        0.0);
+  }
+}
diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/metrics-reporting.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/metrics-reporting.adoc
index 74b2cd7cf87..b97a25280cf 100644
--- a/solr/solr-ref-guide/modules/deployment-guide/pages/metrics-reporting.adoc
+++ b/solr/solr-ref-guide/modules/deployment-guide/pages/metrics-reporting.adoc
@@ -91,6 +91,22 @@ Handlers that support process distributed shard requests also report `shardReque
 * shard replication and transaction log replay on replicas,
 * open / available / pending connections for shard handler and update handler.
 
+RequestHandlers can be configured to roll up core level metrics to the node level in addition to reporting them per core. This is useful if you have a large number of cores per node and are interested in aggregate metrics per node. This is configured by adding `<bool name="aggregateNodeLevelMetricsEnabled">true</bool>` to a xref:configuration-guide:requesthandlers-searchcomponents.adoc#configuring-request-handlers[RequestHandler configuration] in your solrconfig.xml, for example:
+
+```
+<requestHandler name="/select" class="solr.SearchHandler">
+    <!-- default values for query parameters can be specified, these
+         will be overridden by parameters in the request
+      -->
+    <lst name="defaults">
+        <str name="echoParams">explicit</str>
+        <int name="rows">10</int>
+    </lst>
+
+    <bool name="aggregateNodeLevelMetricsEnabled">true</bool>
+</requestHandler>
+```
+
 === Jetty Registry
 
 This registry is returned at `solr.jetty` and includes the following information.