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("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 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',
+)