DropWizard metric support

Gerrit server supports defining and recording metrics.  Metric
reporters for monitoring can be implemented as plugins.  A basic
Graphite reporter is available here:

  https://gerrit-review.googlesource.com/#/c/72202/

Some example metrics are included in this change:

  change/query/query_latency
  (Query latency)

  sshd/sessions/connected
  (SSH sessions connected)

  sshd/sessions/created/count
  (SSH connections created)

  git/upload-pack
  (Upload packs requests)

Partially-by: Gustaf Lundh <gustaflh@axis.com>
Change-Id: I46a07aace57efe236ee724ec8d34c581e2c37965
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..5dfa96c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java
@@ -0,0 +1,43 @@
+// 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.
+ *
+ * <pre>
+ *   CallbackMetric<Long> hits = metricMaker.newCallbackMetric("hits", ...);
+ *   CallbackMetric<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 CallbackMetric<V> implements RegistrationHandle {
+  /**
+   * 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/Counter.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter.java
new file mode 100644
index 0000000..916723f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter.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 Counter 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/Description.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java
new file mode 100644
index 0000000..61729a2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java
@@ -0,0 +1,141 @@
+// 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.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 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() {
+    }
+  }
+
+  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 Counter}.
+   */
+  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 Counter} like total requests handled accumulates over the process
+   * and should be {@code setCumulative()}.
+   */
+  public Description setCumulative() {
+    annotations.put(CUMULATIVE, TRUE_VALUE);
+    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));
+  }
+
+  /**
+   * Decode the unit as a unit of time.
+   *
+   * @return valid time unit.
+   * @throws IllegalStateException if the unit is not a valid unit of time.
+   */
+  public TimeUnit getTimeUnit() {
+    String unit = annotations.get(UNIT);
+    if (unit == null) {
+      throw new IllegalStateException("no unit configured");
+    } else if (Units.NANOSECONDS.equals(unit)) {
+      return TimeUnit.NANOSECONDS;
+    } else if (Units.MICROSECONDS.equals(unit)) {
+      return TimeUnit.MICROSECONDS;
+    } else if (Units.MILLISECONDS.equals(unit)) {
+      return TimeUnit.MILLISECONDS;
+    } else if (Units.SECONDS.equals(unit)) {
+      return TimeUnit.SECONDS;
+    } else {
+      throw new IllegalStateException(String.format(
+          "unit %s not TimeUnit", unit));
+    }
+  }
+
+  /** 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/MetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java
new file mode 100644
index 0000000..2edd1c7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java
@@ -0,0 +1,83 @@
+// 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 Counter newCounter(String name, Description desc);
+
+  /** Metric recording time spent on an operation. */
+  public abstract Timer newTimer(String name, Description desc);
+
+  /**
+   * 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 CallbackMetric<V> metric = newCallbackMetric(name, valueClass, desc);
+    newTrigger(metric, new Runnable() {
+      @Override
+      public void run() {
+        metric.set(trigger.get());
+      }
+    });
+  }
+
+  /** Instantaneous reading of a particular value. */
+  public abstract <V> CallbackMetric<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/Timer.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer.java
new file mode 100644
index 0000000..cde4ea7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer.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 Timer 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/dropwizard/DropWizardMetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
new file mode 100644
index 0000000..4ecec93
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java
@@ -0,0 +1,205 @@
+// 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 com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.metrics.CallbackMetric;
+import com.google.gerrit.metrics.Counter;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Scopes;
+import com.google.inject.Singleton;
+
+import com.codahale.metrics.MetricRegistry;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+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 AbstractModule {
+    @Override
+    protected void configure() {
+      bind(MetricRegistry.class).in(Scopes.SINGLETON);
+      bind(MetricMaker.class).to(DropWizardMetricMaker.class);
+    }
+  }
+
+  private final MetricRegistry registry;
+
+  @Inject
+  DropWizardMetricMaker(MetricRegistry registry) {
+    this.registry = registry;
+  }
+
+  @Override
+  public synchronized Counter newCounter(String name, Description desc) {
+    checkArgument(!desc.isGauge(), "counters must not be gauge");
+    checkNotDefined(name);
+
+    if (desc.isRate()) {
+      final com.codahale.metrics.Meter metric = registry.meter(name);
+      return new CounterImpl(name) {
+        @Override
+        public void incrementBy(long delta) {
+          checkArgument(delta >= 0, "counter delta must be >= 0");
+          metric.mark(delta);
+        }
+      };
+    } else {
+      final com.codahale.metrics.Counter metric = registry.counter(name);
+      return new CounterImpl(name) {
+        @Override
+        public void incrementBy(long delta) {
+          checkArgument(delta >= 0, "counter delta must be >= 0");
+          metric.inc(delta);
+        }
+      };
+    }
+  }
+
+  @Override
+  public synchronized Timer newTimer(final String name, 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");
+    checkNotDefined(name);
+
+    final com.codahale.metrics.Timer metric = registry.timer(name);
+    return new Timer() {
+      @Override
+      public void record(long value, TimeUnit unit) {
+        checkArgument(value >= 0, "timer delta must be >= 0");
+        metric.update(value, unit);
+      }
+
+      @Override
+      public void remove() {
+        registry.remove(name);
+      }
+    };
+  }
+
+  @SuppressWarnings("unused")
+  @Override
+  public <V> CallbackMetric<V> newCallbackMetric(String name,
+      Class<V> valueClass, Description desc) {
+    checkNotDefined(name);
+    return new CallbackMetricImpl<V>(name, valueClass);
+  }
+
+  @Override
+  public synchronized RegistrationHandle newTrigger(
+      Set<CallbackMetric<?>> metrics, Runnable trigger) {
+    for (CallbackMetric<?> m : metrics) {
+      checkNotDefined(((CallbackMetricImpl<?>) m).name);
+    }
+
+    final List<String> names = new ArrayList<>(metrics.size());
+    for (CallbackMetric<?> m : metrics) {
+      CallbackMetricImpl<?> metric = (CallbackMetricImpl<?>) m;
+      registry.register(metric.name, metric.gauge(trigger));
+      names.add(metric.name);
+    }
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        for (String name : names) {
+          registry.remove(name);
+        }
+      }
+    };
+  }
+
+  private void checkNotDefined(String name) {
+    if (registry.getNames().contains(name)) {
+      throw new IllegalStateException(String.format(
+          "metric %s already defined", name));
+    }
+  }
+
+  private abstract class CounterImpl extends Counter {
+    private final String name;
+
+    CounterImpl(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public void remove() {
+      registry.remove(name);
+    }
+  }
+
+  private static class CallbackMetricImpl<V> extends CallbackMetric<V> {
+    private final String name;
+    private V value;
+
+    @SuppressWarnings("unchecked")
+    CallbackMetricImpl(String name, Class<V> valueClass) {
+      this.name = name;
+
+      if (valueClass == Integer.class) {
+        value = (V) Integer.valueOf(0);
+      } else if (valueClass == Long.class) {
+        value = (V) Long.valueOf(0);
+      } else if (valueClass == Double.class) {
+        value = (V) Double.valueOf(0);
+      } else if (valueClass == Float.class) {
+        value = (V) Float.valueOf(0);
+      } else if (valueClass == String.class) {
+        value = (V) "";
+      } else if (valueClass == Boolean.class) {
+        value = (V) Boolean.FALSE;
+      } else {
+        throw new IllegalArgumentException("unsupported value type "
+            + valueClass.getName());
+      }
+    }
+
+    @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;
+        }
+      };
+    }
+  }
+}
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..2f87d38
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java
@@ -0,0 +1,43 @@
+// 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.Counter;
+import com.google.gerrit.metrics.Description;
+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 {
+  private final Counter upload;
+
+  @Inject
+  UploadPackMetricsHook(MetricMaker metricMaker) {
+    upload = metricMaker.newCounter(
+        "git/upload-pack",
+        new Description("Total number of git-upload-pack requests")
+          .setRate()
+          .setUnit("requests"));
+  }
+
+  @Override
+  public void onPostUpload(PackStatistics stats) {
+    upload.increment();
+  }
+}
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..7881433
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginMetricMaker.java
@@ -0,0 +1,91 @@
+// 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.Counter;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer;
+
+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 Counter newCounter(String name, Description desc) {
+    Counter m = root.newCounter(prefix + name, desc);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public Timer newTimer(String name, Description desc) {
+    Timer m = root.newTimer(prefix + name, desc);
+    cleanup.add(m);
+    return m;
+  }
+
+  @Override
+  public <V> CallbackMetric<V> newCallbackMetric(String name,
+      Class<V> valueClass, Description desc) {
+    CallbackMetric<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..b0a376c 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.Timer;
 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")
+    Timer.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 Timer 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..37d2cb7 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.Counter;
+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 Counter 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',
+)