You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by is...@apache.org on 2017/03/12 00:18:42 UTC

[33/50] [abbrv] lucene-solr:jira/solr-6736: SOLR-9858: Collect aggregated metrics from nodes and shard leaders in overseer.

SOLR-9858: Collect aggregated metrics from nodes and shard leaders in overseer.


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/4d7bc947
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/4d7bc947
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/4d7bc947

Branch: refs/heads/jira/solr-6736
Commit: 4d7bc9477144937335e997ad630c4b89f558ddc5
Parents: a6e14ec
Author: Andrzej Bialecki <ab...@apache.org>
Authored: Tue Mar 7 22:00:38 2017 +0100
Committer: Andrzej Bialecki <ab...@apache.org>
Committed: Tue Mar 7 22:01:21 2017 +0100

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   4 +
 .../org/apache/solr/cloud/ElectionContext.java  |   5 +-
 .../java/org/apache/solr/cloud/Overseer.java    |   7 +-
 .../solr/cloud/OverseerNodePrioritizer.java     |   2 +-
 .../solr/cloud/OverseerTaskProcessor.java       |   6 +-
 .../org/apache/solr/cloud/ZkController.java     |   2 +-
 .../org/apache/solr/core/CoreContainer.java     |  30 +-
 .../org/apache/solr/core/JmxMonitoredMap.java   |   9 +-
 .../src/java/org/apache/solr/core/SolrCore.java |   4 +-
 .../org/apache/solr/core/SolrInfoMBean.java     |   4 +-
 .../org/apache/solr/core/SolrXmlConfig.java     |   3 +-
 .../handler/admin/MetricsCollectorHandler.java  | 228 +++++++++++
 .../solr/handler/admin/MetricsHandler.java      |   2 +-
 .../apache/solr/metrics/AggregateMetric.java    | 200 ++++++++++
 .../solr/metrics/SolrCoreMetricManager.java     | 125 +++++-
 .../apache/solr/metrics/SolrMetricManager.java  | 325 ++++++++++++++-
 .../metrics/reporters/JmxObjectNameFactory.java |   6 +-
 .../reporters/solr/SolrClusterReporter.java     | 277 +++++++++++++
 .../metrics/reporters/solr/SolrReporter.java    | 392 +++++++++++++++++++
 .../reporters/solr/SolrShardReporter.java       | 188 +++++++++
 .../metrics/reporters/solr/package-info.java    |  22 ++
 .../java/org/apache/solr/update/PeerSync.java   |   8 +-
 .../org/apache/solr/util/stats/MetricUtils.java | 265 +++++++++----
 .../src/test-files/solr/solr-solrreporter.xml   |  66 ++++
 .../apache/solr/cloud/TestCloudRecovery.java    |   6 +-
 .../apache/solr/core/TestJmxMonitoredMap.java   |   2 +-
 .../solr/metrics/SolrCoreMetricManagerTest.java |  31 +-
 .../solr/metrics/SolrMetricManagerTest.java     |  30 +-
 .../metrics/SolrMetricsIntegrationTest.java     |  15 +-
 .../metrics/reporters/SolrJmxReporterTest.java  |  13 +-
 .../reporters/solr/SolrCloudReportersTest.java  | 163 ++++++++
 .../reporters/solr/SolrShardReporterTest.java   | 117 ++++++
 .../apache/solr/util/stats/MetricUtilsTest.java |  54 ++-
 .../client/solrj/impl/BinaryRequestWriter.java  |   4 +-
 .../solr/client/solrj/io/SolrClientCache.java   |  26 +-
 .../client/solrj/request/TestCoreAdmin.java     |   4 +-
 36 files changed, 2435 insertions(+), 210 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index dc97456..0e78535 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -50,6 +50,10 @@ Upgrading from Solr 6.x
   factors should be indexed in a separate field and combined with the query
   score using a function query.
 
