Merge changes from topic 'metrics-core'

* changes:
  Support parameterized metrics
  Export metrics through REST API
  DropWizard metric support
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 1bda39f..13b1a48 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.UploadPackMetricsHook;
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -196,11 +197,15 @@
 
   static class UploadFactory implements UploadPackFactory<HttpServletRequest> {
     private final TransferConfig config;
+    private final UploadPackMetricsHook uploadMetrics;
     private final DynamicSet<PreUploadHook> preUploadHooks;
 
     @Inject
-    UploadFactory(TransferConfig tc, DynamicSet<PreUploadHook> preUploadHooks) {
+    UploadFactory(TransferConfig tc,
+        UploadPackMetricsHook uploadMetrics,
+        DynamicSet<PreUploadHook> preUploadHooks) {
       this.config = tc;
+      this.uploadMetrics = uploadMetrics;
       this.preUploadHooks = preUploadHooks;
     }
 
@@ -211,6 +216,7 @@
       up.setTimeout(config.getTimeout());
       up.setPreUploadHook(PreUploadHookChain.newChain(
           Lists.newArrayList(preUploadHooks)));
+      up.setPostUploadHook(uploadMetrics);
       return up;
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 5e9c5aa..6eee544 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
 import com.google.gerrit.pgm.http.jetty.JettyEnv;
 import com.google.gerrit.pgm.http.jetty.JettyModule;
 import com.google.gerrit.pgm.http.jetty.ProjectQoSFilter;
@@ -323,6 +324,7 @@
   private Injector createSysInjector() {
     final List<Module> modules = new ArrayList<>();
     modules.add(SchemaVersionCheck.module());
+    modules.add(new DropWizardMetricMaker.Module());
     modules.add(new LogFileCompressor.Module());
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 4671f82..107b3f1 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -53,6 +53,7 @@
     '//lib/commons:lang',
     '//lib/commons:net',
     '//lib/commons:validator',
+    '//lib/dropwizard:dropwizard-core',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java
new file mode 100644
index 0000000..21e869b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+
+/**
+ * Metric whose value is supplied when the trigger is invoked.
+ *
+ * @see CallbackMetric0
+ * @param <V> type of the metric value, typically Integer or Long.
+ */
+public interface CallbackMetric<V> extends RegistrationHandle {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric0.java
new file mode 100644
index 0000000..043e25f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric0.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+/**
+ * Metric whose value is supplied when the trigger is invoked.
+ *
+ * <pre>
+ *   CallbackMetric0<Long> hits = metricMaker.newCallbackMetric("hits", ...);
+ *   CallbackMetric0<Long> total = metricMaker.newCallbackMetric("total", ...);
+ *   metricMaker.newTrigger(hits, total, new Runnable() {
+ *     public void run() {
+ *       hits.set(1);
+ *       total.set(5);
+ *     }
+ *   });
+ * </pre>
+ *
+ * @param <V> type of the metric value, typically Integer or Long.
+ */
+public abstract class CallbackMetric0<V> implements CallbackMetric<V> {
+  /**
+   * Supply the current value of the metric.
+   *
+   * @param value current value.
+   */
+  public abstract void set(V value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter0.java
new file mode 100644
index 0000000..c1d213f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter0.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Metric whose value increments during the life of the process.
+ * <p>
+ * Suitable uses are "total requests handled", "bytes sent", etc.
+ * Use {@link Description#setRate()} to suggest the monitoring system
+ * should also track the rate of increments if this is of interest.
+ * <p>
+ * For an instantaneous read of a value that can change over time
+ * (e.g. "memory in use") use a {@link CallbackMetric}.
+ */
+public abstract class Counter0 implements RegistrationHandle {
+  /** Increment the counter by one event. */
+  public void increment() {
+    incrementBy(1);
+  }
+
+  /**
+   * Increment the counter by a specified amount.
+   *
+   * @param value value to increment by, must be >= 0.
+   */
+  public abstract void incrementBy(long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter1.java
new file mode 100644
index 0000000..3477280
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter1.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Metric whose value increments during the life of the process.
+ * <p>
+ * Suitable uses are "total requests handled", "bytes sent", etc.
+ * Use {@link Description#setRate()} to suggest the monitoring system
+ * should also track the rate of increments if this is of interest.
+ * <p>
+ * For an instantaneous read of a value that can change over time
+ * (e.g. "memory in use") use a {@link CallbackMetric}.
+ *
+ * @param <F1> type of the field.
+ */
+public abstract class Counter1<F1> implements RegistrationHandle {
+  /** Increment the counter by one event. */
+  public void increment(F1 field1) {
+    incrementBy(field1, 1);
+  }
+
+  /**
+   * Increment the counter by a specified amount.
+   *
+   * @param field1 bucket to increment.
+   * @param value value to increment by, must be >= 0.
+   */
+  public abstract void incrementBy(F1 field1, long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter2.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter2.java
new file mode 100644
index 0000000..4bef791
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter2.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Metric whose value increments during the life of the process.
+ * <p>
+ * Suitable uses are "total requests handled", "bytes sent", etc.
+ * Use {@link Description#setRate()} to suggest the monitoring system
+ * should also track the rate of increments if this is of interest.
+ * <p>
+ * For an instantaneous read of a value that can change over time
+ * (e.g. "memory in use") use a {@link CallbackMetric}.
+ *
+ * @param <F1> type of the field.
+ * @param <F2> type of the field.
+ */
+public abstract class Counter2<F1, F2> implements RegistrationHandle {
+  /** Increment the counter by one event. */
+  public void increment(F1 field1, F2 field2) {
+    incrementBy(field1, field2, 1);
+  }
+
+  /**
+   * Increment the counter by a specified amount.
+   *
+   * @param field1 bucket to increment.
+   * @param field2 bucket to increment.
+   * @param value value to increment by, must be >= 0.
+   */
+  public abstract void incrementBy(F1 field1, F2 field2, long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter3.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter3.java
new file mode 100644
index 0000000..391e7e0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter3.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+/**
+ * Metric whose value increments during the life of the process.
+ * <p>
+ * Suitable uses are "total requests handled", "bytes sent", etc.
+ * Use {@link Description#setRate()} to suggest the monitoring system
+ * should also track the rate of increments if this is of interest.
+ * <p>
+ * For an instantaneous read of a value that can change over time
+ * (e.g. "memory in use") use a {@link CallbackMetric}.
+ *
+ * @param <F1> type of the field.
+ * @param <F2> type of the field.
+ * @param <F3> type of the field.
+ */
+public abstract class Counter3<F1, F2, F3> implements RegistrationHandle {
+  /** Increment the counter by one event. */
+  public void increment(F1 field1, F2 field2, F3 field3) {
+    incrementBy(field1, field2, field3, 1);
+  }
+
+  /**
+   * Increment the counter by a specified amount.
+   *
+   * @param field1 bucket to increment.
+   * @param field2 bucket to increment.
+   * @param field3 bucket to increment.
+   * @param value value to increment by, must be >= 0.
+   */
+  public abstract void incrementBy(F1 field1, F2 field2, F3 field3, long value);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java
new file mode 100644
index 0000000..fe244ed
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java
@@ -0,0 +1,172 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/** Describes a metric created by {@link MetricMaker}. */
+public class Description {
+  public static final String DESCRIPTION = "DESCRIPTION";
+  public static final String UNIT = "UNIT";
+  public static final String CUMULATIVE = "CUMULATIVE";
+  public static final String RATE = "RATE";
+  public static final String GAUGE = "GAUGE";
+  public static final String FIELD_ORDERING = "FIELD_ORDERING";
+  public static final String TRUE_VALUE = "1";
+
+  public static class Units {
+    public static final String SECONDS = "seconds";
+    public static final String MILLISECONDS = "milliseconds";
+    public static final String MICROSECONDS = "microseconds";
+    public static final String NANOSECONDS = "nanoseconds";
+
+    public static final String BYTES = "bytes";
+
+    private Units() {
+    }
+  }
+
+  public static enum FieldOrdering {
+    /** Default ordering places fields at end of the parent metric name. */
+    AT_END,
+
+    /**
+     * Splits the metric name by inserting field values before the last '/' in
+     * the metric name. For example {@code "plugins/replication/push_latency"}
+     * with a {@code Field.ofString("remote")} will create submetrics named
+     * {@code "plugins/replication/some-server/push_latency"}.
+     */
+    PREFIX_FIELDS_BASENAME;
+  }
+
+  private final Map<String, String> annotations;
+
+  /**
+   * Describe a metric.
+   *
+   * @param helpText a short one-sentence string explaining the values captured
+   *        by the metric. This may be made available to administrators as
+   *        documentation in the reporting tools.
+   */
+  public Description(String helpText) {
+    annotations = Maps.newLinkedHashMapWithExpectedSize(4);
+    annotations.put(DESCRIPTION, helpText);
+  }
+
+  /** Unit used to describe the value, e.g. "requests", "seconds", etc. */
+  public Description setUnit(String unitName) {
+    annotations.put(UNIT, unitName);
+    return this;
+  }
+
+  /**
+   * Indicates the metric may be usefully interpreted as a count over short
+   * periods of time, such as request arrival rate. May only be applied to a
+   * {@link Counter0}.
+   */
+  public Description setRate() {
+    annotations.put(RATE, TRUE_VALUE);
+    return this;
+  }
+
+  /**
+   * Instantaneously sampled value that may increase or decrease at a later
+   * time. Memory allocated or open network connections are examples of gauges.
+   */
+  public Description setGauge() {
+    annotations.put(GAUGE, TRUE_VALUE);
+    return this;
+  }
+
+  /**
+   * Indicates the metric accumulates over the lifespan of the process. A
+   * {@link Counter0} like total requests handled accumulates over the process
+   * and should be {@code setCumulative()}.
+   */
+  public Description setCumulative() {
+    annotations.put(CUMULATIVE, TRUE_VALUE);
+    return this;
+  }
+
+  /** Configure how fields are ordered into submetric names. */
+  public Description setFieldOrdering(FieldOrdering ordering) {
+    annotations.put(FIELD_ORDERING, ordering.name());
+    return this;
+  }
+
+  /** True if the metric may be interpreted as a rate over time. */
+  public boolean isRate() {
+    return TRUE_VALUE.equals(annotations.get(RATE));
+  }
+
+  /** True if the metric is an instantaneous sample. */
+  public boolean isGauge() {
+    return TRUE_VALUE.equals(annotations.get(GAUGE));
+  }
+
+  /** True if the metric accumulates over the lifespan of the process. */
+  public boolean isCumulative() {
+    return TRUE_VALUE.equals(annotations.get(CUMULATIVE));
+  }
+
+  /** Get the suggested field ordering. */
+  public FieldOrdering getFieldOrdering() {
+    String o = annotations.get(FIELD_ORDERING);
+    return o != null ? FieldOrdering.valueOf(o) : FieldOrdering.AT_END;
+  }
+
+  /**
+   * Decode the unit as a unit of time.
+   *
+   * @return valid time unit.
+   * @throws IllegalArgumentException if the unit is not a valid unit of time.
+   */
+  public TimeUnit getTimeUnit() {
+    return getTimeUnit(annotations.get(UNIT));
+  }
+
+  private static final ImmutableMap<String, TimeUnit> TIME_UNITS = ImmutableMap.of(
+      Units.NANOSECONDS, TimeUnit.NANOSECONDS,
+      Units.MICROSECONDS, TimeUnit.MICROSECONDS,
+      Units.MILLISECONDS, TimeUnit.MILLISECONDS,
+      Units.SECONDS, TimeUnit.SECONDS);
+
+  public static TimeUnit getTimeUnit(String unit) {
+    if (Strings.isNullOrEmpty(unit)) {
+      throw new IllegalArgumentException("no unit configured");
+    }
+    TimeUnit u = TIME_UNITS.get(unit);
+    if (u == null) {
+      throw new IllegalArgumentException(String.format(
+          "unit %s not TimeUnit", unit));
+    }
+    return u;
+  }
+
+  /** Immutable copy of all annotations (configurable properties). */
+  public ImmutableMap<String, String> getAnnotations() {
+    return ImmutableMap.copyOf(annotations);
+  }
+
+  @Override
+  public String toString() {
+    return annotations.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Field.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Field.java
new file mode 100644
index 0000000..a91e428
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Field.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Function;
+import com.google.common.base.Functions;
+
+/** Describes a bucketing field used by a metric. */
+public class Field<T> {
+  /** Break down metrics by boolean true/false. */
+  public static Field<Boolean> ofBoolean(String name) {
+    return ofBoolean(name, null);
+  }
+
+  /** Break down metrics by boolean true/false. */
+  public static Field<Boolean> ofBoolean(String name, String description) {
+    return new Field<>(name, Boolean.class, description);
+  }
+
+  /** Break down metrics by cases of an enum. */
+  public static <E extends Enum<E>> Field<E> ofEnum(Class<E> enumType,
+      String name) {
+    return ofEnum(enumType, name, null);
+  }
+
+  /** Break down metrics by cases of an enum. */
+  public static <E extends Enum<E>> Field<E> ofEnum(Class<E> enumType,
+      String name, String description) {
+    return new Field<>(name, enumType, description);
+  }
+
+  /**
+   * Break down metrics by string.
+   * <p>
+   * Each unique string will allocate a new submetric. <b>Do not use user
+   * content as a field value</b> as field values are never reclaimed.
+   */
+  public static Field<String> ofString(String name) {
+    return ofString(name, null);
+  }
+
+  /**
+   * Break down metrics by string.
+   * <p>
+   * Each unique string will allocate a new submetric. <b>Do not use user
+   * content as a field value</b> as field values are never reclaimed.
+   */
+  public static Field<String> ofString(String name, String description) {
+    return new Field<>(name, String.class, description);
+  }
+
+  /**
+   * Break down metrics by integer.
+   * <p>
+   * Each unique integer will allocate a new submetric. <b>Do not use user
+   * content as a field value</b> as field values are never reclaimed.
+   */
+  public static Field<Integer> ofInteger(String name) {
+    return ofInteger(name, null);
+  }
+
+  /**
+   * Break down metrics by integer.
+   * <p>
+   * Each unique integer will allocate a new submetric. <b>Do not use user
+   * content as a field value</b> as field values are never reclaimed.
+   */
+  public static Field<Integer> ofInteger(String name, String description) {
+    return new Field<>(name, Integer.class, description);
+  }
+
+  private final String name;
+  private final Class<T> keyType;
+  private final Function<T, String> formatter;
+  private final String description;
+
+  private Field(String name, Class<T> keyType, String description) {
+    checkArgument(name.matches("^[a-z_]+$"), "name must match [a-z_]");
+    this.name = name;
+    this.keyType = keyType;
+    this.formatter = initFormatter(keyType);
+    this.description = description;
+  }
+
+  /** Name of this field within the metric. */
+  public String getName() {
+    return name;
+  }
+
+  /** Type of value used within the field. */
+  public Class<T> getType() {
+    return keyType;
+  }
+
+  /** Description text for the field explaining its range of values. */
+  public String getDescription() {
+    return description;
+  }
+
+  public Function<T, String> formatter() {
+    return formatter;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> Function<T, String> initFormatter(Class<T> keyType) {
+    if (keyType == String.class) {
+      return (Function<T, String>) Functions.<String> identity();
+
+    } else if (keyType == Integer.class || keyType == Boolean.class) {
+      return (Function<T, String>) Functions.toStringFunction();
+
+    } else if (Enum.class.isAssignableFrom(keyType)) {
+      return new Function<T, String>() {
+        @Override
+        public String apply(T in) {
+          return ((Enum<?>) in).name();
+        }
+      };
+    }
+    throw new IllegalStateException("unsupported type " + keyType.getName());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java
new file mode 100644
index 0000000..55844b1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.Set;
+
+/** Factory to create metrics for monitoring. */
+public abstract class MetricMaker {
+  /** Metric whose value increments during the life of the process. */
+  public abstract Counter0 newCounter(String name, Description desc);
+  public abstract <F1> Counter1<F1> newCounter(
+      String name, Description desc,
+      Field<F1> field1);
+  public abstract <F1, F2> Counter2<F1, F2> newCounter(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2);
+  public abstract <F1, F2, F3> Counter3<F1, F2, F3> newCounter(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2, Field<F3> field3);
+
+  /** Metric recording time spent on an operation. */
+  public abstract Timer0 newTimer(String name, Description desc);
+  public abstract <F1> Timer1<F1> newTimer(
+      String name, Description desc,
+      Field<F1> field1);
+  public abstract <F1, F2> Timer2<F1, F2> newTimer(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2);
+  public abstract <F1, F2, F3> Timer3<F1, F2, F3> newTimer(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2, Field<F3> field3);
+
+  /**
+   * Instantaneous reading of a value.
+   *
+   * <pre>
+   * metricMaker.newCallbackMetric(&quot;memory&quot;,
+   *     new Description(&quot;Total bytes of memory used&quot;)
+   *        .setGauge()
+   *        .setUnit(Units.BYTES),
+   *     new Supplier&lt;Long&gt;() {
+   *       public Long get() {
+   *         return Runtime.getRuntime().totalMemory();
+   *       }
+   *     });
+   * </pre>
+   *
+   * @param name unique name of the metric.
+   * @param valueClass type of value recorded by the metric.
+   * @param desc description of the metric.
+   * @param trigger function to compute the value of the metric.
+   */
+  public <V> void newCallbackMetric(String name,
+      Class<V> valueClass, Description desc, final Supplier<V> trigger) {
+    final CallbackMetric0<V> metric = newCallbackMetric(name, valueClass, desc);
+    newTrigger(metric, new Runnable() {
+      @Override
+      public void run() {
+        metric.set(trigger.get());
+      }
+    });
+  }
+
+  /** Instantaneous reading of a single value. */
+  public abstract <V> CallbackMetric0<V> newCallbackMetric(
+      String name, Class<V> valueClass, Description desc);
+
+  /** Connect logic to populate a previously created {@link CallbackMetric}. */
+  public RegistrationHandle newTrigger(CallbackMetric<?> metric1, Runnable trigger) {
+    return newTrigger(ImmutableSet.<CallbackMetric<?>>of(metric1), trigger);
+  }
+
+  public RegistrationHandle newTrigger(CallbackMetric<?> metric1,
+      CallbackMetric<?> metric2, Runnable trigger) {
+    return newTrigger(ImmutableSet.of(metric1, metric2), trigger);
+  }
+
+  public RegistrationHandle newTrigger(CallbackMetric<?> metric1,
+      CallbackMetric<?> metric2, CallbackMetric<?> metric3, Runnable trigger) {
+    return newTrigger(ImmutableSet.of(metric1, metric2, metric3), trigger);
+  }
+
+  public abstract RegistrationHandle newTrigger(Set<CallbackMetric<?>> metrics,
+      Runnable trigger);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java
new file mode 100644
index 0000000..fc8cef3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Records elapsed time for an operation or span.
+ * <p>
+ * Typical usage in a try-with-resources block:
+ *
+ * <pre>
+ * try (Timer.Context ctx = timer.start()) {
+ * }
+ * </pre>
+ */
+public abstract class Timer0 implements RegistrationHandle {
+  public class Context implements AutoCloseable {
+    private final long startNanos;
+
+    Context() {
+      this.startNanos = System.nanoTime();
+    }
+
+    @Override
+    public void close() {
+      record(System.nanoTime() - startNanos, NANOSECONDS);
+    }
+  }
+
+  /** Begin a timer for the current block, value will be recorded when closed. */
+  public Context start() {
+    return new Context();
+  }
+
+  /** Record a value in the distribution. */
+  public abstract void record(long value, TimeUnit unit);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer1.java
new file mode 100644
index 0000000..88576f2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer1.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Records elapsed time for an operation or span.
+ * <p>
+ * Typical usage in a try-with-resources block:
+ *
+ * <pre>
+ * try (Timer1.Context ctx = timer.start(field)) {
+ * }
+ * </pre>
+ *
+ * @param <F1> type of the field.
+ */
+public abstract class Timer1<F1> implements RegistrationHandle {
+  public static class Context implements AutoCloseable {
+    private final Timer1<Object> timer;
+    private final Object field1;
+    private final long startNanos;
+
+    @SuppressWarnings("unchecked")
+    <F1> Context(Timer1<F1> timer, F1 field1) {
+      this.timer = (Timer1<Object>) timer;
+      this.field1 = field1;
+      this.startNanos = System.nanoTime();
+    }
+
+    @Override
+    public void close() {
+      timer.record(field1, System.nanoTime() - startNanos, NANOSECONDS);
+    }
+  }
+
+  /** Begin a timer for the current block, value will be recorded when closed. */
+  public Context start(F1 field1) {
+    return new Context(this, field1);
+  }
+
+  /** Record a value in the distribution. */
+  public abstract void record(F1 field1, long value, TimeUnit unit);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer2.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer2.java
new file mode 100644
index 0000000..f4ffebd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer2.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Records elapsed time for an operation or span.
+ * <p>
+ * Typical usage in a try-with-resources block:
+ *
+ * <pre>
+ * try (Timer2.Context ctx = timer.start(field)) {
+ * }
+ * </pre>
+ *
+ * @param <F1> type of the field.
+ * @param <F2> type of the field.
+ */
+public abstract class Timer2<F1, F2> implements RegistrationHandle {
+  public static class Context implements AutoCloseable {
+    private final Timer2<Object, Object> timer;
+    private final Object field1;
+    private final Object field2;
+    private final long startNanos;
+
+    @SuppressWarnings("unchecked")
+    <F1, F2> Context(Timer2<F1, F2> timer, F1 field1, F2 field2) {
+      this.timer = (Timer2<Object, Object>) timer;
+      this.field1 = field1;
+      this.field2 = field2;
+      this.startNanos = System.nanoTime();
+    }
+
+    @Override
+    public void close() {
+      timer.record(field1, field2, System.nanoTime() - startNanos, NANOSECONDS);
+    }
+  }
+
+  /** Begin a timer for the current block, value will be recorded when closed. */
+  public Context start(F1 field1, F2 field2) {
+    return new Context(this, field1, field2);
+  }
+
+  /** Record a value in the distribution. */
+  public abstract void record(F1 field1, F2 field2, long value, TimeUnit unit);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer3.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer3.java
new file mode 100644
index 0000000..60f0d5a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer3.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Records elapsed time for an operation or span.
+ * <p>
+ * Typical usage in a try-with-resources block:
+ *
+ * <pre>
+ * try (Timer3.Context ctx = timer.start(field)) {
+ * }
+ * </pre>
+ *
+ * @param <F1> type of the field.
+ * @param <F2> type of the field.
+ * @param <F3> type of the field.
+ */
+public abstract class Timer3<F1, F2, F3> implements RegistrationHandle {
+  public static class Context implements AutoCloseable {
+    private final Timer3<Object, Object, Object> timer;
+    private final Object field1;
+    private final Object field2;
+    private final Object field3;
+    private final long startNanos;
+
+    @SuppressWarnings("unchecked")
+    <F1, F2, F3> Context(Timer3<F1, F2, F3> timer, F1 f1, F2 f2, F3 f3) {
+      this.timer = (Timer3<Object, Object, Object>) timer;
+      this.field1 = f1;
+      this.field2 = f2;
+      this.field3 = f3;
+      this.startNanos = System.nanoTime();
+    }
+
+    @Override
+    public void close() {
+      timer.record(field1, field2, field3,
+          System.nanoTime() - startNanos, NANOSECONDS);
+    }
+  }
+
+  /** Begin a timer for the current block, value will be recorded when closed. */
+  public Context start(F1 field1, F2 field2, F3 field3) {
+    return new Context(this, field1, field2, field3);
+  }
+
+  /** Record a value in the distribution. */
+  public abstract void record(F1 field1, F2 field2, F3 field3,
+      long value, TimeUnit unit);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java
new file mode 100644
index 0000000..22af5ca
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker.CounterImpl;
+
+import com.codahale.metrics.Metric;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Abstract counter broken down into buckets by {@link Field} values. */
+abstract class BucketedCounter implements BucketedMetric {
+  private final DropWizardMetricMaker metrics;
+  private final String name;
+  private final boolean isRate;
+  private final Description.FieldOrdering ordering;
+  protected final Field<?>[] fields;
+  protected final CounterImpl total;
+  private final Map<Object, CounterImpl> cells;
+
+  BucketedCounter(DropWizardMetricMaker metrics,
+      String name, Description desc, Field<?>... fields) {
+    this.metrics = metrics;
+    this.name = name;
+    this.isRate = desc.isRate();
+    this.ordering = desc.getFieldOrdering();
+    this.fields = fields;
+    this.total = metrics.newCounterImpl(name + "_total", isRate);
+    this.cells = new ConcurrentHashMap<>();
+  }
+
+  void doRemove() {
+    for (CounterImpl c : cells.values()) {
+      c.remove();
+    }
+    total.remove();
+    metrics.remove(name);
+  }
+
+  CounterImpl forceCreate(Object f1, Object f2) {
+    return forceCreate(ImmutableList.of(f1, f2));
+  }
+
+  CounterImpl forceCreate(Object f1, Object f2, Object f3) {
+    return forceCreate(ImmutableList.of(f1, f2, f3));
+  }
+
+  CounterImpl forceCreate(Object key) {
+    CounterImpl c = cells.get(key);
+    if (c != null) {
+      return c;
+    }
+
+    synchronized (cells) {
+      c = cells.get(key);
+      if (c == null) {
+        c = metrics.newCounterImpl(submetric(key), isRate);
+        cells.put(key, c);
+      }
+      return c;
+    }
+  }
+
+  private String submetric(Object key) {
+    return DropWizardMetricMaker.name(ordering, name, name(key));
+  }
+
+  abstract String name(Object key);
+
+  @Override
+  public Metric getTotal() {
+    return total.metric;
+  }
+
+  @Override
+  public Field<?>[] getFields() {
+    return fields;
+  }
+
+  @Override
+  public Map<Object, Metric> getCells() {
+    return Maps.transformValues(
+        cells,
+        new Function<CounterImpl, Metric> () {
+          @Override
+          public Metric apply(CounterImpl in) {
+            return in.metric;
+          }
+        });
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedMetric.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedMetric.java
new file mode 100644
index 0000000..799e594
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedMetric.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.metrics.Field;
+
+import com.codahale.metrics.Metric;
+
+import java.util.Map;
+
+/** Metric broken down into buckets by {@link Field} values. */
+interface BucketedMetric extends Metric {
+  @Nullable Metric getTotal();
+  Field<?>[] getFields();
+  Map<?, Metric> getCells();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
new file mode 100644
index 0000000..ec12e00
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker.TimerImpl;
+
+import com.codahale.metrics.Metric;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Abstract timer broken down into buckets by {@link Field} values. */
+abstract class BucketedTimer implements BucketedMetric {
+  private final DropWizardMetricMaker metrics;
+  private final String name;
+  private final Description.FieldOrdering ordering;
+  protected final Field<?>[] fields;
+  protected final TimerImpl total;
+  private final Map<Object, TimerImpl> cells;
+
+  BucketedTimer(DropWizardMetricMaker metrics, String name,
+      Description desc, Field<?>... fields) {
+    this.metrics = metrics;
+    this.name = name;
+    this.ordering = desc.getFieldOrdering();
+    this.fields = fields;
+    this.total = metrics.newTimerImpl(name + "_total");
+    this.cells = new ConcurrentHashMap<>();
+  }
+
+  void doRemove() {
+    for (TimerImpl c : cells.values()) {
+      c.remove();
+    }
+    total.remove();
+    metrics.remove(name);
+  }
+
+  TimerImpl forceCreate(Object f1, Object f2) {
+    return forceCreate(ImmutableList.of(f1, f2));
+  }
+
+  TimerImpl forceCreate(Object f1, Object f2, Object f3) {
+    return forceCreate(ImmutableList.of(f1, f2, f3));
+  }
+
+  TimerImpl forceCreate(Object key) {
+    TimerImpl c = cells.get(key);
+    if (c != null) {
+      return c;
+    }
+
+    synchronized (cells) {
+      c = cells.get(key);
+      if (c == null) {
+        c = metrics.newTimerImpl(submetric(key));
+        cells.put(key, c);
+      }
+      return c;
+    }
+  }
+
+  private String submetric(Object key) {
+    return DropWizardMetricMaker.name(ordering, name, name(key));
+  }
+
+  abstract String name(Object key);
+
+  @Override
+  public Metric getTotal() {
+    return total.metric;
+  }
+
+  @Override
+  public Field<?>[] getFields() {
+    return fields;
+  }
+
+  @Override
+  public Map<Object, Metric> getCells() {
+    return Maps.transformValues(
+        cells,
+        new Function<TimerImpl, Metric> () {
+          @Override
+          public Metric apply(TimerImpl in) {
+            return in.metric;
+          }
+        });
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackGroup.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackGroup.java
new file mode 100644
index 0000000..94bbf7f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackGroup.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+class CallbackGroup implements Runnable {
+  private static final long PERIOD = TimeUnit.SECONDS.toNanos(2);
+
+  private final AtomicLong reloadAt;
+  private final Runnable trigger;
+
+  CallbackGroup(Runnable trigger) {
+    this.reloadAt = new AtomicLong(0);
+    this.trigger = trigger;
+  }
+
+  @Override
+  public void run() {
+    if (reload()) {
+      trigger.run();
+    }
+  }
+
+  private boolean reload() {
+    for (;;) {
+      long now = System.nanoTime();
+      long next = reloadAt.get();
+      if (next > now) {
+        return false;
+      } else if (reloadAt.compareAndSet(next, now + PERIOD)) {
+        return true;
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java
new file mode 100644
index 0000000..7ad2970
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.gerrit.metrics.CallbackMetric0;
+
+class CallbackMetricImpl0<V> extends CallbackMetric0<V> {
+  @SuppressWarnings("unchecked")
+  static <V> V zeroFor(Class<V> valueClass) {
+    if (valueClass == Integer.class) {
+      return (V) Integer.valueOf(0);
+    } else if (valueClass == Long.class) {
+      return (V) Long.valueOf(0);
+    } else if (valueClass == Double.class) {
+      return (V) Double.valueOf(0);
+    } else if (valueClass == Float.class) {
+      return (V) Float.valueOf(0);
+    } else if (valueClass == String.class) {
+      return (V) "";
+    } else if (valueClass == Boolean.class) {
+      return (V) Boolean.FALSE;
+    } else {
+      throw new IllegalArgumentException("unsupported value type "
+          + valueClass.getName());
+    }
+  }
+
+  final String name;
+  private V value;
+
+  CallbackMetricImpl0(String name, Class<V> valueType) {
+    this.name = name;
+    this.value = zeroFor(valueType);
+  }
+
+  @Override
+  public void set(V value) {
+    this.value = value;
+  }
+
+  @Override
+  public void remove() {
+    // Triggers register and remove the metric.
+  }
+
+  com.codahale.metrics.Gauge<V> gauge(final Runnable trigger) {
+    return new com.codahale.metrics.Gauge<V>() {
+      @Override
+      public V getValue() {
+        trigger.run();
+        return value;
+      }
+    };
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java
new file mode 100644
index 0000000..25647ef
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImpl1.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+
+/** Optimized version of {@link BucketedCounter} for single dimension. */
+class CounterImpl1<F1> extends BucketedCounter {
+  CounterImpl1(DropWizardMetricMaker metrics, String name, Description desc,
+      Field<F1> field1) {
+    super(metrics, name, desc, field1);
+  }
+
+  Counter1<F1> counter() {
+    return new Counter1<F1>() {
+      @Override
+      public void incrementBy(F1 field1, long value) {
+        total.incrementBy(value);
+        forceCreate(field1).incrementBy(value);
+      }
+
+      @Override
+      public void remove() {
+        doRemove();
+      }
+    };
+  }
+
+  @Override
+  String name(Object field1) {
+    @SuppressWarnings("unchecked")
+    Function<Object, String> fmt =
+        (Function<Object, String>) fields[0].formatter();
+
+    return fmt.apply(field1).replace('/', '-');
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java
new file mode 100644
index 0000000..a2f1f84
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CounterImplN.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+
+/** Generalized implementation of N-dimensional counter metrics. */
+class CounterImplN extends BucketedCounter implements BucketedMetric {
+  CounterImplN(DropWizardMetricMaker metrics, String name, Description desc,
+      Field<?>... fields) {
+    super(metrics, name, desc, fields);
+  }
+
+  <F1, F2> Counter2<F1, F2> counter2() {
+    return new Counter2<F1, F2>() {
+      @Override
+      public void incrementBy(F1 field1, F2 field2, long value) {
+        total.incrementBy(value);
+        forceCreate(field1, field2).incrementBy(value);
+      }
+
+      @Override
+      public void remove() {
+        doRemove();
+      }
+    };
+  }
+
+  <F1, F2, F3> Counter3<F1, F2, F3> counter3() {
+    return new Counter3<F1, F2, F3>() {
+      @Override
+      public void incrementBy(F1 field1, F2 field2, F3 field3, long value) {
+        total.incrementBy(value);
+        forceCreate(field1, field2, field3).incrementBy(value);
+      }
+
+      @Override
+      public void remove() {
+        doRemove();
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  String name(Object key) {
+    ImmutableList<Object> keyList = (ImmutableList<Object>) key;
+    String[] parts = new String[fields.length];
+    for (int i = 0; i < fields.length; i++) {
+      Function<Object, String> fmt =
+          (Function<Object, String>) fields[i].formatter();
+
+      parts[i] = fmt.apply(keyList.get(i)).replace('/', '-');
+    }
+    return Joiner.on('/').join(parts);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
new file mode 100644
index 0000000..336bf9e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
@@ -0,0 +1,317 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.metrics.dropwizard.MetricResource.METRIC_KIND;
+import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.metrics.CallbackMetric;
+import com.google.gerrit.metrics.CallbackMetric0;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.metrics.Timer2;
+import com.google.gerrit.metrics.Timer3;
+import com.google.gerrit.metrics.Description.FieldOrdering;
+import com.google.inject.Inject;
+import com.google.inject.Scopes;
+import com.google.inject.Singleton;
+
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricRegistry;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Connects Gerrit metric package onto DropWizard.
+ *
+ * @see <a href="http://www.dropwizard.io/">DropWizard</a>
+ */
+@Singleton
+public class DropWizardMetricMaker extends MetricMaker {
+  public static class Module extends RestApiModule {
+    @Override
+    protected void configure() {
+      bind(MetricRegistry.class).in(Scopes.SINGLETON);
+      bind(DropWizardMetricMaker.class).in(Scopes.SINGLETON);
+      bind(MetricMaker.class).to(DropWizardMetricMaker.class);
+
+      DynamicMap.mapOf(binder(), METRIC_KIND);
+      child(CONFIG_KIND, "metrics").to(MetricsCollection.class);
+      get(METRIC_KIND).to(GetMetric.class);
+    }
+  }
+
+  private final MetricRegistry registry;
+  private final Map<String, BucketedMetric> bucketed;
+  private final Map<String, ImmutableMap<String, String>> descriptions;
+
+  @Inject
+  DropWizardMetricMaker(MetricRegistry registry) {
+    this.registry = registry;
+    this.bucketed = new ConcurrentHashMap<>();
+    this.descriptions = new ConcurrentHashMap<>();
+  }
+
+  Iterable<String> getMetricNames() {
+    return descriptions.keySet();
+  }
+
+  /** Get the underlying metric implementation. */
+  public Metric getMetric(String name) {
+    Metric m = bucketed.get(name);
+    return m != null ? m : registry.getMetrics().get(name);
+  }
+
+  /** Lookup annotations from a metric's {@link Description}. */
+  public ImmutableMap<String, String> getAnnotations(String name) {
+    return descriptions.get(name);
+  }
+
+  @Override
+  public synchronized Counter0 newCounter(String name, Description desc) {
+    checkCounterDescription(desc);
+    define(name, desc);
+    return newCounterImpl(name, desc.isRate());
+  }
+
+  @Override
+  public synchronized <F1> Counter1<F1> newCounter(
+      String name, Description desc,
+      Field<F1> field1) {
+    checkCounterDescription(desc);
+    CounterImpl1<F1> m = new CounterImpl1<>(this, name, desc, field1);
+    define(name, desc);
+    bucketed.put(name, m);
+    return m.counter();
+  }
+
+  @Override
+  public synchronized <F1, F2> Counter2<F1, F2> newCounter(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2) {
+    checkCounterDescription(desc);
+    CounterImplN m = new CounterImplN(this, name, desc, field1, field2);
+    define(name, desc);
+    bucketed.put(name, m);
+    return m.counter2();
+  }
+
+  @Override
+  public synchronized <F1, F2, F3> Counter3<F1, F2, F3> newCounter(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+    checkCounterDescription(desc);
+    CounterImplN m = new CounterImplN(this, name, desc, field1, field2, field3);
+    define(name, desc);
+    bucketed.put(name, m);
+    return m.counter3();
+  }
+
+  private static void checkCounterDescription(Description desc) {
+    checkArgument(!desc.isGauge(), "counters must not be gauge");
+  }
+
+  CounterImpl newCounterImpl(String name, boolean isRate) {
+    if (isRate) {
+      final com.codahale.metrics.Meter m = registry.meter(name);
+      return new CounterImpl(name, m) {
+        @Override
+        public void incrementBy(long delta) {
+          checkArgument(delta >= 0, "counter delta must be >= 0");
+          m.mark(delta);
+        }
+      };
+    } else {
+      final com.codahale.metrics.Counter m = registry.counter(name);
+      return new CounterImpl(name, m) {
+        @Override
+        public void incrementBy(long delta) {
+          checkArgument(delta >= 0, "counter delta must be >= 0");
+          m.inc(delta);
+        }
+      };
+    }
+  }
+
+  @Override
+  public synchronized Timer0 newTimer(String name, Description desc) {
+    checkTimerDescription(desc);
+    define(name, desc);
+    return newTimerImpl(name);
+  }
+
+  @Override
+  public synchronized <F1> Timer1<F1> newTimer(String name, Description desc, Field<F1> field1) {
+    checkTimerDescription(desc);
+    TimerImpl1<F1> m = new TimerImpl1<>(this, name, desc, field1);
+    define(name, desc);
+    bucketed.put(name, m);
+    return m.timer();
+  }
+
+  @Override
+  public synchronized <F1, F2> Timer2<F1, F2> newTimer(String name, Description desc,
+      Field<F1> field1, Field<F2> field2) {
+    checkTimerDescription(desc);
+    TimerImplN m = new TimerImplN(this, name, desc, field1, field2);
+    define(name, desc);
+    bucketed.put(name, m);
+    return m.timer2();
+  }
+
+  @Override
+  public synchronized <F1, F2, F3> Timer3<F1, F2, F3> newTimer(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+    checkTimerDescription(desc);
+    TimerImplN m = new TimerImplN(this, name, desc, field1, field2, field3);
+    define(name, desc);
+    bucketed.put(name, m);
+    return m.timer3();
+  }
+
+  private static void checkTimerDescription(Description desc) {
+    checkArgument(!desc.isGauge(), "timer must not be a gauge");
+    checkArgument(!desc.isRate(), "timer must not be a rate");
+    checkArgument(desc.isCumulative(), "timer must be cumulative");
+    checkArgument(desc.getTimeUnit() != null, "timer must have a unit");
+  }
+
+  TimerImpl newTimerImpl(String name) {
+    return new TimerImpl(name, registry.timer(name));
+  }
+
+  @Override
+  public <V> CallbackMetric0<V> newCallbackMetric(
+      String name, Class<V> valueClass, Description desc) {
+    define(name, desc);
+    return new CallbackMetricImpl0<>(name, valueClass);
+  }
+
+  @Override
+  public synchronized RegistrationHandle newTrigger(
+      Set<CallbackMetric<?>> metrics, Runnable trigger) {
+    if (metrics.size() > 1) {
+      trigger = new CallbackGroup(trigger);
+    }
+
+    for (CallbackMetric<?> m : metrics) {
+      CallbackMetricImpl0<?> metric = (CallbackMetricImpl0<?>) m;
+      if (registry.getMetrics().containsKey(metric.name)) {
+        throw new IllegalStateException(String.format(
+            "metric %s already configured", metric.name));
+      }
+    }
+
+    final List<String> names = new ArrayList<>(metrics.size());
+    for (CallbackMetric<?> m : metrics) {
+      CallbackMetricImpl0<?> metric = (CallbackMetricImpl0<?>) m;
+      registry.register(metric.name, metric.gauge(trigger));
+      names.add(metric.name);
+    }
+
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        for (String name : names) {
+          descriptions.remove(name);
+          registry.remove(name);
+        }
+      }
+    };
+  }
+
+  synchronized void remove(String name) {
+    bucketed.remove(name);
+    descriptions.remove(name);
+  }
+
+  private synchronized void define(String name, Description desc) {
+    if (descriptions.containsKey(name)) {
+      throw new IllegalStateException(String.format(
+          "metric %s already defined", name));
+    }
+    descriptions.put(name, desc.getAnnotations());
+  }
+
+  static String name(Description.FieldOrdering ordering,
+      String codeName,
+      String fieldValues) {
+    if (ordering == FieldOrdering.PREFIX_FIELDS_BASENAME) {
+      int s = codeName.lastIndexOf('/');
+      if (s > 0) {
+        String prefix = codeName.substring(0, s);
+        String metric = codeName.substring(s + 1);
+        return prefix + '/' + fieldValues + '/' + metric;
+      }
+    }
+    return codeName + '/' + fieldValues;
+  }
+
+  abstract class CounterImpl extends Counter0 {
+    private final String name;
+    final Metric metric;
+
+    CounterImpl(String name, Metric metric) {
+      this.name = name;
+      this.metric = metric;
+    }
+
+    @Override
+    public void remove() {
+      descriptions.remove(name);
+      registry.remove(name);
+    }
+  }
+
+  class TimerImpl extends Timer0 {
+    private final String name;
+    final com.codahale.metrics.Timer metric;
+
+    private TimerImpl(String name, com.codahale.metrics.Timer metric) {
+      this.name = name;
+      this.metric = metric;
+    }
+
+    @Override
+    public void record(long value, TimeUnit unit) {
+      checkArgument(value >= 0, "timer delta must be >= 0");
+      metric.update(value, unit);
+    }
+
+    @Override
+    public void remove() {
+      descriptions.remove(name);
+      registry.remove(name);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
new file mode 100644
index 0000000..47064df
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+class GetMetric implements RestReadView<MetricResource> {
+  private final CurrentUser user;
+  private final DropWizardMetricMaker metrics;
+
+  @Option(name = "--data-only", usage = "return only values")
+  boolean dataOnly;
+
+  @Inject
+  GetMetric(CurrentUser user, DropWizardMetricMaker metrics) {
+    this.user = user;
+    this.metrics = metrics;
+  }
+
+  @Override
+  public MetricJson apply(MetricResource resource) throws AuthException {
+    if (!user.getCapabilities().canViewCaches()) {
+      throw new AuthException("restricted to viewCaches");
+    }
+    return new MetricJson(
+        resource.getMetric(),
+        metrics.getAnnotations(resource.getName()),
+        dataOnly);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
new file mode 100644
index 0000000..04d10a2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.Inject;
+
+import com.codahale.metrics.Metric;
+
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+class ListMetrics implements RestReadView<ConfigResource> {
+  private final CurrentUser user;
+  private final DropWizardMetricMaker metrics;
+
+  @Option(name = "--data-only", usage = "return only values")
+  boolean dataOnly;
+
+  @Option(name = "--prefix", aliases = {"-p"}, metaVar = "PREFIX",
+      usage = "match metric by exact match or prefix")
+  List<String> query = new ArrayList<>();
+
+  @Inject
+  ListMetrics(CurrentUser user, DropWizardMetricMaker metrics) {
+    this.user = user;
+    this.metrics = metrics;
+  }
+
+  @Override
+  public Map<String, MetricJson> apply(ConfigResource resource)
+      throws AuthException {
+    if (!user.getCapabilities().canViewCaches()) {
+      throw new AuthException("restricted to viewCaches");
+    }
+
+    SortedMap<String, MetricJson> out = new TreeMap<>();
+    List<String> prefixes = new ArrayList<>(query.size());
+    for (String q : query) {
+      if (q.endsWith("/")) {
+        prefixes.add(q);
+      } else {
+        Metric m = metrics.getMetric(q);
+        if (m != null) {
+          out.put(q, toJson(q, m));
+        }
+      }
+    }
+
+    if (query.isEmpty() || !prefixes.isEmpty()) {
+      for (String name : metrics.getMetricNames()) {
+        if (include(prefixes, name)) {
+          out.put(name, toJson(name, metrics.getMetric(name)));
+        }
+      }
+    }
+
+    return out;
+  }
+
+  private MetricJson toJson(String q, Metric m) {
+    return new MetricJson(m, metrics.getAnnotations(q), dataOnly);
+  }
+
+  private static boolean include(List<String> prefixes, String name) {
+    if (prefixes.isEmpty()) {
+      return true;
+    }
+    for (String p : prefixes) {
+      if (name.startsWith(p)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricJson.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
new file mode 100644
index 0000000..f43dd6f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricJson.java
@@ -0,0 +1,187 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.Snapshot;
+import com.codahale.metrics.Timer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+class MetricJson {
+  String description;
+  String unit;
+  Boolean rate;
+  Boolean gauge;
+  Boolean cumulative;
+
+  Long count;
+  Object value;
+
+  Double rate_1m;
+  Double rate_5m;
+  Double rate_15m;
+  Double rate_mean;
+
+  Double p50;
+  Double p75;
+  Double p95;
+  Double p98;
+  Double p99;
+  Double p99_9;
+
+  Double min;
+  Double max;
+  Double std_dev;
+
+  List<FieldJson> fields;
+  Map<String, Object> buckets;
+
+  MetricJson(Metric metric, ImmutableMap<String, String> atts, boolean dataOnly) {
+    if (!dataOnly) {
+      description = atts.get(Description.DESCRIPTION);
+      unit = atts.get(Description.UNIT);
+      rate = toBool(atts, Description.RATE);
+      gauge = toBool(atts, Description.GAUGE);
+      cumulative = toBool(atts, Description.CUMULATIVE);
+    }
+    init(metric, atts);
+  }
+
+  private void init(Metric metric, ImmutableMap<String, String> atts) {
+    if (metric instanceof BucketedMetric) {
+      BucketedMetric m = (BucketedMetric) metric;
+      if (m.getTotal() != null) {
+        init(m.getTotal(), atts);
+      }
+
+      Field<?>[] fieldList = m.getFields();
+      fields = new ArrayList<>(fieldList.length);
+      for (Field<?> f : fieldList) {
+        fields.add(new FieldJson(f));
+      }
+      buckets = makeBuckets(fieldList, m.getCells(), atts);
+
+    } else if (metric instanceof Counter) {
+      Counter c = (Counter) metric;
+      count = c.getCount();
+
+    } else if (metric instanceof Gauge) {
+      Gauge<?> g = (Gauge<?>) metric;
+      value = g.getValue();
+
+    } else if (metric instanceof Meter) {
+      Meter m = (Meter) metric;
+      count = m.getCount();
+      rate_1m = m.getOneMinuteRate();
+      rate_5m = m.getFiveMinuteRate();
+      rate_15m = m.getFifteenMinuteRate();
+
+    } else if (metric instanceof Timer) {
+      Timer m = (Timer) metric;
+      Snapshot s = m.getSnapshot();
+      count = m.getCount();
+      rate_1m = m.getOneMinuteRate();
+      rate_5m = m.getFiveMinuteRate();
+      rate_15m = m.getFifteenMinuteRate();
+
+      double div =
+          Description.getTimeUnit(atts.get(Description.UNIT)).toNanos(1);
+      p50 = s.getMedian() / div;
+      p75 = s.get75thPercentile() / div;
+      p95 = s.get95thPercentile() / div;
+      p98 = s.get98thPercentile() / div;
+      p99 = s.get99thPercentile() / div;
+      p99_9 = s.get999thPercentile() / div;
+
+      min = s.getMin() / div;
+      max = s.getMax() / div;
+      std_dev = s.getStdDev() / div;
+    }
+  }
+
+  private static Boolean toBool(ImmutableMap<String, String> atts, String key) {
+    return Description.TRUE_VALUE.equals(atts.get(key)) ? true : null;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Map<String, Object> makeBuckets(
+      Field<?>[] fields,
+      Map<?, Metric> metrics,
+      ImmutableMap<String, String> atts) {
+    if (fields.length == 1) {
+      Function<Object, String> fmt =
+          (Function<Object, String>) fields[0].formatter();
+      Map<String, Object> out = new TreeMap<>();
+      for (Map.Entry<?, Metric> e : metrics.entrySet()) {
+        out.put(
+            fmt.apply(e.getKey()),
+            new MetricJson(e.getValue(), atts, true));
+      }
+      return out;
+    }
+
+    Map<String, Object> out = new TreeMap<>();
+    for (Map.Entry<?, Metric> e : metrics.entrySet()) {
+      ImmutableList<Object> keys = (ImmutableList<Object>) e.getKey();
+      Map<String, Object> dst = out;
+
+      for (int i = 0; i < fields.length - 1; i++) {
+        Function<Object, String> fmt =
+            (Function<Object, String>) fields[i].formatter();
+        String key = fmt.apply(keys.get(i));
+        Map<String, Object> t = (Map<String, Object>) dst.get(key);
+        if (t == null) {
+          t = new TreeMap<>();
+          dst.put(key, t);
+        }
+        dst = t;
+      }
+
+      Function<Object, String> fmt =
+          (Function<Object, String>) fields[fields.length - 1].formatter();
+      dst.put(
+          fmt.apply(keys.get(fields.length - 1)),
+          new MetricJson(e.getValue(), atts, true));
+    }
+    return out;
+  }
+
+  static class FieldJson {
+    String name;
+    String type;
+    String description;
+
+    FieldJson(Field<?> field) {
+      this.name = field.getName();
+      this.description = field.getDescription();
+      this.type = Enum.class.isAssignableFrom(field.getType())
+          ? field.getType().getSimpleName()
+          : null;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricResource.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricResource.java
new file mode 100644
index 0000000..d073f37
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricResource.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.TypeLiteral;
+
+import com.codahale.metrics.Metric;
+
+class MetricResource extends ConfigResource {
+  static final TypeLiteral<RestView<MetricResource>> METRIC_KIND =
+      new TypeLiteral<RestView<MetricResource>>() {};
+
+  private final String name;
+  private final Metric metric;
+
+  MetricResource(String name, Metric metric) {
+    this.name = name;
+    this.metric = metric;
+  }
+
+  String getName() {
+    return name;
+  }
+
+  Metric getMetric() {
+    return metric;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
new file mode 100644
index 0000000..81945f1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import com.codahale.metrics.Metric;
+
+@Singleton
+class MetricsCollection implements
+    ChildCollection<ConfigResource, MetricResource> {
+  private final DynamicMap<RestView<MetricResource>> views;
+  private final Provider<ListMetrics> list;
+  private final Provider<CurrentUser> user;
+  private final DropWizardMetricMaker metrics;
+
+  @Inject
+  MetricsCollection(DynamicMap<RestView<MetricResource>> views,
+      Provider<ListMetrics> list, Provider<CurrentUser> user,
+      DropWizardMetricMaker metrics) {
+    this.views = views;
+    this.list = list;
+    this.user = user;
+    this.metrics = metrics;
+  }
+
+  @Override
+  public DynamicMap<RestView<MetricResource>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<ConfigResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public MetricResource parse(ConfigResource parent, IdString id)
+      throws ResourceNotFoundException, AuthException {
+    if (!user.get().getCapabilities().canViewCaches()) {
+      throw new AuthException("restricted to viewCaches");
+    }
+
+    Metric metric = metrics.getMetric(id.get());
+    if (metric == null) {
+      throw new ResourceNotFoundException(id.get());
+    }
+    return new MetricResource(id.get(), metric);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
new file mode 100644
index 0000000..0164f6f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Timer1;
+
+import java.util.concurrent.TimeUnit;
+
+/** Optimized version of {@link BucketedTimer} for single dimension. */
+class TimerImpl1<F1> extends BucketedTimer implements BucketedMetric {
+  TimerImpl1(DropWizardMetricMaker metrics, String name,
+      Description desc, Field<F1> field1) {
+    super(metrics, name, desc, field1);
+  }
+
+  Timer1<F1> timer() {
+    return new Timer1<F1>() {
+      @Override
+      public void record(F1 field1, long value, TimeUnit unit) {
+        total.record(value, unit);
+        forceCreate(field1).record(value, unit);
+      }
+
+      @Override
+      public void remove() {
+        doRemove();
+      }
+    };
+  }
+
+  @Override
+  String name(Object field1) {
+    @SuppressWarnings("unchecked")
+    Function<Object, String> fmt =
+        (Function<Object, String>) fields[0].formatter();
+
+    return fmt.apply(field1).replace('/', '-');
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
new file mode 100644
index 0000000..49c9f14
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImplN.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.metrics.dropwizard;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.Timer2;
+import com.google.gerrit.metrics.Timer3;
+
+import java.util.concurrent.TimeUnit;
+
+/** Generalized implementation of N-dimensional timer metrics. */
+class TimerImplN extends BucketedTimer implements BucketedMetric {
+  TimerImplN(DropWizardMetricMaker metrics, String name,
+      Description desc, Field<?>... fields) {
+    super(metrics, name, desc, fields);
+  }
+
+  <F1, F2> Timer2<F1, F2> timer2() {
+    return new Timer2<F1, F2>() {
+      @Override
+      public void record(F1 field1, F2 field2, long value, TimeUnit unit) {
+        total.record(value, unit);
+        forceCreate(field1, field2).record(value, unit);
+      }
+
+      @Override
+      public void remove() {
+        doRemove();
+      }
+    };
+  }
+
+  <F1, F2, F3> Timer3<F1, F2, F3> timer3() {
+    return new Timer3<F1, F2, F3>() {
+      @Override
+      public void record(F1 field1, F2 field2, F3 field3,
+          long value, TimeUnit unit) {
+        total.record(value, unit);
+        forceCreate(field1, field2, field3).record(value, unit);
+      }
+
+      @Override
+      public void remove() {
+        doRemove();
+      }
+    };
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  String name(Object key) {
+    ImmutableList<Object> keyList = (ImmutableList<Object>) key;
+    String[] parts = new String[fields.length];
+    for (int i = 0; i < fields.length; i++) {
+      Function<Object, String> fmt =
+          (Function<Object, String>) fields[i].formatter();
+
+      parts[i] = fmt.apply(keyList.get(i)).replace('/', '-');
+    }
+    return Joiner.on('/').join(parts);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
new file mode 100644
index 0000000..cbaca6b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.server.git;
+
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.storage.pack.PackStatistics;
+import org.eclipse.jgit.transport.PostUploadHook;
+
+@Singleton
+public class UploadPackMetricsHook implements PostUploadHook {
+  enum Operation {
+    CLONE,
+    FETCH;
+  }
+
+  private final Counter1<Operation> upload;
+
+  @Inject
+  UploadPackMetricsHook(MetricMaker metricMaker) {
+    upload = metricMaker.newCounter(
+        "git/upload-pack",
+        new Description("Total number of git-upload-pack requests")
+          .setRate()
+          .setUnit("requests"),
+        Field.ofEnum(Operation.class, "operation"));
+  }
+
+  @Override
+  public void onPostUpload(PackStatistics stats) {
+    Operation op = Operation.FETCH;
+    if (stats.getUninterestingObjects() == null
+        || stats.getUninterestingObjects().isEmpty()) {
+      op = Operation.CLONE;
+    }
+    upload.increment(op);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 6b458aa..5df364f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.util.PluginRequestContext;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -77,6 +78,7 @@
   private final List<StartPluginListener> onStart;
   private final List<StopPluginListener> onStop;
   private final List<ReloadPluginListener> onReload;
+  private final MetricMaker serverMetrics;
 
   private Module sysModule;
   private Module sshModule;
@@ -102,12 +104,14 @@
       Injector sysInjector,
       ThreadLocalRequestContext local,
       ServerInformation srvInfo,
-      CopyConfigModule ccm) {
+      CopyConfigModule ccm,
+      MetricMaker serverMetrics) {
     this.sysInjector = sysInjector;
     this.srvInfo = srvInfo;
     this.local = local;
     this.copyConfigModule = ccm;
     this.copyConfigKeys = Guice.createInjector(ccm).getAllBindings().keySet();
+    this.serverMetrics = serverMetrics;
 
     onStart = new CopyOnWriteArrayList<>();
     onStart.addAll(listeners(sysInjector, StartPluginListener.class));
@@ -127,6 +131,10 @@
     return srvInfo;
   }
 
+  MetricMaker getServerMetrics() {
+    return serverMetrics;
+  }
+
   boolean hasDynamicItem(TypeLiteral<?> type) {
     return sysItems.containsKey(type)
         || (sshItems != null && sshItems.containsKey(type))
@@ -424,6 +432,7 @@
       }
     }
   }
+
   private void reattachItem(
       ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
       Map<TypeLiteral<?>, DynamicItem<?>> items,
@@ -564,6 +573,9 @@
     if (StopPluginListener.class.isAssignableFrom(type)) {
       return false;
     }
+    if (MetricMaker.class.isAssignableFrom(type)) {
+      return false;
+    }
 
     if (type.getName().startsWith("com.google.inject.")) {
       return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginMetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginMetricMaker.java
new file mode 100644
index 0000000..8bf78b5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginMetricMaker.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed 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 com.google.gerrit.server.plugins;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.metrics.CallbackMetric;
+import com.google.gerrit.metrics.CallbackMetric0;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Counter1;
+import com.google.gerrit.metrics.Counter2;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.metrics.Timer1;
+import com.google.gerrit.metrics.Timer2;
+import com.google.gerrit.metrics.Timer3;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+class PluginMetricMaker extends MetricMaker implements LifecycleListener {
+  private final MetricMaker root;
+  private final String prefix;
+  private final Set<RegistrationHandle> cleanup;
+
+  PluginMetricMaker(MetricMaker root, String pluginName) {
+    this.root = root;
+    this.prefix = "plugins/" + pluginName;
+    cleanup = Collections.synchronizedSet(new HashSet<RegistrationHandle>());
+  }
+
+  @Override
+  public Counter0 newCounter(String name, Description desc) {
+    Counter0 m = root.newCounter(prefix + name, desc);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public <F1> Counter1<F1> newCounter(
+      String name, Description desc,
+      Field<F1> field1) {
+    Counter1<F1> m = root.newCounter(prefix + name, desc, field1);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public <F1, F2> Counter2<F1, F2> newCounter(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2) {
+    Counter2<F1, F2> m = root.newCounter(prefix + name, desc, field1, field2);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public <F1, F2, F3> Counter3<F1, F2, F3> newCounter(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+    Counter3<F1, F2, F3> m =
+        root.newCounter(prefix + name, desc, field1, field2, field3);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public Timer0 newTimer(String name, Description desc) {
+    Timer0 m = root.newTimer(prefix + name, desc);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public <F1> Timer1<F1> newTimer(
+      String name, Description desc,
+      Field<F1> field1) {
+    Timer1<F1> m = root.newTimer(prefix + name, desc, field1);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public <F1, F2> Timer2<F1, F2> newTimer(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2) {
+    Timer2<F1, F2> m = root.newTimer(prefix + name, desc, field1, field2);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public <F1, F2, F3> Timer3<F1, F2, F3> newTimer(
+      String name, Description desc,
+      Field<F1> field1, Field<F2> field2, Field<F3> field3) {
+    Timer3<F1, F2, F3> m =
+        root.newTimer(prefix + name, desc, field1, field2, field3);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public <V> CallbackMetric0<V> newCallbackMetric(
+      String name, Class<V> valueClass, Description desc) {
+    CallbackMetric0<V> m = root.newCallbackMetric(prefix + name, valueClass, desc);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public RegistrationHandle newTrigger(Set<CallbackMetric<?>> metrics,
+      Runnable trigger) {
+    final RegistrationHandle handle = root.newTrigger(metrics, trigger);
+    cleanup.add(handle);
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        handle.remove();
+        cleanup.remove(handle);
+      }
+    };
+  }
+
+  @Override
+  public void start() {
+  }
+
+  @Override
+  public void stop() {
+    synchronized (cleanup) {
+      Iterator<RegistrationHandle> itr = cleanup.iterator();
+      while (itr.hasNext()) {
+        itr.next().remove();
+        itr.remove();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
index 14c1185..ea96a56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -236,7 +236,7 @@
     if (getApiType() == ApiType.PLUGIN) {
       modules.add(env.getSysModule());
     }
-    modules.add(new ServerPluginInfoModule(this));
+    modules.add(new ServerPluginInfoModule(this, env.getServerMetrics()));
     return Guice.createInjector(modules);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
index b0e9453..a7f0087 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPluginInfoModule.java
@@ -17,6 +17,8 @@
 import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
 import com.google.gerrit.extensions.annotations.PluginData;
 import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.PluginUser;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
@@ -32,10 +34,12 @@
   private final Path dataDir;
 
   private volatile boolean ready;
+  private final MetricMaker serverMetrics;
 
-  ServerPluginInfoModule(ServerPlugin plugin) {
+  ServerPluginInfoModule(ServerPlugin plugin, MetricMaker serverMetrics) {
     this.plugin = plugin;
     this.dataDir = plugin.getDataDir();
+    this.serverMetrics = serverMetrics;
   }
 
   @Override
@@ -47,6 +51,17 @@
     bind(String.class)
       .annotatedWith(PluginCanonicalWebUrl.class)
       .toInstance(plugin.getPluginCanonicalWebUrl());
+
+    install(new LifecycleModule() {
+      @Override
+      public void configure() {
+        PluginMetricMaker metrics = new PluginMetricMaker(
+            serverMetrics,
+            plugin.getName());
+        bind(MetricMaker.class).toInstance(metrics);
+        listener().toInstance(metrics);
+      }
+    });
   }
 
   @Provides
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index a2c8b81..c3c70b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -20,6 +20,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.index.IndexConfig;
@@ -32,6 +35,7 @@
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -42,6 +46,7 @@
   private final ChangeControl.GenericFactory changeControlFactory;
   private final IndexRewriter rewriter;
   private final IndexConfig indexConfig;
+  private final Metrics metrics;
 
   private int limitFromCaller;
   private int start;
@@ -52,12 +57,14 @@
       Provider<CurrentUser> userProvider,
       ChangeControl.GenericFactory changeControlFactory,
       IndexRewriter rewriter,
-      IndexConfig indexConfig) {
+      IndexConfig indexConfig,
+      Metrics metrics) {
     this.db = db;
     this.userProvider = userProvider;
     this.changeControlFactory = changeControlFactory;
     this.rewriter = rewriter;
     this.indexConfig = indexConfig;
+    this.metrics = metrics;
   }
 
   public QueryProcessor enforceVisibility(boolean enforce) {
@@ -114,6 +121,9 @@
   private List<QueryResult> queryChanges(List<String> queryStrings,
       List<Predicate<ChangeData>> queries)
       throws OrmException, QueryParseException {
+    @SuppressWarnings("resource")
+    Timer0.Context context = metrics.executionTime.start();
+
     Predicate<ChangeData> visibleToMe = enforceVisibility
         ? new IsVisibleToPredicate(db, changeControlFactory, userProvider.get())
         : null;
@@ -170,6 +180,7 @@
           limits.get(i),
           matches.get(i).toList()));
     }
+    context.close(); // only measure successful queries
     return out;
   }
 
@@ -203,4 +214,19 @@
     }
     return Ordering.natural().min(possibleLimits);
   }
+
+  @Singleton
+  static class Metrics {
+    final Timer0 executionTime;
+
+    @Inject
+    Metrics(MetricMaker metricMaker) {
+      executionTime = metricMaker.newTimer(
+          "change/query/query_latency",
+          new Description("Successful change query latency,"
+              + " accumulated over the life of the process")
+            .setCumulative()
+            .setUnit(Description.Units.MILLISECONDS));
+    }
+  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index e5cd619..7d22c73 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.common.DisabledChangeHooks;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.gpg.GpgModule;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -132,6 +133,7 @@
             .toInstance(cfg);
       }
     });
+    install(new DropWizardMetricMaker.Module());
     install(cfgInjector.getInstance(GerritGlobalModule.class));
     install(new ChangeCacheImplModule(false));
     factory(GarbageCollection.Factory.class);
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index dcff98e..279b024 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -21,6 +21,7 @@
     '//lib/auto:auto-value',
     '//lib/commons:codec',
     '//lib/commons:collections',
+    '//lib/dropwizard:dropwizard-core',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',  # SSH should not depend on servlet
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 3afb208..6bbc5ab 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -19,10 +19,14 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
@@ -126,6 +130,7 @@
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * SSH daemon to communicate with Gerrit.
@@ -170,7 +175,8 @@
       final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
       @GerritServerConfig final Config cfg, final SshLog sshLog,
       @SshListenAddresses final List<SocketAddress> listen,
-      @SshAdvertisedAddresses final List<String> advertised) {
+      @SshAdvertisedAddresses final List<String> advertised,
+      MetricMaker metricMaker) {
     setPort(IANA_SSH_PORT /* never used */);
 
     this.cfg = cfg;
@@ -245,10 +251,33 @@
     setKeyPairProvider(hostKeyProvider);
     setCommandFactory(commandFactory);
     setShellFactory(noShell);
+
+    final AtomicInteger connected = new AtomicInteger();
+    metricMaker.newCallbackMetric(
+        "sshd/sessions/connected",
+        Integer.class,
+        new Description("Currently connected SSH sessions")
+          .setGauge()
+          .setUnit("sessions"),
+        new Supplier<Integer>() {
+          @Override
+          public Integer get() {
+            return connected.get();
+          }
+        });
+
+    final Counter0 sesssionsCreated = metricMaker.newCounter(
+        "sshd/sessions/created",
+        new Description("Rate of new SSH sessions")
+          .setRate()
+          .setUnit("sessions"));
+
     setSessionFactory(new SessionFactory() {
       @Override
       protected AbstractSession createSession(final IoSession io)
           throws Exception {
+        connected.incrementAndGet();
+        sesssionsCreated.increment();
         if (io instanceof MinaSession) {
           if (((MinaSession) io).getSession()
               .getConfig() instanceof SocketSessionConfig) {
@@ -269,6 +298,7 @@
         s.addCloseSessionListener(new SshFutureListener<CloseFuture>() {
           @Override
           public void operationComplete(CloseFuture future) {
+            connected.decrementAndGet();
             if (sd.isAuthenticationError()) {
               sshLog.onAuthFail(sd);
             }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
index 9873c04..d278f4b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.UploadPackMetricsHook;
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.server.git.validators.UploadValidationException;
 import com.google.gerrit.server.git.validators.UploadValidators;
@@ -58,6 +59,9 @@
   @Inject
   private SshSession session;
 
+  @Inject
+  private UploadPackMetricsHook uploadMetrics;
+
   @Override
   protected void runImpl() throws IOException, Failure {
     if (!projectControl.canRunUploadPack()) {
@@ -71,6 +75,7 @@
     }
     up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
+    up.setPostUploadHook(uploadMetrics);
 
     List<PreUploadHook> allPreUploadHooks = Lists.newArrayList(preUploadHooks);
     allPreUploadHooks.add(uploadValidatorsFactory.create(project, repo,
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index bf90705..ec79d83 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.lucene.LuceneIndexModule;
+import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
@@ -288,6 +289,7 @@
 
   private Injector createSysInjector() {
     final List<Module> modules = new ArrayList<>();
+    modules.add(new DropWizardMetricMaker.Module());
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
diff --git a/lib/dropwizard/BUCK b/lib/dropwizard/BUCK
new file mode 100644
index 0000000..de73e13
--- /dev/null
+++ b/lib/dropwizard/BUCK
@@ -0,0 +1,8 @@
+include_defs('//lib/maven.defs')
+
+maven_jar(
+  name = 'dropwizard-core',
+  id = 'io.dropwizard.metrics:metrics-core:3.1.2',
+  sha1 = '224f03afd2521c6c94632f566beb1bb5ee32cf07',
+  license = 'Apache2.0',
+)