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("memory",
+ * new Description("Total bytes of memory used")
+ * .setGauge()
+ * .setUnit(Units.BYTES),
+ * new Supplier<Long>() {
+ * 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',
+)