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

[46/50] lucene-solr:jira/solr-8593: SOLR-9854: Collect metrics for index merges and index store IO. (squashed)

SOLR-9854: Collect metrics for index merges and index store IO. (squashed)


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

Branch: refs/heads/jira/solr-8593
Commit: 48ca9fc3f4f8d95293cee7bb59eff61247ede181
Parents: 7b2e3db
Author: Andrzej Bialecki <ab...@apache.org>
Authored: Tue Jan 3 11:11:50 2017 +0100
Committer: Andrzej Bialecki <ab...@apache.org>
Committed: Tue Jan 3 12:12:01 2017 +0100

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   2 +
 .../solr/core/MetricsDirectoryFactory.java      | 511 +++++++++++++++++++
 .../src/java/org/apache/solr/core/SolrCore.java |  24 +-
 .../apache/solr/core/SolrDeletionPolicy.java    |   6 +
 .../org/apache/solr/core/SolrInfoMBean.java     |   2 +-
 .../solr/core/StandardDirectoryFactory.java     |   2 +-
 .../org/apache/solr/update/SolrIndexConfig.java |  16 +-
 .../org/apache/solr/update/SolrIndexWriter.java | 144 ++++++
 .../org/apache/solr/util/stats/MetricUtils.java |  79 ++-
 .../conf/solrconfig-indexmetrics.xml            |  57 +++
 .../test/org/apache/solr/core/TestConfig.java   |   2 +
 .../solr/handler/TestReplicationHandler.java    |  13 +-
 .../admin/CoreMergeIndexesAdminHandlerTest.java |  10 +-
 .../solr/handler/admin/MetricsHandlerTest.java  |   6 +-
 .../apache/solr/update/SolrIndexConfigTest.java |   1 +
 .../solr/update/SolrIndexMetricsTest.java       |  94 ++++
 .../apache/solr/util/stats/MetricUtilsTest.java |  24 +-
 17 files changed, 945 insertions(+), 48 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 8609f91..afcd295 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -212,6 +212,8 @@ New Features
 
 * SOLR-9896: Instrument and collect metrics from query, update, core admin and core load thread pools. (shalin)
 