+New Features
+----------------------
+* SOLR-9857, SOLR-9858: Collect aggregated metrics from nodes and shard leaders in overseer. (ab)
+
 Bug Fixes
 ----------------------
 * SOLR-9262: Connection and read timeouts are being ignored by UpdateShardHandler after SOLR-4509.

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/cloud/ElectionContext.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/ElectionContext.java b/solr/core/src/java/org/apache/solr/cloud/ElectionContext.java
index ff6fb30..d3ad322 100644
--- a/solr/core/src/java/org/apache/solr/cloud/ElectionContext.java
+++ b/solr/core/src/java/org/apache/solr/cloud/ElectionContext.java
@@ -714,14 +714,13 @@ final class OverseerElectionContext extends ElectionContext {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
   private final SolrZkClient zkClient;
   private Overseer overseer;
-  public static final String OVERSEER_ELECT = "/overseer_elect";
 
   public OverseerElectionContext(SolrZkClient zkClient, Overseer overseer, final String zkNodeName) {
-    super(zkNodeName, OVERSEER_ELECT, OVERSEER_ELECT + "/leader", null, zkClient);
+    super(zkNodeName, Overseer.OVERSEER_ELECT, Overseer.OVERSEER_ELECT + "/leader", null, zkClient);
     this.overseer = overseer;
     this.zkClient = zkClient;
     try {
-      new ZkCmdExecutor(zkClient.getZkClientTimeout()).ensureExists(OVERSEER_ELECT, zkClient);
+      new ZkCmdExecutor(zkClient.getZkClientTimeout()).ensureExists(Overseer.OVERSEER_ELECT, zkClient);
     } catch (KeeperException e) {
       throw new SolrException(ErrorCode.SERVER_ERROR, e);
     } catch (InterruptedException e) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/cloud/Overseer.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/Overseer.java b/solr/core/src/java/org/apache/solr/cloud/Overseer.java
index 3a8aa3e..61f15fc 100644
--- a/solr/core/src/java/org/apache/solr/cloud/Overseer.java
+++ b/solr/core/src/java/org/apache/solr/cloud/Overseer.java
@@ -65,7 +65,8 @@ public class Overseer implements Closeable {
   public static final int STATE_UPDATE_DELAY = 1500;  // delay between cloud state updates
 
   public static final int NUM_RESPONSES_TO_STORE = 10000;
-  
+  public static final String OVERSEER_ELECT = "/overseer_elect";
+
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   enum LeaderStatus {DONT_KNOW, NO, YES}
@@ -281,7 +282,7 @@ public class Overseer implements Closeable {
     private void checkIfIamStillLeader() {
       if (zkController != null && zkController.getCoreContainer().isShutDown()) return;//shutting down no need to go further
       org.apache.zookeeper.data.Stat stat = new org.apache.zookeeper.data.Stat();
-      String path = OverseerElectionContext.OVERSEER_ELECT + "/leader";
+      String path = OVERSEER_ELECT + "/leader";
       byte[] data;
       try {
         data = zkClient.getData(path, null, stat, true);
@@ -394,7 +395,7 @@ public class Overseer implements Closeable {
       boolean success = true;
       try {
         ZkNodeProps props = ZkNodeProps.load(zkClient.getData(
-            OverseerElectionContext.OVERSEER_ELECT + "/leader", null, null, true));
+            OVERSEER_ELECT + "/leader", null, null, true));
         if (myId.equals(props.getStr("id"))) {
           return LeaderStatus.YES;
         }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/cloud/OverseerNodePrioritizer.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/OverseerNodePrioritizer.java b/solr/core/src/java/org/apache/solr/cloud/OverseerNodePrioritizer.java
index 6512d26..798eca3 100644
--- a/solr/core/src/java/org/apache/solr/cloud/OverseerNodePrioritizer.java
+++ b/solr/core/src/java/org/apache/solr/cloud/OverseerNodePrioritizer.java
@@ -65,7 +65,7 @@ public class OverseerNodePrioritizer {
     String ldr = OverseerTaskProcessor.getLeaderNode(zk);
     if(overseerDesignates.contains(ldr)) return;
     log.info("prioritizing overseer nodes at {} overseer designates are {}", overseerId, overseerDesignates);
-    List<String> electionNodes = OverseerTaskProcessor.getSortedElectionNodes(zk, OverseerElectionContext.OVERSEER_ELECT + LeaderElector.ELECTION_NODE);
+    List<String> electionNodes = OverseerTaskProcessor.getSortedElectionNodes(zk, Overseer.OVERSEER_ELECT + LeaderElector.ELECTION_NODE);
     if(electionNodes.size()<2) return;
     log.info("sorted nodes {}", electionNodes);
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/cloud/OverseerTaskProcessor.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/OverseerTaskProcessor.java b/solr/core/src/java/org/apache/solr/cloud/OverseerTaskProcessor.java
index ad53346..bed71a6 100644
--- a/solr/core/src/java/org/apache/solr/cloud/OverseerTaskProcessor.java
+++ b/solr/core/src/java/org/apache/solr/cloud/OverseerTaskProcessor.java
@@ -337,7 +337,7 @@ public class OverseerTaskProcessor implements Runnable, Closeable {
   public static List<String> getSortedOverseerNodeNames(SolrZkClient zk) throws KeeperException, InterruptedException {
     List<String> children = null;
     try {
-      children = zk.getChildren(OverseerElectionContext.OVERSEER_ELECT + LeaderElector.ELECTION_NODE, null, true);
+      children = zk.getChildren(Overseer.OVERSEER_ELECT + LeaderElector.ELECTION_NODE, null, true);
     } catch (Exception e) {
       log.warn("error ", e);
       return new ArrayList<>();
@@ -370,7 +370,7 @@ public class OverseerTaskProcessor implements Runnable, Closeable {
   public static String getLeaderId(SolrZkClient zkClient) throws KeeperException,InterruptedException{
     byte[] data = null;
     try {
-      data = zkClient.getData(OverseerElectionContext.OVERSEER_ELECT + "/leader", null, new Stat(), true);
+      data = zkClient.getData(Overseer.OVERSEER_ELECT + "/leader", null, new Stat(), true);
     } catch (KeeperException.NoNodeException e) {
       return null;
     }
@@ -384,7 +384,7 @@ public class OverseerTaskProcessor implements Runnable, Closeable {
     boolean success = true;
     try {
       ZkNodeProps props = ZkNodeProps.load(zkStateReader.getZkClient().getData(
-          OverseerElectionContext.OVERSEER_ELECT + "/leader", null, null, true));
+          Overseer.OVERSEER_ELECT + "/leader", null, null, true));
       if (myId.equals(props.getStr("id"))) {
         return LeaderStatus.YES;
       }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/cloud/ZkController.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/ZkController.java b/solr/core/src/java/org/apache/solr/cloud/ZkController.java
index c083736..333acd4 100644
--- a/solr/core/src/java/org/apache/solr/cloud/ZkController.java
+++ b/solr/core/src/java/org/apache/solr/cloud/ZkController.java
@@ -1715,7 +1715,7 @@ public class ZkController {
           //however delete it . This is possible when the last attempt at deleting the election node failed.
           if (electionNode.startsWith(getNodeName())) {
             try {
-              zkClient.delete(OverseerElectionContext.OVERSEER_ELECT + LeaderElector.ELECTION_NODE + "/" + electionNode, -1, true);
+              zkClient.delete(Overseer.OVERSEER_ELECT + LeaderElector.ELECTION_NODE + "/" + electionNode, -1, true);
             } catch (NoNodeException e) {
               //no problem
             } catch (InterruptedException e) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/core/CoreContainer.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index e3977d7..b9597ae 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -69,6 +69,7 @@ import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.handler.admin.ConfigSetsHandler;
 import org.apache.solr.handler.admin.CoreAdminHandler;
 import org.apache.solr.handler.admin.InfoHandler;
+import org.apache.solr.handler.admin.MetricsCollectorHandler;
 import org.apache.solr.handler.admin.MetricsHandler;
 import org.apache.solr.handler.admin.SecurityConfHandler;
 import org.apache.solr.handler.admin.SecurityConfHandlerLocal;
@@ -177,6 +178,8 @@ public class CoreContainer {
 
   protected MetricsHandler metricsHandler;
 
+  protected MetricsCollectorHandler metricsCollectorHandler;
+
   private enum CoreInitFailedAction { fromleader, none }
 
   /**
@@ -511,15 +514,18 @@ public class CoreContainer {
     coreAdminHandler   = createHandler(CORES_HANDLER_PATH, cfg.getCoreAdminHandlerClass(), CoreAdminHandler.class);
     configSetsHandler = createHandler(CONFIGSETS_HANDLER_PATH, cfg.getConfigSetsHandlerClass(), ConfigSetsHandler.class);
     metricsHandler = createHandler(METRICS_PATH, MetricsHandler.class.getName(), MetricsHandler.class);
+    metricsCollectorHandler = createHandler(MetricsCollectorHandler.HANDLER_PATH, MetricsCollectorHandler.class.getName(), MetricsCollectorHandler.class);
+    // may want to add some configuration here in the future
+    metricsCollectorHandler.init(null);
     containerHandlers.put(AUTHZ_PATH, securityConfHandler);
     securityConfHandler.initializeMetrics(metricManager, SolrInfoMBean.Group.node.toString(), AUTHZ_PATH);
     containerHandlers.put(AUTHC_PATH, securityConfHandler);
     if(pkiAuthenticationPlugin != null)
       containerHandlers.put(PKIAuthenticationPlugin.PATH, pkiAuthenticationPlugin.getRequestHandler());
 
-    metricManager.loadReporters(cfg.getMetricReporterPlugins(), loader, SolrInfoMBean.Group.node);
-    metricManager.loadReporters(cfg.getMetricReporterPlugins(), loader, SolrInfoMBean.Group.jvm);
-    metricManager.loadReporters(cfg.getMetricReporterPlugins(), loader, SolrInfoMBean.Group.jetty);
+    metricManager.loadReporters(cfg.getMetricReporterPlugins(), loader, null, SolrInfoMBean.Group.node);
+    metricManager.loadReporters(cfg.getMetricReporterPlugins(), loader, null, SolrInfoMBean.Group.jvm);
+    metricManager.loadReporters(cfg.getMetricReporterPlugins(), loader, null, SolrInfoMBean.Group.jetty);
 
     coreConfigService = ConfigSetService.createConfigSetService(cfg, loader, zkSys.zkController);
 
@@ -537,6 +543,10 @@ public class CoreContainer {
     metricManager.register(SolrMetricManager.getRegistryName(SolrInfoMBean.Group.node),
         unloadedCores, true, "unloaded",SolrInfoMBean.Category.CONTAINER.toString(), "cores");
 
+    if (isZooKeeperAware()) {
+      metricManager.loadClusterReporters(cfg.getMetricReporterPlugins(), this);
+    }
+
     // setup executor to load cores in parallel
     ExecutorService coreLoadExecutor = MetricUtils.instrumentedExecutorService(
         ExecutorUtil.newMDCAwareFixedThreadPool(
@@ -660,10 +670,16 @@ public class CoreContainer {
     isShutDown = true;
 
     ExecutorUtil.shutdownAndAwaitTermination(coreContainerWorkExecutor);
+    if (metricManager != null) {
+      metricManager.closeReporters(SolrMetricManager.getRegistryName(SolrInfoMBean.Group.node));
+    }
 
     if (isZooKeeperAware()) {
       cancelCoreRecoveries();
-      zkSys.zkController.publishNodeAsDown(zkSys.zkController.getNodeName()); 
+      zkSys.zkController.publishNodeAsDown(zkSys.zkController.getNodeName());
+      if (metricManager != null) {
+        metricManager.closeReporters(SolrMetricManager.getRegistryName(SolrInfoMBean.Group.cluster));
+      }
     }
 
     try {
@@ -722,10 +738,6 @@ public class CoreContainer {
       }
     }
 
-    if (metricManager != null) {
-      metricManager.closeReporters(SolrMetricManager.getRegistryName(SolrInfoMBean.Group.node));
-    }
-
     // It should be safe to close the authorization plugin at this point.
     try {
       if(authorizationPlugin != null) {
@@ -1232,7 +1244,7 @@ public class CoreContainer {
     try (SolrCore core = getCore(name)) {
       if (core != null) {
         String oldRegistryName = core.getCoreMetricManager().getRegistryName();
-        String newRegistryName = SolrCoreMetricManager.createRegistryName(core.getCoreDescriptor().getCollectionName(), toName);
+        String newRegistryName = SolrCoreMetricManager.createRegistryName(core, toName);
         metricManager.swapRegistries(oldRegistryName, newRegistryName);
         registerCore(toName, core, true, false);
         SolrCore old = solrCores.remove(name);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/core/JmxMonitoredMap.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/JmxMonitoredMap.java b/solr/core/src/java/org/apache/solr/core/JmxMonitoredMap.java
index b2a5c79..8bfa662 100644
--- a/solr/core/src/java/org/apache/solr/core/JmxMonitoredMap.java
+++ b/solr/core/src/java/org/apache/solr/core/JmxMonitoredMap.java
@@ -20,6 +20,7 @@ import javax.management.Attribute;
 import javax.management.AttributeList;
 import javax.management.AttributeNotFoundException;
 import javax.management.DynamicMBean;
+import javax.management.InstanceNotFoundException;
 import javax.management.InvalidAttributeValueException;
 import javax.management.MBeanAttributeInfo;
 import javax.management.MBeanException;
@@ -53,7 +54,6 @@ import org.apache.lucene.store.AlreadyClosedException;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.core.SolrConfig.JmxConfiguration;
-import org.apache.solr.metrics.SolrCoreMetricManager;
 import org.apache.solr.metrics.reporters.JmxObjectNameFactory;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -93,9 +93,10 @@ public class JmxMonitoredMap<K, V> extends
 
   private final String registryName;
 
-  public JmxMonitoredMap(String coreName, String coreHashCode,
+  public JmxMonitoredMap(String coreName, String coreHashCode, String registryName,
                          final JmxConfiguration jmxConfig) {
     this.coreHashCode = coreHashCode;
+    this.registryName = registryName;
     jmxRootName = (null != jmxConfig.rootName ?
                    jmxConfig.rootName
                    : ("solr" + (null != coreName ? "/" + coreName : "")));
@@ -117,7 +118,6 @@ public class JmxMonitoredMap<K, V> extends
 
       if (servers == null || servers.isEmpty()) {
         server = null;
-        registryName = null;
         nameFactory = null;
         log.debug("No JMX servers found, not exposing Solr information with JMX.");
         return;
@@ -141,7 +141,6 @@ public class JmxMonitoredMap<K, V> extends
       }
       server = newServer;
     }
-    registryName = SolrCoreMetricManager.createRegistryName(null, coreName);
     nameFactory = new JmxObjectNameFactory(REPORTER_NAME + coreHashCode, registryName);
   }
 
@@ -166,6 +165,8 @@ public class JmxMonitoredMap<K, V> extends
         for (ObjectName name : objectNames) {
           try {
             server.unregisterMBean(name);
+          } catch (InstanceNotFoundException ie) {
+            // ignore - someone else already deleted this one
           } catch (Exception e) {
             log.warn("Exception un-registering mbean {}", name, e);
           }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/core/SolrCore.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/SolrCore.java b/solr/core/src/java/org/apache/solr/core/SolrCore.java
index f22c472..13c3bdd 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java
@@ -860,6 +860,7 @@ public final class SolrCore implements SolrInfoMBean, Closeable {
     this.configSetProperties = configSetProperties;
     // Initialize the metrics manager
     this.coreMetricManager = initCoreMetricManager(config);
+    this.coreMetricManager.loadReporters();
 
     if (updateHandler == null) {
       directoryFactory = initDirectoryFactory();
@@ -1101,13 +1102,12 @@ public final class SolrCore implements SolrInfoMBean, Closeable {
    */
   private SolrCoreMetricManager initCoreMetricManager(SolrConfig config) {
     SolrCoreMetricManager coreMetricManager = new SolrCoreMetricManager(this);
-    coreMetricManager.loadReporters();
     return coreMetricManager;
   }
 
   private Map<String,SolrInfoMBean> initInfoRegistry(String name, SolrConfig config) {
     if (config.jmxConfig.enabled) {
-      return new JmxMonitoredMap<String, SolrInfoMBean>(name, String.valueOf(this.hashCode()), config.jmxConfig);
+      return new JmxMonitoredMap<String, SolrInfoMBean>(name, coreMetricManager.getRegistryName(), String.valueOf(this.hashCode()), config.jmxConfig);
     } else  {
       log.debug("JMX monitoring not detected for core: " + name);
       return new ConcurrentHashMap<>();

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/core/SolrInfoMBean.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/SolrInfoMBean.java b/solr/core/src/java/org/apache/solr/core/SolrInfoMBean.java
index bf77db4..63bdef0 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrInfoMBean.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrInfoMBean.java
@@ -36,9 +36,9 @@ public interface SolrInfoMBean {
     SEARCHER, REPLICATION, TLOG, INDEX, DIRECTORY, HTTP, OTHER }
 
   /**
-   * Top-level group of beans for a subsystem.
+   * Top-level group of beans or metrics for a subsystem.
    */
-  enum Group { jvm, jetty, node, core }
+  enum Group { jvm, jetty, node, core, collection, shard, cluster, overseer }
 
   /**
    * Simple common usage name, e.g. BasicQueryHandler,

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java b/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
index e41cd8d..951d8d5 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
@@ -451,7 +451,8 @@ public class SolrXmlConfig {
       return new PluginInfo[0];
     PluginInfo[] configs = new PluginInfo[nodes.getLength()];
     for (int i = 0; i < nodes.getLength(); i++) {
-      configs[i] = new PluginInfo(nodes.item(i), "SolrMetricReporter", true, true);
+      // we don't require class in order to support predefined replica and node reporter classes
+      configs[i] = new PluginInfo(nodes.item(i), "SolrMetricReporter", true, false);
     }
     return configs;
   }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/handler/admin/MetricsCollectorHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/MetricsCollectorHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/MetricsCollectorHandler.java
new file mode 100644
index 0000000..de39a61
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/admin/MetricsCollectorHandler.java
@@ -0,0 +1,228 @@
+/*
+ * 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.admin;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.codahale.metrics.MetricRegistry;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.loader.ContentStreamLoader;
+import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.handler.loader.CSVLoader;
+import org.apache.solr.handler.loader.JavabinLoader;
+import org.apache.solr.handler.loader.JsonLoader;
+import org.apache.solr.handler.loader.XMLLoader;
+import org.apache.solr.metrics.AggregateMetric;
+import org.apache.solr.metrics.SolrMetricManager;
+import org.apache.solr.metrics.reporters.solr.SolrReporter;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.update.AddUpdateCommand;
+import org.apache.solr.update.CommitUpdateCommand;
+import org.apache.solr.update.DeleteUpdateCommand;
+import org.apache.solr.update.MergeIndexesCommand;
+import org.apache.solr.update.RollbackUpdateCommand;
+import org.apache.solr.update.processor.UpdateRequestProcessor;
+import org.apache.solr.util.stats.MetricUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler to collect and aggregate metric reports.  Each report indicates the target registry where
+ * metrics values should be collected and aggregated. Metrics with the same names are
+ * aggregated using {@link AggregateMetric} instances, which track the source of updates and
+ * their count, as well as providing simple statistics over collected values.
+ *
+ * Each report consists of {@link SolrInputDocument}-s that are expected to contain
+ * the following fields:
+ * <ul>
+ *   <li>{@link SolrReporter#GROUP_ID} - (required) specifies target registry name where metrics will be grouped.</li>
+ *   <li>{@link SolrReporter#REPORTER_ID} - (required) id of the reporter that sent this update. This can be eg.
+ *   node name or replica name or other id that uniquely identifies the source of metrics values.</li>
+ *   <li>{@link MetricUtils#METRIC_NAME} - (required) metric name (in the source registry)</li>
+ *   <li>{@link SolrReporter#LABEL_ID} - (optional) label to prepend to metric names in the target registry.</li>
+ *   <li>{@link SolrReporter#REGISTRY_ID} - (optional) name of the source registry.</li>
+ * </ul>
+ * Remaining fields are assumed to be single-valued, and to contain metric attributes and their values. Example:
+ * <pre>
+ *   &lt;doc&gt;
+ *     &lt;field name="_group_"&gt;solr.core.collection1.shard1.leader&lt;/field&gt;
+ *     &lt;field name="_reporter_"&gt;core_node3&lt;/field&gt;
+ *     &lt;field name="metric"&gt;INDEX.merge.errors&lt;/field&gt;
+ *     &lt;field name="value"&gt;0&lt;/field&gt;
+ *   &lt;/doc&gt;
+ * </pre>
+ */
+public class MetricsCollectorHandler extends RequestHandlerBase {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  public static final String HANDLER_PATH = "/admin/metrics/collector";
+
+  private final CoreContainer coreContainer;
+  private final SolrMetricManager metricManager;
+  private final Map<String, ContentStreamLoader> loaders = new HashMap<>();
+  private SolrParams params;
+
+  public MetricsCollectorHandler(final CoreContainer coreContainer) {
+    this.coreContainer = coreContainer;
+    this.metricManager = coreContainer.getMetricManager();
+
+  }
+
+  @Override
+  public void init(NamedList initArgs) {
+    super.init(initArgs);
+    if (initArgs != null) {
+      params = SolrParams.toSolrParams(initArgs);
+    } else {
+      params = new ModifiableSolrParams();
+    }
+    loaders.put("application/xml", new XMLLoader().init(params) );
+    loaders.put("application/json", new JsonLoader().init(params) );
+    loaders.put("application/csv", new CSVLoader().init(params) );
+    loaders.put("application/javabin", new JavabinLoader().init(params) );
+    loaders.put("text/csv", loaders.get("application/csv") );
+    loaders.put("text/xml", loaders.get("application/xml") );
+    loaders.put("text/json", loaders.get("application/json"));
+  }
+
+  @Override
+  public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+    if (coreContainer == null || coreContainer.isShutDown()) {
+      // silently drop request
+      return;
+    }
+    //log.info("#### " + req.toString());
+    if (req.getContentStreams() == null) { // no content
+      return;
+    }
+    for (ContentStream cs : req.getContentStreams()) {
+      if (cs.getContentType() == null) {
+        log.warn("Missing content type - ignoring");
+        continue;
+      }
+      ContentStreamLoader loader = loaders.get(cs.getContentType());
+      if (loader == null) {
+        throw new SolrException(SolrException.ErrorCode.UNSUPPORTED_MEDIA_TYPE, "Unsupported content type for stream: " + cs.getSourceInfo() + ", contentType=" + cs.getContentType());
+      }
+      loader.load(req, rsp, cs, new MetricUpdateProcessor(metricManager));
+    }
+  }
+
+  @Override
+  public String getDescription() {
+    return "Handler for collecting and aggregating metric reports.";
+  }
+
+  private static class MetricUpdateProcessor extends UpdateRequestProcessor {
+    private final SolrMetricManager metricManager;
+
+    public MetricUpdateProcessor(SolrMetricManager metricManager) {
+      super(null);
+      this.metricManager = metricManager;
+    }
+
+    @Override
+    public void processAdd(AddUpdateCommand cmd) throws IOException {
+      SolrInputDocument doc = cmd.solrDoc;
+      if (doc == null) {
+        return;
+      }
+      String metricName = (String)doc.getFieldValue(MetricUtils.METRIC_NAME);
+      if (metricName == null) {
+        log.warn("Missing " + MetricUtils.METRIC_NAME + " field in document, skipping: " + doc);
+        return;
+      }
+      doc.remove(MetricUtils.METRIC_NAME);
+      // XXX we could modify keys by using this original registry name
+      doc.remove(SolrReporter.REGISTRY_ID);
+      String groupId = (String)doc.getFieldValue(SolrReporter.GROUP_ID);
+      if (groupId == null) {
+        log.warn("Missing " + SolrReporter.GROUP_ID + " field in document, skipping: " + doc);
+        return;
+      }
+      doc.remove(SolrReporter.GROUP_ID);
+      String reporterId = (String)doc.getFieldValue(SolrReporter.REPORTER_ID);
+      if (reporterId == null) {
+        log.warn("Missing " + SolrReporter.REPORTER_ID + " field in document, skipping: " + doc);
+        return;
+      }
+      doc.remove(SolrReporter.REPORTER_ID);
+      String labelId = (String)doc.getFieldValue(SolrReporter.LABEL_ID);
+      doc.remove(SolrReporter.LABEL_ID);
+      doc.forEach(f -> {
+        String key = MetricRegistry.name(labelId, metricName, f.getName());
+        MetricRegistry registry = metricManager.registry(groupId);
+        AggregateMetric metric = getOrRegister(registry, key, new AggregateMetric());
+        Object o = f.getFirstValue();
+        if (o != null) {
+          metric.set(reporterId, o);
+        } else {
+          // remove missing values
+          metric.clear(reporterId);
+        }
+      });
+    }
+
+    private AggregateMetric getOrRegister(MetricRegistry registry, String name, AggregateMetric add) {
+      AggregateMetric existing = (AggregateMetric)registry.getMetrics().get(name);
+      if (existing != null) {
+        return existing;
+      }
+      try {
+        registry.register(name, add);
+        return add;
+      } catch (IllegalArgumentException e) {
+        // someone added before us
+        existing = (AggregateMetric)registry.getMetrics().get(name);
+        if (existing == null) { // now, that is weird...
+          throw new IllegalArgumentException("Inconsistent metric status, " + name);
+        }
+        return existing;
+      }
+    }
+
+    @Override
+    public void processDelete(DeleteUpdateCommand cmd) throws IOException {
+      throw new UnsupportedOperationException("processDelete");
+    }
+
+    @Override
+    public void processMergeIndexes(MergeIndexesCommand cmd) throws IOException {
+      throw new UnsupportedOperationException("processMergeIndexes");
+    }
+
+    @Override
+    public void processCommit(CommitUpdateCommand cmd) throws IOException {
+      throw new UnsupportedOperationException("processCommit");
+    }
+
+    @Override
+    public void processRollback(RollbackUpdateCommand cmd) throws IOException {
+      throw new UnsupportedOperationException("processRollback");
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/handler/admin/MetricsHandler.java
----------------------------------------------------------------------
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 385317b..b53c818 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
@@ -79,7 +79,7 @@ public class MetricsHandler extends RequestHandlerBase implements PermissionName
     NamedList response = new NamedList();
     for (String registryName : requestedRegistries) {
       MetricRegistry registry = metricManager.registry(registryName);
-      response.add(registryName, MetricUtils.toNamedList(registry, metricFilters, mustMatchFilter));
+      response.add(registryName, MetricUtils.toNamedList(registry, metricFilters, mustMatchFilter, false, false, null));
     }
     rsp.getValues().add("metrics", response);
   }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/metrics/AggregateMetric.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/metrics/AggregateMetric.java b/solr/core/src/java/org/apache/solr/metrics/AggregateMetric.java
new file mode 100644
index 0000000..babc99d
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/metrics/AggregateMetric.java
@@ -0,0 +1,200 @@
+/*
+ * 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 java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.codahale.metrics.Metric;
+
+/**
+ * This class is used for keeping several partial named values and providing useful statistics over them.
+ */
+public class AggregateMetric implements Metric {
+
+  /**
+   * Simple class to represent current value and how many times it was set.
+   */
+  public static class Update {
+    public Object value;
+    public final AtomicInteger updateCount = new AtomicInteger();
+
+    public Update(Object value) {
+      update(value);
+    }
+
+    public void update(Object value) {
+      this.value = value;
+      updateCount.incrementAndGet();
+    }
+
+    @Override
+    public String toString() {
+      return "Update{" +
+          "value=" + value +
+          ", updateCount=" + updateCount +
+          '}';
+    }
+  }
+
+  private final Map<String, Update> values = new ConcurrentHashMap<>();
+
+  public void set(String name, Object value) {
+    final Update existing = values.get(name);
+    if (existing == null) {
+      final Update created = new Update(value);
+      final Update raced = values.putIfAbsent(name, created);
+      if (raced != null) {
+        raced.update(value);
+      }
+    } else {
+      existing.update(value);
+    }
+  }
+
+  public void clear(String name) {
+    values.remove(name);
+  }
+
+  public void clear() {
+    values.clear();
+  }
+
+  public int size() {
+    return values.size();
+  }
+
+  public boolean isEmpty() {
+    return values.isEmpty();
+  }
+
+  public Map<String, Update> getValues() {
+    return Collections.unmodifiableMap(values);
+  }
+
+  // --------- stats ---------
+  public double getMax() {
+    if (values.isEmpty()) {
+      return 0;
+    }
+    Double res = null;
+    for (Update u : values.values()) {
+      if (!(u.value instanceof Number)) {
+        continue;
+      }
+      Number n = (Number)u.value;
+      if (res == null) {
+        res = n.doubleValue();
+        continue;
+      }
+      if (n.doubleValue() > res) {
+        res = n.doubleValue();
+      }
+    }
+    return res;
+  }
+
+  public double getMin() {
+    if (values.isEmpty()) {
+      return 0;
+    }
+    Double res = null;
+    for (Update u : values.values()) {
+      if (!(u.value instanceof Number)) {
+        continue;
+      }
+      Number n = (Number)u.value;
+      if (res == null) {
+        res = n.doubleValue();
+        continue;
+      }
+      if (n.doubleValue() < res) {
+        res = n.doubleValue();
+      }
+    }
+    return res;
+  }
+
+  public double getMean() {
+    if (values.isEmpty()) {
+      return 0;
+    }
+    double total = 0;
+    for (Update u : values.values()) {
+      if (!(u.value instanceof Number)) {
+        continue;
+      }
+      Number n = (Number)u.value;
+      total += n.doubleValue();
+    }
+    return total / values.size();
+  }
+
+  public double getStdDev() {
+    int size = values.size();
+    if (size < 2) {
+      return 0;
+    }
+    final double mean = getMean();
+    double sum = 0;
+    int count = 0;
+    for (Update u : values.values()) {
+      if (!(u.value instanceof Number)) {
+        continue;
+      }
+      count++;
+      Number n = (Number)u.value;
+      final double diff = n.doubleValue() - mean;
+      sum += diff * diff;
+    }
+    if (count < 2) {
+      return 0;
+    }
+    final double variance = sum / (count - 1);
+    return Math.sqrt(variance);
+  }
+
+  public double getSum() {
+    if (values.isEmpty()) {
+      return 0;
+    }
+    double res = 0;
+    for (Update u : values.values()) {
+      if (!(u.value instanceof Number)) {
+        continue;
+      }
+      Number n = (Number)u.value;
+      res += n.doubleValue();
+    }
+    return res;
+  }
+
+  @Override
+  public String toString() {
+    return "AggregateMetric{" +
+        "size=" + size() +
+        ", max=" + getMax() +
+        ", min=" + getMin() +
+        ", mean=" + getMean() +
+        ", stddev=" + getStdDev() +
+        ", sum=" + getSum() +
+        ", values=" + values +
+        '}';
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java b/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
index eb5b687..43f3535 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrCoreMetricManager.java
@@ -20,6 +20,7 @@ import java.io.Closeable;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 
+import org.apache.solr.cloud.CloudDescriptor;
 import org.apache.solr.core.NodeConfig;
 import org.apache.solr.core.PluginInfo;
 import org.apache.solr.core.SolrCore;
@@ -36,8 +37,14 @@ public class SolrCoreMetricManager implements Closeable {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   private final SolrCore core;
+  private final String tag;
   private final SolrMetricManager metricManager;
   private String registryName;
+  private String collectionName;
+  private String shardName;
+  private String replicaName;
+  private String leaderRegistryName;
+  private boolean cloudMode;
 
   /**
    * Constructs a metric manager.
@@ -46,8 +53,26 @@ public class SolrCoreMetricManager implements Closeable {
    */
   public SolrCoreMetricManager(SolrCore core) {
     this.core = core;
+    this.tag = String.valueOf(core.hashCode());
     this.metricManager = core.getCoreDescriptor().getCoreContainer().getMetricManager();
-    registryName = createRegistryName(core.getCoreDescriptor().getCollectionName(), core.getName());
+    initCloudMode();
+    registryName = createRegistryName(cloudMode, collectionName, shardName, replicaName, core.getName());
+    leaderRegistryName = createLeaderRegistryName(cloudMode, collectionName, shardName);
+  }
+
+  private void initCloudMode() {
+    CloudDescriptor cd = core.getCoreDescriptor().getCloudDescriptor();
+    if (cd != null) {
+      cloudMode = true;
+      collectionName = core.getCoreDescriptor().getCollectionName();
+      shardName = cd.getShardId();
+      //replicaName = cd.getCoreNodeName();
+      String coreName = core.getName();
+      replicaName = parseReplicaName(collectionName, coreName);
+      if (replicaName == null) {
+        replicaName = cd.getCoreNodeName();
+      }
+    }
   }
 
   /**
@@ -57,7 +82,11 @@ public class SolrCoreMetricManager implements Closeable {
   public void loadReporters() {
     NodeConfig nodeConfig = core.getCoreDescriptor().getCoreContainer().getConfig();
     PluginInfo[] pluginInfos = nodeConfig.getMetricReporterPlugins();
-    metricManager.loadReporters(pluginInfos, core.getResourceLoader(), SolrInfoMBean.Group.core, registryName);
+    metricManager.loadReporters(pluginInfos, core.getResourceLoader(), tag,
+        SolrInfoMBean.Group.core, registryName);
+    if (cloudMode) {
+      metricManager.loadShardReporters(pluginInfos, core);
+    }
   }
 
   /**
@@ -67,12 +96,18 @@ public class SolrCoreMetricManager implements Closeable {
    */
   public void afterCoreSetName() {
     String oldRegistryName = registryName;
-    registryName = createRegistryName(core.getCoreDescriptor().getCollectionName(), core.getName());
+    String oldLeaderRegistryName = leaderRegistryName;
+    initCloudMode();
+    registryName = createRegistryName(cloudMode, collectionName, shardName, replicaName, core.getName());
+    leaderRegistryName = createLeaderRegistryName(cloudMode, collectionName, shardName);
     if (oldRegistryName.equals(registryName)) {
       return;
     }
     // close old reporters
-    metricManager.closeReporters(oldRegistryName);
+    metricManager.closeReporters(oldRegistryName, tag);
+    if (oldLeaderRegistryName != null) {
+      metricManager.closeReporters(oldLeaderRegistryName, tag);
+    }
     // load reporters again, using the new core name
     loadReporters();
   }
@@ -96,7 +131,7 @@ public class SolrCoreMetricManager implements Closeable {
    */
   @Override
   public void close() throws IOException {
-    metricManager.closeReporters(getRegistryName());
+    metricManager.closeReporters(getRegistryName(), tag);
   }
 
   public SolrCore getCore() {
@@ -104,7 +139,7 @@ public class SolrCoreMetricManager implements Closeable {
   }
 
   /**
-   * Retrieves the metric registry name of the manager.
+   * Metric registry name of the manager.
    *
    * In order to make it easier for reporting tools to aggregate metrics from
    * different cores that logically belong to a single collection we convert the
@@ -124,22 +159,74 @@ public class SolrCoreMetricManager implements Closeable {
     return registryName;
   }
 
-  public static String createRegistryName(String collectionName, String coreName) {
-    if (collectionName == null || (collectionName != null && !coreName.startsWith(collectionName + "_"))) {
-      // single core, or unknown naming scheme
+  /**
+   * Metric registry name for leader metrics. This is null if not in cloud mode.
+   * @return metric registry name for leader metrics
+   */
+  public String getLeaderRegistryName() {
+    return leaderRegistryName;
+  }
+
+  /**
+   * Return a tag specific to this instance.
+   */
+  public String getTag() {
+    return tag;
+  }
+
+  public static String createRegistryName(boolean cloud, String collectionName, String shardName, String replicaName, String coreName) {
+    if (cloud) { // build registry name from logical names
+      return SolrMetricManager.getRegistryName(SolrInfoMBean.Group.core, collectionName, shardName, replicaName);
+    } else {
       return SolrMetricManager.getRegistryName(SolrInfoMBean.Group.core, coreName);
     }
-    // split "collection1_shard1_1_replica1" into parts
-    String str = coreName.substring(collectionName.length() + 1);
-    String shard;
-    String replica = null;
-    int pos = str.lastIndexOf("_replica");
-    if (pos == -1) { // ?? no _replicaN part ??
-      shard = str;
+  }
+
+  /**
+   * This method is used by {@link org.apache.solr.core.CoreContainer#rename(String, String)}.
+   * @param aCore existing core with old name
+   * @param coreName new name
+   * @return new registry name
+   */
+  public static String createRegistryName(SolrCore aCore, String coreName) {
+    CloudDescriptor cd = aCore.getCoreDescriptor().getCloudDescriptor();
+    String replicaName = null;
+    if (cd != null) {
+      replicaName = parseReplicaName(cd.getCollectionName(), coreName);
+    }
+    return createRegistryName(
+        cd != null,
+        cd != null ? cd.getCollectionName() : null,
+        cd != null ? cd.getShardId() : null,
+        replicaName,
+        coreName
+        );
+  }
+
+  public static String parseReplicaName(String collectionName, String coreName) {
+    if (collectionName == null || !coreName.startsWith(collectionName)) {
+      return null;
+    } else {
+      // split "collection1_shard1_1_replica1" into parts
+      if (coreName.length() > collectionName.length()) {
+        String str = coreName.substring(collectionName.length() + 1);
+        int pos = str.lastIndexOf("_replica");
+        if (pos == -1) { // ?? no _replicaN part ??
+          return str;
+        } else {
+          return str.substring(pos + 1);
+        }
+      } else {
+        return null;
+      }
+    }
+  }
+
+  public static String createLeaderRegistryName(boolean cloud, String collectionName, String shardName) {
+    if (cloud) {
+      return SolrMetricManager.getRegistryName(SolrInfoMBean.Group.collection, collectionName, shardName, "leader");
     } else {
-      shard = str.substring(0, pos);
-      replica = str.substring(pos + 1);
+      return null;
     }
-    return SolrMetricManager.getRegistryName(SolrInfoMBean.Group.core, collectionName, shard, replica);
   }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
----------------------------------------------------------------------
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 cac5389..3a4c3fe 100644
--- a/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
+++ b/solr/core/src/java/org/apache/solr/metrics/SolrMetricManager.java
@@ -18,9 +18,13 @@ package org.apache.solr.metrics;
 
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -29,6 +33,9 @@ import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
 
 import com.codahale.metrics.Counter;
 import com.codahale.metrics.Histogram;
@@ -39,9 +46,14 @@ import com.codahale.metrics.MetricRegistry;
 import com.codahale.metrics.MetricSet;
 import com.codahale.metrics.SharedMetricRegistries;
 import com.codahale.metrics.Timer;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.PluginInfo;
+import org.apache.solr.core.SolrCore;
 import org.apache.solr.core.SolrInfoMBean;
 import org.apache.solr.core.SolrResourceLoader;
+import org.apache.solr.metrics.reporters.solr.SolrClusterReporter;
+import org.apache.solr.metrics.reporters.solr.SolrShardReporter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -87,27 +99,39 @@ public class SolrMetricManager {
   private final Lock reportersLock = new ReentrantLock();
   private final Lock swapLock = new ReentrantLock();
 
+  public static final int DEFAULT_CLOUD_REPORTER_PERIOD = 60;
+
   public SolrMetricManager() { }
 
   /**
    * An implementation of {@link MetricFilter} that selects metrics
-   * with names that start with a prefix.
+   * with names that start with one of prefixes.
    */
   public static class PrefixFilter implements MetricFilter {
-    private final String[] prefixes;
+    private final Set<String> prefixes = new HashSet<>();
     private final Set<String> matched = new HashSet<>();
     private boolean allMatch = false;
 
     /**
-     * Create a filter that uses the provided prefix.
+     * Create a filter that uses the provided prefixes.
      * @param prefixes prefixes to use, must not be null. If empty then any
      *               name will match, if not empty then match on any prefix will
      *                 succeed (logical OR).
      */
     public PrefixFilter(String... prefixes) {
       Objects.requireNonNull(prefixes);
-      this.prefixes = prefixes;
-      if (prefixes.length == 0) {
+      if (prefixes.length > 0) {
+        this.prefixes.addAll(Arrays.asList(prefixes));
+      }
+      if (this.prefixes.isEmpty()) {
+        allMatch = true;
+      }
+    }
+
+    public PrefixFilter(Collection<String> prefixes) {
+      Objects.requireNonNull(prefixes);
+      this.prefixes.addAll(prefixes);
+      if (this.prefixes.isEmpty()) {
         allMatch = true;
       }
     }
@@ -141,6 +165,85 @@ public class SolrMetricManager {
     public void reset() {
       matched.clear();
     }
+
+    @Override
+    public String toString() {
+      return "PrefixFilter{" +
+          "prefixes=" + prefixes +
+          '}';
+    }
+  }
+
+  /**
+   * An implementation of {@link MetricFilter} that selects metrics
+   * with names that match regular expression patterns.
+   */
+  public static class RegexFilter implements MetricFilter {
+    private final Set<Pattern> compiledPatterns = new HashSet<>();
+    private final Set<String> matched = new HashSet<>();
+    private boolean allMatch = false;
+
+    /**
+     * Create a filter that uses the provided prefix.
+     * @param patterns regex patterns to use, must not be null. If empty then any
+     *               name will match, if not empty then match on any pattern will
+     *                 succeed (logical OR).
+     */
+    public RegexFilter(String... patterns) throws PatternSyntaxException {
+      this(patterns != null ? Arrays.asList(patterns) : Collections.emptyList());
+    }
+
+    public RegexFilter(Collection<String> patterns) throws PatternSyntaxException {
+      Objects.requireNonNull(patterns);
+      if (patterns.isEmpty()) {
+        allMatch = true;
+        return;
+      }
+      patterns.forEach(p -> {
+        Pattern pattern = Pattern.compile(p);
+        compiledPatterns.add(pattern);
+      });
+      if (patterns.isEmpty()) {
+        allMatch = true;
+      }
+    }
+
+    @Override
+    public boolean matches(String name, Metric metric) {
+      if (allMatch) {
+        matched.add(name);
+        return true;
+      }
+      for (Pattern p : compiledPatterns) {
+        if (p.matcher(name).matches()) {
+          matched.add(name);
+          return true;
+        }
+      }
+      return false;
+    }
+
+    /**
+     * Return the set of names that matched this filter.
+     * @return matching names
+     */
+    public Set<String> getMatched() {
+      return Collections.unmodifiableSet(matched);
+    }
+
+    /**
+     * Clear the set of names that matched.
+     */
+    public void reset() {
+      matched.clear();
+    }
+
+    @Override
+    public String toString() {
+      return "RegexFilter{" +
+          "compiledPatterns=" + compiledPatterns +
+          '}';
+    }
   }
 
   /**
@@ -150,7 +253,40 @@ public class SolrMetricManager {
     Set<String> set = new HashSet<>();
     set.addAll(registries.keySet());
     set.addAll(SharedMetricRegistries.names());
-    return Collections.unmodifiableSet(set);
+    return set;
+  }
+
+  /**
+   * Return set of existing registry names that match a regex pattern
+   * @param patterns regex patterns. NOTE: users need to make sure that patterns that
+   *                 don't start with a wildcard use the full registry name starting with
+   *                 {@link #REGISTRY_NAME_PREFIX}
+   * @return set of existing registry names where at least one pattern matched.
+   */
+  public Set<String> registryNames(String... patterns) throws PatternSyntaxException {
+    if (patterns == null || patterns.length == 0) {
+      return registryNames();
+    }
+    List<Pattern> compiled = new ArrayList<>();
+    for (String pattern : patterns) {
+      compiled.add(Pattern.compile(pattern));
+    }
+    return registryNames((Pattern[])compiled.toArray(new Pattern[compiled.size()]));
+  }
+
+  public Set<String> registryNames(Pattern... patterns) {
+    Set<String> allNames = registryNames();
+    if (patterns == null || patterns.length == 0) {
+      return allNames;
+    }
+    return allNames.stream().filter(s -> {
+      for (Pattern p : patterns) {
+        if (p.matcher(s).matches()) {
+          return true;
+        }
+      }
+      return false;
+    }).collect(Collectors.toSet());
   }
 
   /**
@@ -209,7 +345,7 @@ public class SolrMetricManager {
    */
   public void removeRegistry(String registry) {
     // close any reporters for this registry first
-    closeReporters(registry);
+    closeReporters(registry, null);
     // make sure we use a name with prefix, with overrides
     registry = overridableRegistryName(registry);
     if (isSharedRegistry(registry)) {
@@ -490,10 +626,12 @@ public class SolrMetricManager {
    * the list. If both attributes are present then only "group" attribute will be processed.
    * @param pluginInfos plugin configurations
    * @param loader resource loader
+   * @param tag optional tag for the reporters, to distinguish reporters logically created for different parent
+   *            component instances.
    * @param group selected group, not null
    * @param registryNames optional child registry name elements
    */
-  public void loadReporters(PluginInfo[] pluginInfos, SolrResourceLoader loader, SolrInfoMBean.Group group, String... registryNames) {
+  public void loadReporters(PluginInfo[] pluginInfos, SolrResourceLoader loader, String tag, SolrInfoMBean.Group group, String... registryNames) {
     if (pluginInfos == null || pluginInfos.length == 0) {
       return;
     }
@@ -533,7 +671,7 @@ public class SolrMetricManager {
         }
       }
       try {
-        loadReporter(registryName, loader, info);
+        loadReporter(registryName, loader, info, tag);
       } catch (Exception e) {
         log.warn("Error loading metrics reporter, plugin info: " + info, e);
       }
@@ -545,9 +683,12 @@ public class SolrMetricManager {
    * @param registry reporter is associated with this registry
    * @param loader loader to use when creating an instance of the reporter
    * @param pluginInfo plugin configuration. Plugin "name" and "class" attributes are required.
+   * @param tag optional tag for the reporter, to distinguish reporters logically created for different parent
+   *            component instances.
+   * @return instance of newly created and registered reporter
    * @throws Exception if any argument is missing or invalid
    */
-  public void loadReporter(String registry, SolrResourceLoader loader, PluginInfo pluginInfo) throws Exception {
+  public SolrMetricReporter loadReporter(String registry, SolrResourceLoader loader, PluginInfo pluginInfo, String tag) throws Exception {
     if (registry == null || pluginInfo == null || pluginInfo.name == null || pluginInfo.className == null) {
       throw new IllegalArgumentException("loadReporter called with missing arguments: " +
           "registry=" + registry + ", loader=" + loader + ", pluginInfo=" + pluginInfo);
@@ -558,14 +699,19 @@ public class SolrMetricManager {
         pluginInfo.className,
         SolrMetricReporter.class,
         new String[0],
-        new Class[] { SolrMetricManager.class, String.class },
-        new Object[] { this, registry }
+        new Class[]{SolrMetricManager.class, String.class},
+        new Object[]{this, registry}
     );
     try {
       reporter.init(pluginInfo);
     } catch (IllegalStateException e) {
       throw new IllegalArgumentException("reporter init failed: " + pluginInfo, e);
     }
+    registerReporter(registry, pluginInfo.name, tag, reporter);
+    return reporter;
+  }
+
+  private void registerReporter(String registry, String name, String tag, SolrMetricReporter reporter) throws Exception {
     try {
       if (!reportersLock.tryLock(10, TimeUnit.SECONDS)) {
         throw new Exception("Could not obtain lock to modify reporters registry: " + registry);
@@ -579,12 +725,15 @@ public class SolrMetricManager {
         perRegistry = new HashMap<>();
         reporters.put(registry, perRegistry);
       }
-      SolrMetricReporter oldReporter = perRegistry.get(pluginInfo.name);
+      if (tag != null && !tag.isEmpty()) {
+        name = name + "@" + tag;
+      }
+      SolrMetricReporter oldReporter = perRegistry.get(name);
       if (oldReporter != null) { // close it
-        log.info("Replacing existing reporter '" + pluginInfo.name + "' in registry '" + registry + "': " + oldReporter.toString());
+        log.info("Replacing existing reporter '" + name + "' in registry '" + registry + "': " + oldReporter.toString());
         oldReporter.close();
       }
-      perRegistry.put(pluginInfo.name, reporter);
+      perRegistry.put(name, reporter);
 
     } finally {
       reportersLock.unlock();
@@ -595,9 +744,11 @@ public class SolrMetricManager {
    * Close and unregister a named {@link SolrMetricReporter} for a registry.
    * @param registry registry name
    * @param name reporter name
+   * @param tag optional tag for the reporter, to distinguish reporters logically created for different parent
+   *            component instances.
    * @return true if a named reporter existed and was closed.
    */
-  public boolean closeReporter(String registry, String name) {
+  public boolean closeReporter(String registry, String name, String tag) {
     // make sure we use a name with prefix, with overrides
     registry = overridableRegistryName(registry);
     try {
@@ -614,6 +765,9 @@ public class SolrMetricManager {
       if (perRegistry == null) {
         return false;
       }
+      if (tag != null && !tag.isEmpty()) {
+        name = name + "@" + tag;
+      }
       SolrMetricReporter reporter = perRegistry.remove(name);
       if (reporter == null) {
         return false;
@@ -635,6 +789,17 @@ public class SolrMetricManager {
    * @return names of closed reporters
    */
   public Set<String> closeReporters(String registry) {
+    return closeReporters(registry, null);
+  }
+
+  /**
+   * Close and unregister all {@link SolrMetricReporter}-s for a registry.
+   * @param registry registry name
+   * @param tag optional tag for the reporter, to distinguish reporters logically created for different parent
+   *            component instances.
+   * @return names of closed reporters
+   */
+  public Set<String> closeReporters(String registry, String tag) {
     // make sure we use a name with prefix, with overrides
     registry = overridableRegistryName(registry);
     try {
@@ -646,18 +811,28 @@ public class SolrMetricManager {
       log.warn("Interrupted while trying to obtain lock to modify reporters registry: " + registry);
       return Collections.emptySet();
     }
-    log.info("Closing metric reporters for: " + registry);
+    log.info("Closing metric reporters for registry=" + registry + ", tag=" + tag);
     try {
-      Map<String, SolrMetricReporter> perRegistry = reporters.remove(registry);
+      Map<String, SolrMetricReporter> perRegistry = reporters.get(registry);
       if (perRegistry != null) {
-        for (SolrMetricReporter reporter : perRegistry.values()) {
+        Set<String> names = new HashSet<>(perRegistry.keySet());
+        Set<String> removed = new HashSet<>();
+        names.forEach(name -> {
+          if (tag != null && !tag.isEmpty() && !name.endsWith("@" + tag)) {
+            return;
+          }
+          SolrMetricReporter reporter = perRegistry.remove(name);
           try {
             reporter.close();
           } catch (IOException ioe) {
             log.warn("Exception closing reporter " + reporter, ioe);
           }
+          removed.add(name);
+        });
+        if (removed.size() == names.size()) {
+          reporters.remove(registry);
         }
-        return perRegistry.keySet();
+        return removed;
       } else {
         return Collections.emptySet();
       }
@@ -695,4 +870,114 @@ public class SolrMetricManager {
       reportersLock.unlock();
     }
   }
+
+  private List<PluginInfo> prepareCloudPlugins(PluginInfo[] pluginInfos, String group, String className,
+                                                      Map<String, String> defaultAttributes,
+                                                      Map<String, Object> defaultInitArgs,
+                                                      PluginInfo defaultPlugin) {
+    List<PluginInfo> result = new ArrayList<>();
+    if (pluginInfos == null) {
+      pluginInfos = new PluginInfo[0];
+    }
+    for (PluginInfo info : pluginInfos) {
+      String groupAttr = info.attributes.get("group");
+      if (!group.equals(groupAttr)) {
+        continue;
+      }
+      info = preparePlugin(info, className, defaultAttributes, defaultInitArgs);
+      if (info != null) {
+        result.add(info);
+      }
+    }
+    if (result.isEmpty() && defaultPlugin != null) {
+      defaultPlugin = preparePlugin(defaultPlugin, className, defaultAttributes, defaultInitArgs);
+      if (defaultPlugin != null) {
+        result.add(defaultPlugin);
+      }
+    }
+    return result;
+  }
+
+  private PluginInfo preparePlugin(PluginInfo info, String className, Map<String, String> defaultAttributes,
+                                   Map<String, Object> defaultInitArgs) {
+    if (info == null) {
+      return null;
+    }
+    String classNameAttr = info.attributes.get("class");
+    if (className != null) {
+      if (classNameAttr != null && !className.equals(classNameAttr)) {
+        log.warn("Conflicting class name attributes, expected " + className + " but was " + classNameAttr + ", skipping " + info);
+        return null;
+      }
+    }
+
+    Map<String, String> attrs = new HashMap<>(info.attributes);
+    defaultAttributes.forEach((k, v) -> {
+      if (!attrs.containsKey(k)) {
+        attrs.put(k, v);
+      }
+    });
+    attrs.put("class", className);
+    Map<String, Object> initArgs = new HashMap<>();
+    if (info.initArgs != null) {
+      initArgs.putAll(info.initArgs.asMap(10));
+    }
+    defaultInitArgs.forEach((k, v) -> {
+      if (!initArgs.containsKey(k)) {
+        initArgs.put(k, v);
+      }
+    });
+    return new PluginInfo(info.type, attrs, new NamedList(initArgs), null);
+  }
+
+  public void loadShardReporters(PluginInfo[] pluginInfos, SolrCore core) {
+    // don't load for non-cloud cores
+    if (core.getCoreDescriptor().getCloudDescriptor() == null) {
+      return;
+    }
+    // prepare default plugin if none present in the config
+    Map<String, String> attrs = new HashMap<>();
+    attrs.put("name", "shardDefault");
+    attrs.put("group", SolrInfoMBean.Group.shard.toString());
+    Map<String, Object> initArgs = new HashMap<>();
+    initArgs.put("period", DEFAULT_CLOUD_REPORTER_PERIOD);
+
+    String registryName = core.getCoreMetricManager().getRegistryName();
+    // collect infos and normalize
+    List<PluginInfo> infos = prepareCloudPlugins(pluginInfos, SolrInfoMBean.Group.shard.toString(), SolrShardReporter.class.getName(),
+        attrs, initArgs, null);
+    for (PluginInfo info : infos) {
+      try {
+        SolrMetricReporter reporter = loadReporter(registryName, core.getResourceLoader(), info,
+            String.valueOf(core.hashCode()));
+        ((SolrShardReporter)reporter).setCore(core);
+      } catch (Exception e) {
+        log.warn("Could not load shard reporter, pluginInfo=" + info, e);
+      }
+    }
+  }
+
+  public void loadClusterReporters(PluginInfo[] pluginInfos, CoreContainer cc) {
+    // don't load for non-cloud instances
+    if (!cc.isZooKeeperAware()) {
+      return;
+    }
+    Map<String, String> attrs = new HashMap<>();
+    attrs.put("name", "clusterDefault");
+    attrs.put("group", SolrInfoMBean.Group.cluster.toString());
+    Map<String, Object> initArgs = new HashMap<>();
+    initArgs.put("period", DEFAULT_CLOUD_REPORTER_PERIOD);
+    List<PluginInfo> infos = prepareCloudPlugins(pluginInfos, SolrInfoMBean.Group.cluster.toString(), SolrClusterReporter.class.getName(),
+        attrs, initArgs, null);
+    String registryName = getRegistryName(SolrInfoMBean.Group.cluster);
+    for (PluginInfo info : infos) {
+      try {
+        SolrMetricReporter reporter = loadReporter(registryName, cc.getResourceLoader(), info, null);
+        ((SolrClusterReporter)reporter).setCoreContainer(cc);
+      } catch (Exception e) {
+        log.warn("Could not load node reporter, pluginInfo=" + info, e);
+      }
+    }
+  }
+
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/metrics/reporters/JmxObjectNameFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/metrics/reporters/JmxObjectNameFactory.java b/solr/core/src/java/org/apache/solr/metrics/reporters/JmxObjectNameFactory.java
index 4df5257..1f5b4f0 100644
--- a/solr/core/src/java/org/apache/solr/metrics/reporters/JmxObjectNameFactory.java
+++ b/solr/core/src/java/org/apache/solr/metrics/reporters/JmxObjectNameFactory.java
@@ -41,9 +41,9 @@ public class JmxObjectNameFactory implements ObjectNameFactory {
    * @param additionalProperties additional properties as key, value pairs.
    */
   public JmxObjectNameFactory(String reporterName, String domain, String... additionalProperties) {
-    this.reporterName = reporterName;
+    this.reporterName = reporterName.replaceAll(":", "_");
     this.domain = domain;
-    this.subdomains = domain.split("\\.");
+    this.subdomains = domain.replaceAll(":", "_").split("\\.");
     if (additionalProperties != null && (additionalProperties.length % 2) != 0) {
       throw new IllegalArgumentException("additionalProperties length must be even: " + Arrays.toString(additionalProperties));
     }
@@ -83,7 +83,7 @@ public class JmxObjectNameFactory implements ObjectNameFactory {
         }
         sb.append(','); // separate from other properties
       } else {
-        sb.append(currentDomain);
+        sb.append(currentDomain.replaceAll(":", "_"));
         sb.append(':');
       }
     } else {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/4d7bc947/solr/core/src/java/org/apache/solr/metrics/reporters/solr/SolrClusterReporter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/metrics/reporters/solr/SolrClusterReporter.java b/solr/core/src/java/org/apache/solr/metrics/reporters/solr/SolrClusterReporter.java
new file mode 100644
index 0000000..846e805
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/metrics/reporters/solr/SolrClusterReporter.java
@@ -0,0 +1,277 @@
+/*
+ * 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.reporters.solr;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.apache.http.client.HttpClient;
+import org.apache.solr.cloud.Overseer;
+import org.apache.solr.cloud.ZkController;
+import org.apache.solr.common.cloud.SolrZkClient;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrInfoMBean;
+import org.apache.solr.handler.admin.MetricsCollectorHandler;
+import org.apache.solr.metrics.SolrMetricManager;
+import org.apache.solr.metrics.SolrMetricReporter;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This reporter sends selected metrics from local registries to {@link Overseer}.
+ * <p>The following configuration properties are supported:</p>
+ * <ul>
+ *   <li>handler - (optional str) handler path where reports are sent. Default is
+ *   {@link MetricsCollectorHandler#HANDLER_PATH}.</li>
+ *   <li>period - (optional int) how often reports are sent, in seconds. Default is 60. Setting this
+ *   to 0 disables the reporter.</li>
+ *   <li>report - (optional multiple lst) report configuration(s), see below.</li>
+ * </ul>
+ * Each report configuration consist of the following properties:
+ * <ul>
+ *   <li>registry - (required str) regex pattern matching source registries (see {@link SolrMetricManager#registryNames(String...)}),
+ *   may contain capture groups.</li>
+ *   <li>group - (required str) target registry name where metrics will be grouped. This can be a regex pattern that
+ *   contains back-references to capture groups collected by <code>registry</code> pattern</li>
+ *   <li>label - (optional str) optional prefix to prepend to metric names, may contain back-references to
+ *   capture groups collected by <code>registry</code> pattern</li>
+ *   <li>filter - (optional multiple str) regex expression(s) matching selected metrics to be reported.</li>
+ * </ul>
+ * NOTE: this reporter uses predefined "overseer" group, and it's always created even if explicit configuration
+ * is missing. Default configuration uses report specifications from {@link #DEFAULT_REPORTS}.
+ * <p>Example configuration:</p>
+ * <pre>
+ *       &lt;reporter name="test" group="overseer"&gt;
+ *         &lt;str name="handler"&gt;/admin/metrics/collector&lt;/str&gt;
+ *         &lt;int name="period"&gt;11&lt;/int&gt;
+ *         &lt;lst name="report"&gt;
+ *           &lt;str name="group"&gt;overseer&lt;/str&gt;
+ *           &lt;str name="label"&gt;jvm&lt;/str&gt;
+ *           &lt;str name="registry"&gt;solr\.jvm&lt;/str&gt;
+ *           &lt;str name="filter"&gt;memory\.total\..*&lt;/str&gt;
+ *           &lt;str name="filter"&gt;memory\.heap\..*&lt;/str&gt;
+ *           &lt;str name="filter"&gt;os\.SystemLoadAverage&lt;/str&gt;
+ *           &lt;str name="filter"&gt;threads\.count&lt;/str&gt;
+ *         &lt;/lst&gt;
+ *         &lt;lst name="report"&gt;
+ *           &lt;str name="group"&gt;overseer&lt;/str&gt;
+ *           &lt;str name="label"&gt;leader.$1&lt;/str&gt;
+ *           &lt;str name="registry"&gt;solr\.core\.(.*)\.leader&lt;/str&gt;
+ *           &lt;str name="filter"&gt;UPDATE\./update/.*&lt;/str&gt;
+ *         &lt;/lst&gt;
+ *       &lt;/reporter&gt;
+ * </pre>
+ *
+ */
+public class SolrClusterReporter extends SolrMetricReporter {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  public static final String CLUSTER_GROUP = SolrMetricManager.overridableRegistryName(SolrInfoMBean.Group.cluster.toString());
+
+  public static final List<SolrReporter.Report> DEFAULT_REPORTS = new ArrayList<SolrReporter.Report>() {{
+    add(new SolrReporter.Report(CLUSTER_GROUP, "jetty",
+        SolrMetricManager.overridableRegistryName(SolrInfoMBean.Group.jetty.toString()),
+        Collections.emptySet())); // all metrics
+    add(new SolrReporter.Report(CLUSTER_GROUP, "jvm",
+        SolrMetricManager.overridableRegistryName(SolrInfoMBean.Group.jvm.toString()),
+        new HashSet<String>() {{
+          add("memory\\.total\\..*");
+          add("memory\\.heap\\..*");
+          add("os\\.SystemLoadAverage");
+          add("os\\.FreePhysicalMemorySize");
+          add("os\\.FreeSwapSpaceSize");
+          add("os\\.OpenFileDescriptorCount");
+          add("threads\\.count");
+        }})); // all metrics
+    // XXX anything interesting here?
+    //add(new SolrReporter.Specification(OVERSEER_GROUP, "node", SolrMetricManager.overridableRegistryName(SolrInfoMBean.Group.node.toString()),
+    //    Collections.emptySet())); // all metrics
+    add(new SolrReporter.Report(CLUSTER_GROUP, "leader.$1", "solr\\.collection\\.(.*)\\.leader",
+        new HashSet<String>(){{
+          add("UPDATE\\./update/.*");
+          add("QUERY\\./select.*");
+          add("INDEX\\..*");
+          add("TLOG\\..*");
+    }}));
+  }};
+
+  private String handler = MetricsCollectorHandler.HANDLER_PATH;
+  private int period = SolrMetricManager.DEFAULT_CLOUD_REPORTER_PERIOD;
+  private List<SolrReporter.Report> reports = new ArrayList<>();
+
+  private SolrReporter reporter;
+
+  /**
+   * Create a reporter for metrics managed in a named registry.
+   *
+   * @param metricManager metric manager
+   * @param registryName  this is ignored
+   */
+  public SolrClusterReporter(SolrMetricManager metricManager, String registryName) {
+    super(metricManager, registryName);
+  }
+
+  public void setHandler(String handler) {
+    this.handler = handler;
+  }
+
+  public void setPeriod(int period) {
+    this.period = period;
+  }
+
+  public void setReport(List<Map> reportConfig) {
+    if (reportConfig == null || reportConfig.isEmpty()) {
+      return;
+    }
+    reportConfig.forEach(map -> {
+      SolrReporter.Report r = SolrReporter.Report.fromMap(map);
+      if (r != null) {
+        reports.add(r);
+      }
+    });
+  }
+
+  // for unit tests
+  int getPeriod() {
+    return period;
+  }
+
+  List<SolrReporter.Report> getReports() {
+    return reports;
+  }
+
+  @Override
+  protected void validate() throws IllegalStateException {
+    if (period < 1) {
+      log.info("Turning off node reporter, period=" + period);
+    }
+    if (reports.isEmpty()) { // set defaults
+      reports = DEFAULT_REPORTS;
+    }
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (reporter != null) {
+      reporter.close();;
+    }
+  }
+
+  public void setCoreContainer(CoreContainer cc) {
+    if (reporter != null) {
+      reporter.close();;
+    }
+    // start reporter only in cloud mode
+    if (!cc.isZooKeeperAware()) {
+      log.warn("Not ZK-aware, not starting...");
+      return;
+    }
+    if (period < 1) { // don't start it
+      return;
+    }
+    HttpClient httpClient = cc.getUpdateShardHandler().getHttpClient();
+    ZkController zk = cc.getZkController();
+    String reporterId = zk.getNodeName();
+    reporter = SolrReporter.Builder.forReports(metricManager, reports)
+        .convertRatesTo(TimeUnit.SECONDS)
+        .convertDurationsTo(TimeUnit.MILLISECONDS)
+        .withHandler(handler)
+        .withReporterId(reporterId)
+        .cloudClient(false) // we want to send reports specifically to a selected leader instance
+        .skipAggregateValues(true) // we don't want to transport details of aggregates
+        .skipHistograms(true) // we don't want to transport histograms
+        .build(httpClient, new OverseerUrlSupplier(zk));
+
+    reporter.start(period, TimeUnit.SECONDS);
+  }
+
+  // TODO: fix this when there is an elegant way to retrieve URL of a node that runs Overseer leader.
+  // package visibility for unit tests
+  static class OverseerUrlSupplier implements Supplier<String> {
+    private static final long DEFAULT_INTERVAL = 30000000; // 30s
+    private ZkController zk;
+    private String lastKnownUrl = null;
+    private long lastCheckTime = 0;
+    private long interval = DEFAULT_INTERVAL;
+
+    OverseerUrlSupplier(ZkController zk) {
+      this.zk = zk;
+    }
+
+    @Override
+    public String get() {
+      if (zk == null) {
+        return null;
+      }
+      // primitive caching for lastKnownUrl
+      long now = System.nanoTime();
+      if (lastKnownUrl != null && (now - lastCheckTime) < interval) {
+        return lastKnownUrl;
+      }
+      if (!zk.isConnected()) {
+        return lastKnownUrl;
+      }
+      lastCheckTime = now;
+      SolrZkClient zkClient = zk.getZkClient();
+      ZkNodeProps props;
+      try {
+        props = ZkNodeProps.load(zkClient.getData(
+            Overseer.OVERSEER_ELECT + "/leader", null, null, true));
+      } catch (KeeperException e) {
+        log.warn("Could not obtain overseer's address, skipping.", e);
+        return lastKnownUrl;
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        return lastKnownUrl;
+      }
+      if (props == null) {
+        return lastKnownUrl;
+      }
+      String oid = props.getStr("id");
+      if (oid == null) {
+        return lastKnownUrl;
+      }
+      String[] ids = oid.split("-");
+      if (ids.length != 3) { // unknown format
+        log.warn("Unknown format of leader id, skipping: " + oid);
+        return lastKnownUrl;
+      }
+      // convert nodeName back to URL
+      String url = zk.getZkStateReader().getBaseUrlForNodeName(ids[1]);
+      // check that it's parseable
+      try {
+        new java.net.URL(url);
+      } catch (MalformedURLException mue) {
+        log.warn("Malformed Overseer's leader URL: url", mue);
+        return lastKnownUrl;
+      }
+      lastKnownUrl = url;
+      return url;
+    }
+  }
+
+}