+* SOLR-9854: Collect metrics for index merges and index store IO (ab)
+
 Optimizations
 ----------------------
 * SOLR-9704: Facet Module / JSON Facet API: Optimize blockChildren facets that have

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/java/org/apache/solr/core/MetricsDirectoryFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/MetricsDirectoryFactory.java b/solr/core/src/java/org/apache/solr/core/MetricsDirectoryFactory.java
new file mode 100644
index 0000000..62e82ac
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/core/MetricsDirectoryFactory.java
@@ -0,0 +1,511 @@
+/*
+ * 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.core;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FilterDirectory;
+import org.apache.lucene.store.IOContext;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.store.LockFactory;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.metrics.SolrMetricManager;
+import org.apache.solr.util.plugin.SolrCoreAware;
+
+/**
+ * An implementation of {@link DirectoryFactory} that decorates provided factory by
+ * adding metrics for directory IO operations.
+ */
+public class MetricsDirectoryFactory extends DirectoryFactory implements SolrCoreAware {
+  private final SolrMetricManager metricManager;
+  private final String registry;
+  private final DirectoryFactory in;
+  private boolean directoryDetails = false;
+
+  public MetricsDirectoryFactory(SolrMetricManager metricManager, String registry, DirectoryFactory in) {
+    this.metricManager = metricManager;
+    this.registry = registry;
+    this.in = in;
+  }
+
+  public DirectoryFactory getDelegate() {
+    return in;
+  }
+
+  /**
+   * Currently the following arguments are supported:
+   * <ul>
+   *   <li><code>directoryDetails</code> - (optional bool) when true then additional detailed metrics
+   *   will be collected. These include eg. IO size histograms and per-file counters and histograms</li>
+   * </ul>
+   * @param args init args
+   */
+  @Override
+  public void init(NamedList args) {
+    // should be already inited
+    // in.init(args);
+    if (args == null) {
+      return;
+    }
+    Boolean dd = args.getBooleanArg("directoryDetails");
+    if (dd != null) {
+      directoryDetails = dd;
+    } else {
+      directoryDetails = false;
+    }
+  }
+
+  @Override
+  public void doneWithDirectory(Directory dir) throws IOException {
+    // unwrap
+    if (dir instanceof MetricsDirectory) {
+      dir = ((MetricsDirectory)dir).getDelegate();
+    }
+    in.doneWithDirectory(dir);
+  }
+
+  @Override
+  public void addCloseListener(Directory dir, CachingDirectoryFactory.CloseListener closeListener) {
+    // unwrap
+    if (dir instanceof MetricsDirectory) {
+      dir = ((MetricsDirectory)dir).getDelegate();
+    }
+    in.addCloseListener(dir, closeListener);
+  }
+
+  @Override
+  public void close() throws IOException {
+    in.close();
+  }
+
+  @Override
+  protected Directory create(String path, LockFactory lockFactory, DirContext dirContext) throws IOException {
+    Directory dir = in.create(path, lockFactory, dirContext);
+    return new MetricsDirectory(metricManager, registry, dir, directoryDetails);
+  }
+
+  @Override
+  protected LockFactory createLockFactory(String rawLockType) throws IOException {
+    return in.createLockFactory(rawLockType);
+  }
+
+  @Override
+  public boolean exists(String path) throws IOException {
+    return in.exists(path);
+  }
+
+  @Override
+  public void remove(Directory dir) throws IOException {
+    // unwrap
+    if (dir instanceof MetricsDirectory) {
+      dir = ((MetricsDirectory)dir).getDelegate();
+    }
+    in.remove(dir);
+  }
+
+  @Override
+  public void remove(Directory dir, boolean afterCoreClose) throws IOException {
+    // unwrap
+    if (dir instanceof MetricsDirectory) {
+      dir = ((MetricsDirectory)dir).getDelegate();
+    }
+    in.remove(dir, afterCoreClose);
+  }
+
+  @Override
+  public boolean isSharedStorage() {
+    return in.isSharedStorage();
+  }
+
+  @Override
+  public boolean isAbsolute(String path) {
+    return in.isAbsolute(path);
+  }
+
+  @Override
+  public boolean searchersReserveCommitPoints() {
+    return in.searchersReserveCommitPoints();
+  }
+
+  @Override
+  public String getDataHome(CoreDescriptor cd) throws IOException {
+    return in.getDataHome(cd);
+  }
+
+  @Override
+  public long size(Directory directory) throws IOException {
+    return in.size(directory);
+  }
+
+  @Override
+  public long size(String path) throws IOException {
+    return in.size(path);
+  }
+
+  @Override
+  public Collection<SolrInfoMBean> offerMBeans() {
+    return in.offerMBeans();
+  }
+
+  @Override
+  public void cleanupOldIndexDirectories(String dataDirPath, String currentIndexDirPath) {
+    in.cleanupOldIndexDirectories(dataDirPath, currentIndexDirPath);
+  }
+
+  @Override
+  public void remove(String path, boolean afterCoreClose) throws IOException {
+    in.remove(path, afterCoreClose);
+  }
+
+  @Override
+  public void remove(String path) throws IOException {
+    in.remove(path);
+  }
+
+  @Override
+  public void move(Directory fromDir, Directory toDir, String fileName, IOContext ioContext) throws IOException {
+    in.move(fromDir, toDir, fileName, ioContext);
+  }
+
+  @Override
+  public Directory get(String path, DirContext dirContext, String rawLockType) throws IOException {
+    Directory dir = in.get(path, dirContext, rawLockType);
+    if (dir instanceof MetricsDirectory) {
+      return dir;
+    } else {
+      return new MetricsDirectory(metricManager, registry, dir, directoryDetails);
+    }
+  }
+
+  @Override
+  public void renameWithOverwrite(Directory dir, String fileName, String toName) throws IOException {
+    super.renameWithOverwrite(dir, fileName, toName);
+  }
+
+  @Override
+  public String normalize(String path) throws IOException {
+    return in.normalize(path);
+  }
+
+  @Override
+  protected boolean deleteOldIndexDirectory(String oldDirPath) throws IOException {
+    return in.deleteOldIndexDirectory(oldDirPath);
+  }
+
+  @Override
+  public void initCoreContainer(CoreContainer cc) {
+    in.initCoreContainer(cc);
+  }
+
+  @Override
+  protected Directory getBaseDir(Directory dir) {
+    return in.getBaseDir(dir);
+  }
+
+  @Override
+  public void incRef(Directory dir) {
+    // unwrap
+    if (dir instanceof MetricsDirectory) {
+      dir = ((MetricsDirectory)dir).getDelegate();
+    }
+    in.incRef(dir);
+  }
+
+  @Override
+  public boolean isPersistent() {
+    return in.isPersistent();
+  }
+
+  @Override
+  public void inform(SolrCore core) {
+    if (in instanceof  SolrCoreAware) {
+      ((SolrCoreAware)in).inform(core);
+    }
+  }
+
+  @Override
+  public void release(Directory dir) throws IOException {
+    // unwrap
+    if (dir instanceof MetricsDirectory) {
+      dir = ((MetricsDirectory)dir).getDelegate();
+    }
+    in.release(dir);
+  }
+
+
+
+  private static final String SEGMENTS = "segments";
+  private static final String SEGMENTS_PREFIX = "segments_";
+  private static final String PENDING_SEGMENTS_PREFIX = "pending_segments_";
+  private static final String TEMP = "temp";
+  private static final String OTHER = "other";
+
+  public static class MetricsDirectory extends FilterDirectory {
+
+    private final Directory in;
+    private final String registry;
+    private final SolrMetricManager metricManager;
+    private final Meter totalReads;
+    private final Histogram totalReadSizes;
+    private final Meter totalWrites;
+    private final Histogram totalWriteSizes;
+    private final boolean directoryDetails;
+
+    private final String PREFIX = SolrInfoMBean.Category.DIRECTORY.toString() + ".";
+
+    public MetricsDirectory(SolrMetricManager metricManager, String registry, Directory in, boolean directoryDetails) throws IOException {
+      super(in);
+      this.metricManager = metricManager;
+      this.registry = registry;
+      this.in = in;
+      this.directoryDetails = directoryDetails;
+      this.totalReads = metricManager.meter(registry, "reads", SolrInfoMBean.Category.DIRECTORY.toString(), "total");
+      this.totalWrites = metricManager.meter(registry, "writes", SolrInfoMBean.Category.DIRECTORY.toString(), "total");
+      if (directoryDetails) {
+        this.totalReadSizes = metricManager.histogram(registry, "readSizes", SolrInfoMBean.Category.DIRECTORY.toString(), "total");
+        this.totalWriteSizes = metricManager.histogram(registry, "writeSizes", SolrInfoMBean.Category.DIRECTORY.toString(), "total");
+      } else {
+        this.totalReadSizes = null;
+        this.totalWriteSizes = null;
+      }
+    }
+
+    private String getMetricName(String name, boolean output) {
+      if (!directoryDetails) {
+        return null;
+      }
+      String lastName;
+      if (name.startsWith(SEGMENTS_PREFIX) || name.startsWith(PENDING_SEGMENTS_PREFIX)) {
+        lastName = SEGMENTS;
+      } else {
+        int pos = name.lastIndexOf('.');
+        if (pos != -1 && name.length() > pos + 1) {
+          lastName = name.substring(pos + 1);
+        } else {
+          lastName = OTHER;
+        }
+      }
+      StringBuilder sb = new StringBuilder(PREFIX);
+      sb.append(lastName);
+      sb.append('.');
+      if (output) {
+        sb.append("write");
+      } else {
+        sb.append("read");
+      }
+      return sb.toString();
+    }
+
+    @Override
+    public IndexOutput createOutput(String name, IOContext context) throws IOException {
+      IndexOutput output = in.createOutput(name, context);
+      if (output != null) {
+        return new MetricsOutput(totalWrites, totalWriteSizes, metricManager, registry, getMetricName(name, true), output);
+      } else {
+        return null;
+      }
+    }
+
+    @Override
+    public IndexOutput createTempOutput(String prefix, String suffix, IOContext context) throws IOException {
+      IndexOutput output = in.createTempOutput(prefix, suffix, context);
+      if (output != null) {
+        return new MetricsOutput(totalWrites, totalWriteSizes, metricManager, registry, getMetricName(TEMP, true), output);
+      } else {
+        return null;
+      }
+    }
+
+    @Override
+    public IndexInput openInput(String name, IOContext context) throws IOException {
+      IndexInput input = in.openInput(name, context);
+      if (input != null) {
+        return new MetricsInput(totalReads, totalReadSizes, metricManager, registry, getMetricName(name, false), input);
+      } else {
+        return null;
+      }
+    }
+  }
+
+  public static class MetricsOutput extends IndexOutput {
+    private final IndexOutput in;
+    private final Histogram histogram;
+    private final Meter meter;
+    private final Meter totalMeter;
+    private final Histogram totalHistogram;
+    private final boolean withDetails;
+
+    public MetricsOutput(Meter totalMeter, Histogram totalHistogram, SolrMetricManager metricManager,
+                         String registry, String metricName, IndexOutput in) {
+      super(in.toString(), in.getName());
+      this.in = in;
+      this.totalMeter = totalMeter;
+      this.totalHistogram = totalHistogram;
+      if (metricName != null && totalHistogram != null) {
+        withDetails = true;
+        String histName = metricName + "Sizes";
+        String meterName = metricName + "s";
+        this.histogram = metricManager.histogram(registry, histName);
+        this.meter = metricManager.meter(registry, meterName);
+      } else {
+        withDetails = false;
+        this.histogram = null;
+        this.meter = null;
+      }
+    }
+
+    @Override
+    public void writeByte(byte b) throws IOException {
+      in.writeByte(b);
+      totalMeter.mark();
+      if (withDetails) {
+        totalHistogram.update(1);
+        meter.mark();
+        histogram.update(1);
+      }
+    }
+
+    @Override
+    public void writeBytes(byte[] b, int offset, int length) throws IOException {
+      in.writeBytes(b, offset, length);
+      totalMeter.mark(length);
+      if (withDetails) {
+        totalHistogram.update(length);
+        meter.mark(length);
+        histogram.update(length);
+      }
+    }
+
+    @Override
+    public void close() throws IOException {
+      in.close();
+    }
+
+    @Override
+    public long getFilePointer() {
+      return in.getFilePointer();
+    }
+
+    @Override
+    public long getChecksum() throws IOException {
+      return in.getChecksum();
+    }
+  }
+
+  public static class MetricsInput extends IndexInput {
+    private final IndexInput in;
+    private final Meter totalMeter;
+    private final Histogram totalHistogram;
+    private final Histogram histogram;
+    private final Meter meter;
+    private final boolean withDetails;
+
+    public MetricsInput(Meter totalMeter, Histogram totalHistogram, SolrMetricManager metricManager, String registry, String metricName, IndexInput in) {
+      super(in.toString());
+      this.in = in;
+      this.totalMeter = totalMeter;
+      this.totalHistogram = totalHistogram;
+      if (metricName != null && totalHistogram != null) {
+        withDetails = true;
+        String histName = metricName + "Sizes";
+        String meterName = metricName + "s";
+        this.histogram = metricManager.histogram(registry, histName);
+        this.meter = metricManager.meter(registry, meterName);
+      } else {
+        withDetails = false;
+        this.histogram = null;
+        this.meter = null;
+      }
+    }
+
+    public MetricsInput(Meter totalMeter, Histogram totalHistogram, Histogram histogram, Meter meter, IndexInput in) {
+      super(in.toString());
+      this.in = in;
+      this.totalMeter = totalMeter;
+      this.totalHistogram  = totalHistogram;
+      this.histogram = histogram;
+      this.meter = meter;
+      if (totalHistogram != null && meter != null && histogram != null) {
+        withDetails = true;
+      } else {
+        withDetails = false;
+      }
+    }
+
+    @Override
+    public void close() throws IOException {
+      in.close();
+    }
+
+    @Override
+    public long getFilePointer() {
+      return in.getFilePointer();
+    }
+
+    @Override
+    public void seek(long pos) throws IOException {
+      in.seek(pos);
+    }
+
+    @Override
+    public long length() {
+      return in.length();
+    }
+
+    @Override
+    public IndexInput clone() {
+      return new MetricsInput(totalMeter, totalHistogram, histogram, meter, in.clone());
+    }
+
+    @Override
+    public IndexInput slice(String sliceDescription, long offset, long length) throws IOException {
+      IndexInput slice = in.slice(sliceDescription, offset, length);
+      if (slice != null) {
+        return new MetricsInput(totalMeter, totalHistogram, histogram, meter, slice);
+      } else {
+        return null;
+      }
+    }
+
+    @Override
+    public byte readByte() throws IOException {
+      totalMeter.mark();
+      if (withDetails) {
+        totalHistogram.update(1);
+        meter.mark();
+        histogram.update(1);
+      }
+      return in.readByte();
+    }
+
+    @Override
+    public void readBytes(byte[] b, int offset, int len) throws IOException {
+      totalMeter.mark(len);
+      if (withDetails) {
+        totalHistogram.update(len);
+        meter.mark(len);
+        histogram.update(len);
+      }
+      in.readBytes(b, offset, len);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/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 a8d7738..a9fec5a 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java
@@ -642,7 +642,14 @@ public final class SolrCore implements SolrInfoMBean, Closeable {
       dirFactory = new NRTCachingDirectoryFactory();
       dirFactory.initCoreContainer(getCoreDescriptor().getCoreContainer());
     }
-    return dirFactory;
+    if (solrConfig.indexConfig.metricsInfo != null && solrConfig.indexConfig.metricsInfo.isEnabled()) {
+      final DirectoryFactory factory = new MetricsDirectoryFactory(coreDescriptor.getCoreContainer().getMetricManager(),
+          coreMetricManager.getRegistryName(), dirFactory);
+        factory.init(solrConfig.indexConfig.metricsInfo.initArgs);
+      return factory;
+    } else {
+      return dirFactory;
+    }
   }
 
   private void initIndexReaderFactory() {
@@ -846,6 +853,8 @@ public final class SolrCore implements SolrInfoMBean, Closeable {
     resourceLoader = config.getResourceLoader();
     this.solrConfig = config;
     this.configSetProperties = configSetProperties;
+    // Initialize the metrics manager
+    this.coreMetricManager = initCoreMetricManager(config);
 
     if (updateHandler == null) {
       directoryFactory = initDirectoryFactory();
@@ -863,17 +872,14 @@ public final class SolrCore implements SolrInfoMBean, Closeable {
 
     checkVersionFieldExistsInSchema(schema, coreDescriptor);
 
-    // Initialize the metrics manager
-    this.coreMetricManager = initCoreMetricManager(config);
-
     SolrMetricManager metricManager = this.coreDescriptor.getCoreContainer().getMetricManager();
 
     // initialize searcher-related metrics
-    newSearcherCounter = metricManager.counter(coreMetricManager.getRegistryName(), "newSearcher");
-    newSearcherTimer = metricManager.timer(coreMetricManager.getRegistryName(), "newSearcherTime");
-    newSearcherWarmupTimer = metricManager.timer(coreMetricManager.getRegistryName(), "newSearcherWarmup");
-    newSearcherMaxReachedCounter = metricManager.counter(coreMetricManager.getRegistryName(), "newSearcherMaxReached");
-    newSearcherOtherErrorsCounter = metricManager.counter(coreMetricManager.getRegistryName(), "newSearcherErrors");
+    newSearcherCounter = metricManager.counter(coreMetricManager.getRegistryName(), "new", Category.SEARCHER.toString());
+    newSearcherTimer = metricManager.timer(coreMetricManager.getRegistryName(), "time", Category.SEARCHER.toString(), "new");
+    newSearcherWarmupTimer = metricManager.timer(coreMetricManager.getRegistryName(), "warmup", Category.SEARCHER.toString(), "new");
+    newSearcherMaxReachedCounter = metricManager.counter(coreMetricManager.getRegistryName(), "maxReached", Category.SEARCHER.toString(), "new");
+    newSearcherOtherErrorsCounter = metricManager.counter(coreMetricManager.getRegistryName(), "errors", Category.SEARCHER.toString(), "new");
 
     // Initialize JMX
     this.infoRegistry = initInfoRegistry(name, config);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/java/org/apache/solr/core/SolrDeletionPolicy.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/SolrDeletionPolicy.java b/solr/core/src/java/org/apache/solr/core/SolrDeletionPolicy.java
index 34482cd..eba2964 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrDeletionPolicy.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrDeletionPolicy.java
@@ -114,6 +114,9 @@ public class SolrDeletionPolicy extends IndexDeletionPolicy implements NamedList
 
     protected void appendDetails(StringBuilder sb, IndexCommit c) {
       Directory dir = c.getDirectory();
+      if (dir instanceof MetricsDirectoryFactory.MetricsDirectory) { // unwrap
+        dir = ((MetricsDirectoryFactory.MetricsDirectory) dir).getDelegate();
+      }
       if (dir instanceof FSDirectory) {
         FSDirectory fsd = (FSDirectory) dir;
         sb.append("dir=").append(fsd.getDirectory());
@@ -194,6 +197,9 @@ public class SolrDeletionPolicy extends IndexDeletionPolicy implements NamedList
   private String getId(IndexCommit commit) {
     StringBuilder sb = new StringBuilder();
     Directory dir = commit.getDirectory();
+    if (dir instanceof MetricsDirectoryFactory.MetricsDirectory) { // unwrap
+      dir = ((MetricsDirectoryFactory.MetricsDirectory) dir).getDelegate();
+    }
 
     // For anything persistent, make something that will
     // be the same, regardless of the Directory instance.

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/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 c5fb84b..c64af47 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrInfoMBean.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrInfoMBean.java
@@ -32,7 +32,7 @@ public interface SolrInfoMBean {
   /**
    * Category of {@link SolrCore} component.
    */
-  enum Category { CORE, QUERYHANDLER, UPDATEHANDLER, CACHE, HIGHLIGHTING, QUERYPARSER, OTHER }
+  enum Category { CORE, QUERYHANDLER, UPDATEHANDLER, CACHE, HIGHLIGHTING, QUERYPARSER, SEARCHER, INDEX, DIRECTORY, OTHER }
 
   /**
    * Top-level group of beans for a subsystem.

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/java/org/apache/solr/core/StandardDirectoryFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/StandardDirectoryFactory.java b/solr/core/src/java/org/apache/solr/core/StandardDirectoryFactory.java
index 1d8793a..37c15ed 100644
--- a/solr/core/src/java/org/apache/solr/core/StandardDirectoryFactory.java
+++ b/solr/core/src/java/org/apache/solr/core/StandardDirectoryFactory.java
@@ -142,7 +142,7 @@ public class StandardDirectoryFactory extends CachingDirectoryFactory {
 
     super.move(fromDir, toDir, fileName, ioContext);
   }
-  
+
   // perform an atomic rename if possible
   public void renameWithOverwrite(Directory dir, String fileName, String toName) throws IOException {
     Directory baseDir = getBaseDir(dir);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java b/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java
index 1a9801f..d484e85 100644
--- a/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java
+++ b/solr/core/src/java/org/apache/solr/update/SolrIndexConfig.java
@@ -18,6 +18,7 @@ package org.apache.solr.update;
 
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -78,6 +79,7 @@ public class SolrIndexConfig implements MapSerializable {
   public final PluginInfo mergePolicyInfo;
   public final PluginInfo mergePolicyFactoryInfo;
   public final PluginInfo mergeSchedulerInfo;
+  public final PluginInfo metricsInfo;
   
   public final PluginInfo mergedSegmentWarmerInfo;
   
@@ -99,6 +101,8 @@ public class SolrIndexConfig implements MapSerializable {
     mergePolicyFactoryInfo = null;
     mergeSchedulerInfo = null;
     mergedSegmentWarmerInfo = null;
+    // enable coarse-grained metrics by default
+    metricsInfo = new PluginInfo("metrics", Collections.emptyMap(), null, null);
   }
   
   /**
@@ -144,6 +148,12 @@ public class SolrIndexConfig implements MapSerializable {
     writeLockTimeout=solrConfig.getInt(prefix+"/writeLockTimeout", def.writeLockTimeout);
     lockType=solrConfig.get(prefix+"/lockType", def.lockType);
 
+    List<PluginInfo> infos = solrConfig.readPluginInfos(prefix + "/metrics", false, false);
+    if (infos.isEmpty()) {
+      metricsInfo = def.metricsInfo;
+    } else {
+      metricsInfo = infos.get(0);
+    }
     mergeSchedulerInfo = getPluginInfo(prefix + "/mergeScheduler", solrConfig, def.mergeSchedulerInfo);
     mergePolicyInfo = getPluginInfo(prefix + "/mergePolicy", solrConfig, def.mergePolicyInfo);
     mergePolicyFactoryInfo = getPluginInfo(prefix + "/mergePolicyFactory", solrConfig, def.mergePolicyFactoryInfo);
@@ -197,6 +207,9 @@ public class SolrIndexConfig implements MapSerializable {
         "lockType", lockType,
         "infoStreamEnabled", infoStream != InfoStream.NO_OUTPUT);
     if(mergeSchedulerInfo != null) m.put("mergeScheduler",mergeSchedulerInfo);
+    if (metricsInfo != null) {
+      m.put("metrics", metricsInfo);
+    }
     if (mergePolicyInfo != null) {
       m.put("mergePolicy", mergePolicyInfo);
     } else if (mergePolicyFactoryInfo != null) {
@@ -237,7 +250,8 @@ public class SolrIndexConfig implements MapSerializable {
     iwc.setSimilarity(schema.getSimilarity());
     MergePolicy mergePolicy = buildMergePolicy(schema);
     iwc.setMergePolicy(mergePolicy);
-    iwc.setMergeScheduler(buildMergeScheduler(schema));
+    MergeScheduler mergeScheduler = buildMergeScheduler(schema);
+    iwc.setMergeScheduler(mergeScheduler);
     iwc.setInfoStream(infoStream);
 
     if (mergePolicy instanceof SortingMergePolicy) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/java/org/apache/solr/update/SolrIndexWriter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/update/SolrIndexWriter.java b/solr/core/src/java/org/apache/solr/update/SolrIndexWriter.java
index d75214a..626bc8e 100644
--- a/solr/core/src/java/org/apache/solr/update/SolrIndexWriter.java
+++ b/solr/core/src/java/org/apache/solr/update/SolrIndexWriter.java
@@ -20,12 +20,19 @@ import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Timer;
 import org.apache.lucene.codecs.Codec;
 import org.apache.lucene.index.IndexDeletionPolicy;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.MergePolicy;
+import org.apache.lucene.index.SegmentCommitInfo;
 import org.apache.lucene.store.Directory;
 import org.apache.lucene.util.InfoStream;
 import org.apache.solr.common.util.IOUtils;
@@ -33,6 +40,8 @@ import org.apache.solr.common.util.SuppressForbidden;
 import org.apache.solr.core.DirectoryFactory;
 import org.apache.solr.core.DirectoryFactory.DirContext;
 import org.apache.solr.core.SolrCore;
+import org.apache.solr.core.SolrInfoMBean;
+import org.apache.solr.metrics.SolrMetricManager;
 import org.apache.solr.schema.IndexSchema;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -60,6 +69,28 @@ public class SolrIndexWriter extends IndexWriter {
   private InfoStream infoStream;
   private Directory directory;
 
+  // metrics
+  private long majorMergeDocs = 512 * 1024;
+  private final Timer majorMerge;
+  private final Timer minorMerge;
+  private final Meter majorMergedDocs;
+  private final Meter majorDeletedDocs;
+  private final Counter mergeErrors;
+  private final Meter flushMeter; // original counter is package-private in IndexWriter
+  private final boolean mergeDetails;
+  private final AtomicInteger runningMajorMerges = new AtomicInteger();
+  private final Gauge<Integer> runningMajorMergesGauge;
+  private final AtomicInteger runningMinorMerges = new AtomicInteger();
+  private final Gauge<Integer> runningMinorMergesGauge;
+  private final AtomicInteger runningMajorMergesSegments = new AtomicInteger();
+  private final Gauge<Integer> runningMajorMergesSegmentsGauge;
+  private final AtomicInteger runningMinorMergesSegments = new AtomicInteger();
+  private final Gauge<Integer> runningMinorMergesSegmentsGauge;
+  private final AtomicLong runningMajorMergesDocs = new AtomicLong();
+  private final Gauge<Long> runningMajorMergesDocsGauge;
+  private final AtomicLong runningMinorMergesDocs = new AtomicLong();
+  private final Gauge<Long> runningMinorMergesDocsGauge;
+
   public static SolrIndexWriter create(SolrCore core, String name, String path, DirectoryFactory directoryFactory, boolean create, IndexSchema schema, SolrIndexConfig config, IndexDeletionPolicy delPolicy, Codec codec) throws IOException {
 
     SolrIndexWriter w = null;
@@ -84,6 +115,20 @@ public class SolrIndexWriter extends IndexWriter {
     this.directory = d;
     numOpens.incrementAndGet();
     log.debug("Opened Writer " + name);
+    // no metrics
+    minorMerge = null;
+    majorMerge = null;
+    mergeErrors = null;
+    majorMergedDocs = null;
+    majorDeletedDocs = null;
+    runningMinorMergesGauge = null;
+    runningMinorMergesDocsGauge = null;
+    runningMinorMergesSegmentsGauge = null;
+    runningMajorMergesGauge = null;
+    runningMajorMergesDocsGauge = null;
+    runningMajorMergesSegmentsGauge = null;
+    flushMeter = null;
+    mergeDetails = false;
   }
 
   private SolrIndexWriter(SolrCore core, String name, String path, Directory directory, boolean create, IndexSchema schema, SolrIndexConfig config, IndexDeletionPolicy delPolicy, Codec codec) throws IOException {
@@ -97,6 +142,51 @@ public class SolrIndexWriter extends IndexWriter {
     infoStream = getConfig().getInfoStream();
     this.directory = directory;
     numOpens.incrementAndGet();
+    SolrMetricManager metricManager = core.getCoreDescriptor().getCoreContainer().getMetricManager();
+    String registry = core.getCoreMetricManager().getRegistryName();
+    minorMerge = metricManager.timer(registry, "minor", SolrInfoMBean.Category.INDEX.toString(), "merge");
+    majorMerge = metricManager.timer(registry, "major", SolrInfoMBean.Category.INDEX.toString(), "merge");
+    mergeErrors = metricManager.counter(registry, "errors", SolrInfoMBean.Category.INDEX.toString(), "merge");
+    runningMajorMergesGauge = () -> runningMajorMerges.get();
+    runningMinorMergesGauge = () -> runningMinorMerges.get();
+    runningMajorMergesDocsGauge = () -> runningMajorMergesDocs.get();
+    runningMinorMergesDocsGauge = () -> runningMinorMergesDocs.get();
+    runningMajorMergesSegmentsGauge = () -> runningMajorMergesSegments.get();
+    runningMinorMergesSegmentsGauge = () -> runningMinorMergesSegments.get();
+    metricManager.register(registry, runningMajorMergesGauge, true, "running", SolrInfoMBean.Category.INDEX.toString(), "merge", "major");
+    metricManager.register(registry, runningMinorMergesGauge, true, "running", SolrInfoMBean.Category.INDEX.toString(), "merge", "minor");
+    metricManager.register(registry, runningMajorMergesDocsGauge, true, "running.docs", SolrInfoMBean.Category.INDEX.toString(), "merge", "major");
+    metricManager.register(registry, runningMinorMergesDocsGauge, true, "running.docs", SolrInfoMBean.Category.INDEX.toString(), "merge", "minor");
+    metricManager.register(registry, runningMajorMergesSegmentsGauge, true, "running.segments", SolrInfoMBean.Category.INDEX.toString(), "merge", "major");
+    metricManager.register(registry, runningMinorMergesSegmentsGauge, true, "running.segments", SolrInfoMBean.Category.INDEX.toString(), "merge", "minor");
+    flushMeter = metricManager.meter(registry, "flush", SolrInfoMBean.Category.INDEX.toString());
+    if (config.metricsInfo != null && config.metricsInfo.initArgs != null) {
+      Object v = config.metricsInfo.initArgs.get("majorMergeDocs");
+      if (v != null) {
+        try {
+          majorMergeDocs = Long.parseLong(String.valueOf(v));
+        } catch (Exception e) {
+          log.warn("Invalid 'majorMergeDocs' argument, using default 512k", e);
+        }
+      }
+      Boolean Details = config.metricsInfo.initArgs.getBooleanArg("mergeDetails");
+      if (Details != null) {
+        mergeDetails = Details;
+      } else {
+        mergeDetails = false;
+      }
+      if (mergeDetails) {
+        majorMergedDocs = metricManager.meter(registry, "docs", SolrInfoMBean.Category.INDEX.toString(), "merge", "major");
+        majorDeletedDocs = metricManager.meter(registry, "deletedDocs", SolrInfoMBean.Category.INDEX.toString(), "merge", "major");
+      } else {
+        majorMergedDocs = null;
+        majorDeletedDocs = null;
+      }
+    } else {
+      mergeDetails = false;
+      majorMergedDocs = null;
+      majorDeletedDocs = null;
+    }
   }
 
   @SuppressForbidden(reason = "Need currentTimeMillis, commit time should be used only for debugging purposes, " +
@@ -112,6 +202,60 @@ public class SolrIndexWriter extends IndexWriter {
     this.directoryFactory = factory;
   }
 
+  // we override this method to collect metrics for merges.
+  @Override
+  public void merge(MergePolicy.OneMerge merge) throws IOException {
+    long deletedDocs = 0;
+    long totalNumDocs = merge.totalNumDocs();
+    for (SegmentCommitInfo info : merge.segments) {
+      totalNumDocs -= info.getDelCount();
+      deletedDocs += info.getDelCount();
+    }
+    boolean major = totalNumDocs > majorMergeDocs;
+    int segmentsCount = merge.segments.size();
+    Timer.Context context;
+    if (major) {
+      runningMajorMerges.incrementAndGet();
+      runningMajorMergesDocs.addAndGet(totalNumDocs);
+      runningMajorMergesSegments.addAndGet(segmentsCount);
+      if (mergeDetails) {
+        majorMergedDocs.mark(totalNumDocs);
+        majorDeletedDocs.mark(deletedDocs);
+      }
+      context = majorMerge.time();
+    } else {
+      runningMinorMerges.incrementAndGet();
+      runningMinorMergesDocs.addAndGet(totalNumDocs);
+      runningMinorMergesSegments.addAndGet(segmentsCount);
+      context = minorMerge.time();
+    }
+    try {
+      super.merge(merge);
+    } catch (Throwable t) {
+      mergeErrors.inc();
+      throw t;
+    } finally {
+      context.stop();
+      if (major) {
+        runningMajorMerges.decrementAndGet();
+        runningMajorMergesDocs.addAndGet(-totalNumDocs);
+        runningMajorMergesSegments.addAndGet(-segmentsCount);
+      } else {
+        runningMinorMerges.decrementAndGet();
+        runningMinorMergesDocs.addAndGet(-totalNumDocs);
+        runningMinorMergesSegments.addAndGet(-segmentsCount);
+      }
+    }
+  }
+
+  @Override
+  protected void doAfterFlush() throws IOException {
+    if (flushMeter != null) { // this is null when writer is used only for snapshot cleanup
+      flushMeter.mark();
+    }
+    super.doAfterFlush();
+  }
+
   /**
    * use DocumentBuilder now...
    * private final void addField(Document doc, String name, String val) {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/java/org/apache/solr/util/stats/MetricUtils.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/util/stats/MetricUtils.java b/solr/core/src/java/org/apache/solr/util/stats/MetricUtils.java
index af5a0b5..4a83c86 100644
--- a/solr/core/src/java/org/apache/solr/util/stats/MetricUtils.java
+++ b/solr/core/src/java/org/apache/solr/util/stats/MetricUtils.java
@@ -40,7 +40,7 @@ import org.apache.solr.common.util.NamedList;
 public class MetricUtils {
 
   /**
-   * Adds metrics from a Timer to a NamedList, using well-known names.
+   * Adds metrics from a Timer to a NamedList, using well-known back-compat names.
    * @param lst The NamedList to add the metrics data to
    * @param timer The Timer to extract the metrics from
    */
@@ -68,7 +68,7 @@ public class MetricUtils {
   }
 
   /**
-   * Returns a NamedList respresentation of the given metric registry. Only those metrics
+   * Returns a NamedList representation of the given metric registry. Only those metrics
    * are converted to NamedList which match at least one of the given MetricFilter instances.
    *
    * @param registry      the {@link MetricRegistry} to be converted to NamedList
@@ -104,31 +104,74 @@ public class MetricUtils {
   static NamedList histogramToNamedList(Histogram histogram) {
     NamedList response = new NamedList();
     Snapshot snapshot = histogram.getSnapshot();
-    response.add("requests", histogram.getCount());
-    response.add("minTime", nsToMs(snapshot.getMin()));
-    response.add("maxTime", nsToMs(snapshot.getMax()));
-    response.add("avgTimePerRequest", nsToMs(snapshot.getMean()));
-    response.add("medianRequestTime", nsToMs(snapshot.getMedian()));
-    response.add("75thPcRequestTime", nsToMs(snapshot.get75thPercentile()));
-    response.add("95thPcRequestTime", nsToMs(snapshot.get95thPercentile()));
-    response.add("99thPcRequestTime", nsToMs(snapshot.get99thPercentile()));
-    response.add("999thPcRequestTime", nsToMs(snapshot.get999thPercentile()));
+    response.add("count", histogram.getCount());
+    // non-time based values
+    addSnapshot(response, snapshot, false);
     return response;
   }
 
+  // optionally convert ns to ms
+  static double nsToMs(boolean convert, double value) {
+    if (convert) {
+      return nsToMs(value);
+    } else {
+      return value;
+    }
+  }
+
+  static final String MS = "_ms";
+
+  static final String MIN = "min";
+  static final String MIN_MS = MIN + MS;
+  static final String MAX = "max";
+  static final String MAX_MS = MAX + MS;
+  static final String MEAN = "mean";
+  static final String MEAN_MS = MEAN + MS;
+  static final String MEDIAN = "median";
+  static final String MEDIAN_MS = MEDIAN + MS;
+  static final String STDDEV = "stddev";
+  static final String STDDEV_MS = STDDEV + MS;
+  static final String P75 = "p75";
+  static final String P75_MS = P75 + MS;
+  static final String P95 = "p95";
+  static final String P95_MS = P95 + MS;
+  static final String P99 = "p99";
+  static final String P99_MS = P99 + MS;
+  static final String P999 = "p999";
+  static final String P999_MS = P999 + MS;
+
+  // some snapshots represent time in ns, other snapshots represent raw values (eg. chunk size)
+  static void addSnapshot(NamedList response, Snapshot snapshot, boolean ms) {
+    response.add((ms ? MIN_MS: MIN), nsToMs(ms, snapshot.getMin()));
+    response.add((ms ? MAX_MS: MAX), nsToMs(ms, snapshot.getMax()));
+    response.add((ms ? MEAN_MS : MEAN), nsToMs(ms, snapshot.getMean()));
+    response.add((ms ? MEDIAN_MS: MEDIAN), nsToMs(ms, snapshot.getMedian()));
+    response.add((ms ? STDDEV_MS: STDDEV), nsToMs(ms, snapshot.getStdDev()));
+    response.add((ms ? P75_MS: P75), nsToMs(ms, snapshot.get75thPercentile()));
+    response.add((ms ? P95_MS: P95), nsToMs(ms, snapshot.get95thPercentile()));
+    response.add((ms ? P99_MS: P99), nsToMs(ms, snapshot.get99thPercentile()));
+    response.add((ms ? P999_MS: P999), nsToMs(ms, snapshot.get999thPercentile()));
+  }
+
   static NamedList timerToNamedList(Timer timer) {
     NamedList response = new NamedList();
-    addMetrics(response, timer);
+    response.add("count", timer.getCount());
+    response.add("meanRate", timer.getMeanRate());
+    response.add("1minRate", timer.getOneMinuteRate());
+    response.add("5minRate", timer.getFiveMinuteRate());
+    response.add("15minRate", timer.getFifteenMinuteRate());
+    // time-based values in nanoseconds
+    addSnapshot(response, timer.getSnapshot(), true);
     return response;
   }
 
   static NamedList meterToNamedList(Meter meter) {
     NamedList response = new NamedList();
-    response.add("requests", meter.getCount());
-    response.add("avgRequestsPerSecond", meter.getMeanRate());
-    response.add("1minRateRequestsPerSecond", meter.getOneMinuteRate());
-    response.add("5minRateRequestsPerSecond", meter.getFiveMinuteRate());
-    response.add("15minRateRequestsPerSecond", meter.getFifteenMinuteRate());
+    response.add("count", meter.getCount());
+    response.add("meanRate", meter.getMeanRate());
+    response.add("1minRate", meter.getOneMinuteRate());
+    response.add("5minRate", meter.getFiveMinuteRate());
+    response.add("15minRate", meter.getFifteenMinuteRate());
     return response;
   }
 
@@ -140,7 +183,7 @@ public class MetricUtils {
 
   static NamedList counterToNamedList(Counter counter) {
     NamedList response = new NamedList();
-    response.add("requests", counter.getCount());
+    response.add("count", counter.getCount());
     return response;
   }
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/test-files/solr/collection1/conf/solrconfig-indexmetrics.xml
----------------------------------------------------------------------
diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-indexmetrics.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-indexmetrics.xml
new file mode 100644
index 0000000..1acf18d
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-indexmetrics.xml
@@ -0,0 +1,57 @@
+<?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.
+-->
+
+<config>
+
+  <dataDir>${solr.data.dir:}</dataDir>
+
+  <directoryFactory name="DirectoryFactory"
+                    class="${solr.directoryFactory:solr.NRTCachingDirectoryFactory}"/>
+  <schemaFactory class="ClassicIndexSchemaFactory"/>
+
+  <luceneMatchVersion>${tests.luceneMatchVersion:LATEST}</luceneMatchVersion>
+
+  <indexConfig>
+    <metrics>
+      <bool name="directoryDetails">${solr.tests.directoryDetails:false}</bool>
+      <bool name="mergeDetails">${solr.tests.mergeDetails:false}</bool>
+    </metrics>
+    <!-- intentionally set very low values here to trigger multiple flushes and merges.
+         DO NOT USE THESE ABSURD VALUES IN PRODUCTION. -->
+    <mergeFactor>3</mergeFactor>
+    <maxBufferedDocs>100</maxBufferedDocs>
+
+    <mergePolicy class="org.apache.lucene.index.TieredMergePolicy"/>
+  </indexConfig>
+
+  <updateHandler class="solr.DirectUpdateHandler2">
+    <commitWithin>
+      <softCommit>${solr.commitwithin.softcommit:true}</softCommit>
+    </commitWithin>
+
+  </updateHandler>
+  <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>
+
+  </requestHandler>
+</config>

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/test/org/apache/solr/core/TestConfig.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/core/TestConfig.java b/solr/core/src/test/org/apache/solr/core/TestConfig.java
index 55e1e17..8244b32 100644
--- a/solr/core/src/test/org/apache/solr/core/TestConfig.java
+++ b/solr/core/src/test/org/apache/solr/core/TestConfig.java
@@ -128,6 +128,8 @@ public class TestConfig extends SolrTestCaseJ4 {
 
     ++numDefaultsTested; assertEquals("default infoStream", InfoStream.NO_OUTPUT, sic.infoStream);
 
+    ++numDefaultsTested; assertNotNull("default metrics", sic.metricsInfo);
+
     // mergePolicyInfo and mergePolicyFactoryInfo are mutually exclusive
     // so ++ count them only once for both instead of individually
     ++numDefaultsTested; ++numNullDefaults;

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java b/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java
index 685ef99..345b86d 100644
--- a/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java
+++ b/solr/core/src/test/org/apache/solr/handler/TestReplicationHandler.java
@@ -66,6 +66,8 @@ import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.core.CachingDirectoryFactory;
 import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.DirectoryFactory;
+import org.apache.solr.core.MetricsDirectoryFactory;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.core.StandardDirectoryFactory;
 import org.apache.solr.core.snapshots.SolrSnapshotMetaDataManager;
@@ -895,12 +897,21 @@ public class TestReplicationHandler extends SolrTestCaseJ4 {
     }
   }
 
+  private CachingDirectoryFactory getCachingDirectoryFactory(SolrCore core) {
+    DirectoryFactory df = core.getDirectoryFactory();
+    if (df instanceof MetricsDirectoryFactory) {
+      return (CachingDirectoryFactory)((MetricsDirectoryFactory)df).getDelegate();
+    } else {
+      return (CachingDirectoryFactory)df;
+    }
+  }
+
   private void checkForSingleIndex(JettySolrRunner jetty) {
     CoreContainer cores = jetty.getCoreContainer();
     Collection<SolrCore> theCores = cores.getCores();
     for (SolrCore core : theCores) {
       String ddir = core.getDataDir();
-      CachingDirectoryFactory dirFactory = (CachingDirectoryFactory) core.getDirectoryFactory();
+      CachingDirectoryFactory dirFactory = getCachingDirectoryFactory(core);
       synchronized (dirFactory) {
         Set<String> livePaths = dirFactory.getLivePaths();
         // one for data, one for hte index under data and one for the snapshot metadata.

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/test/org/apache/solr/handler/admin/CoreMergeIndexesAdminHandlerTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/CoreMergeIndexesAdminHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/CoreMergeIndexesAdminHandlerTest.java
index 6f1a802..937cc86 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/CoreMergeIndexesAdminHandlerTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/CoreMergeIndexesAdminHandlerTest.java
@@ -24,6 +24,8 @@ import org.apache.lucene.store.LockFactory;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.params.CoreAdminParams;
 import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.DirectoryFactory;
+import org.apache.solr.core.MetricsDirectoryFactory;
 import org.apache.solr.core.MockFSDirectoryFactory;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.response.SolrQueryResponse;
@@ -75,7 +77,13 @@ public class CoreMergeIndexesAdminHandlerTest extends SolrTestCaseJ4 {
     final CoreAdminHandler admin = new CoreAdminHandler(cores);
 
     try (SolrCore core = cores.getCore("collection1")) {
-      FailingDirectoryFactory dirFactory = (FailingDirectoryFactory)core.getDirectoryFactory();
+      DirectoryFactory df = core.getDirectoryFactory();
+      FailingDirectoryFactory dirFactory;
+      if (df instanceof MetricsDirectoryFactory) {
+        dirFactory = (FailingDirectoryFactory)((MetricsDirectoryFactory)df).getDelegate();
+      } else {
+        dirFactory = (FailingDirectoryFactory)df;
+      }
 
       try {
         dirFactory.fail = true;

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
index 3667285..e15778d 100644
--- a/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/admin/MetricsHandlerTest.java
@@ -49,9 +49,9 @@ public class MetricsHandlerTest extends SolrTestCaseJ4 {
     assertNotNull(values.get("solr.node"));
     NamedList nl = (NamedList) values.get("solr.core.collection1");
     assertNotNull(nl);
-    assertNotNull(nl.get("newSearcherErrors")); // counter type
-    assertNotNull(((NamedList) nl.get("newSearcherErrors")).get("requests"));
-    assertEquals(0L, ((NamedList) nl.get("newSearcherErrors")).get("requests"));
+    assertNotNull(nl.get("SEARCHER.new.errors")); // counter type
+    assertNotNull(((NamedList) nl.get("SEARCHER.new.errors")).get("count"));
+    assertEquals(0L, ((NamedList) nl.get("SEARCHER.new.errors")).get("count"));
     nl = (NamedList) values.get("solr.node");
     assertNotNull(nl.get("cores.loaded")); // int gauge
     assertEquals(1, ((NamedList) nl.get("cores.loaded")).get("value"));

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/test/org/apache/solr/update/SolrIndexConfigTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/update/SolrIndexConfigTest.java b/solr/core/src/test/org/apache/solr/update/SolrIndexConfigTest.java
index 4fec5c3..7d1c4c7 100644
--- a/solr/core/src/test/org/apache/solr/update/SolrIndexConfigTest.java
+++ b/solr/core/src/test/org/apache/solr/update/SolrIndexConfigTest.java
@@ -197,6 +197,7 @@ public class SolrIndexConfigTest extends SolrTestCaseJ4 {
     } else {
       assertNull(m.get("mergedSegmentWarmer"));
     }
+    ++mSizeExpected; assertNotNull(m.get("metrics"));
 
     assertEquals(mSizeExpected, m.size());
   }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/test/org/apache/solr/update/SolrIndexMetricsTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/update/SolrIndexMetricsTest.java b/solr/core/src/test/org/apache/solr/update/SolrIndexMetricsTest.java
new file mode 100644
index 0000000..e17b1bd
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/update/SolrIndexMetricsTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.update;
+
+import java.util.Map;
+
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrInputDocument;
+import org.apache.solr.request.SolrQueryRequest;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Test proper registration and collection of index and directory metrics.
+ */
+public class SolrIndexMetricsTest extends SolrTestCaseJ4 {
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    System.setProperty("solr.tests.mergeDetails", "true");
+    System.setProperty("solr.tests.directoryDetails", "true");
+    initCore("solrconfig-indexmetrics.xml", "schema.xml");
+  }
+
+  @Test
+  public void testIndexMetrics() throws Exception {
+    SolrQueryRequest req = lrf.makeRequest();
+    UpdateHandler uh = req.getCore().getUpdateHandler();
+    AddUpdateCommand add = new AddUpdateCommand(req);
+    for (int i = 0; i < 1000; i++) {
+      add.clear();
+      add.solrDoc = new SolrInputDocument();
+      add.solrDoc.addField("id", "" + i);
+      add.solrDoc.addField("foo_s", "foo-" + i);
+      uh.addDoc(add);
+    }
+    uh.commit(new CommitUpdateCommand(req, false));
+    MetricRegistry registry = h.getCoreContainer().getMetricManager().registry(h.getCore().getCoreMetricManager().getRegistryName());
+    assertNotNull(registry);
+    // make sure all merges are finished
+    h.reload();
+
+    Map<String, Metric> metrics = registry.getMetrics();
+
+    assertTrue(metrics.entrySet().stream().filter(e -> e.getKey().startsWith("INDEX")).count() >= 12);
+    // this is variable, depending on the codec and the number of created files
+    assertTrue(metrics.entrySet().stream().filter(e -> e.getKey().startsWith("DIRECTORY")).count() > 50);
+
+    // check basic index meters
+    Timer timer = (Timer)metrics.get("INDEX.merge.minor");
+    assertEquals("minorMerge: " + timer.getCount(), 4, timer.getCount());
+    timer = (Timer)metrics.get("INDEX.merge.major");
+    assertEquals("majorMerge: " + timer.getCount(), 0, timer.getCount());
+    Meter meter = (Meter)metrics.get("INDEX.merge.major.docs");
+    assertEquals("majorMergeDocs: " + meter.getCount(), 0, meter.getCount());
+    meter = (Meter)metrics.get("INDEX.flush");
+    assertEquals("flush: " + meter.getCount(), 19, meter.getCount());
+
+    // check basic directory meters
+    meter = (Meter)metrics.get("DIRECTORY.total.reads");
+    assertTrue("totalReads", meter.getCount() > 0);
+    meter = (Meter)metrics.get("DIRECTORY.total.writes");
+    assertTrue("totalWrites", meter.getCount() > 0);
+    Histogram histogram = (Histogram)metrics.get("DIRECTORY.total.readSizes");
+    assertTrue("readSizes", histogram.getCount() > 0);
+    histogram = (Histogram)metrics.get("DIRECTORY.total.writeSizes");
+    assertTrue("writeSizes", histogram.getCount() > 0);
+    // check detailed meters
+    meter = (Meter)metrics.get("DIRECTORY.segments.writes");
+    assertTrue("segmentsWrites", meter.getCount() > 0);
+    histogram = (Histogram)metrics.get("DIRECTORY.segments.writeSizes");
+    assertTrue("segmentsWriteSizes", histogram.getCount() > 0);
+
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/48ca9fc3/solr/core/src/test/org/apache/solr/util/stats/MetricUtilsTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/util/stats/MetricUtilsTest.java b/solr/core/src/test/org/apache/solr/util/stats/MetricUtilsTest.java
index 31e8154..e39ad6e 100644
--- a/solr/core/src/test/org/apache/solr/util/stats/MetricUtilsTest.java
+++ b/solr/core/src/test/org/apache/solr/util/stats/MetricUtilsTest.java
@@ -23,7 +23,6 @@ import com.codahale.metrics.Snapshot;
 import com.codahale.metrics.Timer;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.SimpleOrderedMap;
 import org.junit.Test;
 
 public class MetricUtilsTest extends SolrTestCaseJ4 {
@@ -34,24 +33,23 @@ public class MetricUtilsTest extends SolrTestCaseJ4 {
     final Timer timer = new Timer();
     final int iterations = random().nextInt(100);
     for (int i = 0; i < iterations; ++i) {
-      timer.update(random().nextInt(), TimeUnit.NANOSECONDS);
+      timer.update(Math.abs(random().nextInt()) + 1, TimeUnit.NANOSECONDS);
     }
     // obtain timer metrics
-    final NamedList<Object> lst = new SimpleOrderedMap<>();
-    MetricUtils.addMetrics(lst, timer);
+    NamedList lst = MetricUtils.timerToNamedList(timer);
     // check that expected metrics were obtained
-    assertEquals(lst.size(), 9);
+    assertEquals(14, lst.size());
     final Snapshot snapshot = timer.getSnapshot();
     // cannot test avgRequestsPerMinute directly because mean rate changes as time increases!
     // assertEquals(lst.get("avgRequestsPerSecond"), timer.getMeanRate());
-    assertEquals(lst.get("5minRateRequestsPerSecond"), timer.getFiveMinuteRate());
-    assertEquals(lst.get("15minRateRequestsPerSecond"), timer.getFifteenMinuteRate());
-    assertEquals(lst.get("avgTimePerRequest"), MetricUtils.nsToMs(snapshot.getMean()));
-    assertEquals(lst.get("medianRequestTime"), MetricUtils.nsToMs(snapshot.getMedian()));
-    assertEquals(lst.get("75thPcRequestTime"), MetricUtils.nsToMs(snapshot.get75thPercentile()));
-    assertEquals(lst.get("95thPcRequestTime"), MetricUtils.nsToMs(snapshot.get95thPercentile()));
-    assertEquals(lst.get("99thPcRequestTime"), MetricUtils.nsToMs(snapshot.get99thPercentile()));
-    assertEquals(lst.get("999thPcRequestTime"), MetricUtils.nsToMs(snapshot.get999thPercentile()));
+    assertEquals(timer.getFiveMinuteRate(), lst.get("5minRate"));
+    assertEquals(timer.getFifteenMinuteRate(), lst.get("15minRate"));
+    assertEquals(MetricUtils.nsToMs(snapshot.getMean()), lst.get("mean_ms"));
+    assertEquals(MetricUtils.nsToMs(snapshot.getMedian()), lst.get("median_ms"));
+    assertEquals(MetricUtils.nsToMs(snapshot.get75thPercentile()), lst.get("p75_ms"));
+    assertEquals(MetricUtils.nsToMs(snapshot.get95thPercentile()), lst.get("p95_ms"));
+    assertEquals(MetricUtils.nsToMs(snapshot.get99thPercentile()), lst.get("p99_ms"));
+    assertEquals(MetricUtils.nsToMs(snapshot.get999thPercentile()), lst.get("p999_ms"));
   }
 
 }