From f70a242ad59510ba70ee09ea8221235d66975fe1 Mon Sep 17 00:00:00 2001 From: Shawn Pearce Date: Sun, 8 Nov 2015 11:44:03 -0800 Subject: [PATCH] DropWizard metric support Gerrit server supports defining and recording metrics. Metric reporters for monitoring can be implemented as plugins. A basic Graphite reporter is available here: https://gerrit-review.googlesource.com/#/c/72202/ Some example metrics are included in this change: change/query/query_latency (Query latency) sshd/sessions/connected (SSH sessions connected) sshd/sessions/created/count (SSH connections created) git/upload-pack (Upload packs requests) Partially-by: Gustaf Lundh Change-Id: I46a07aace57efe236ee724ec8d34c581e2c37965 --- .../gerrit/httpd/GitOverHttpServlet.java | 8 +- .../java/com/google/gerrit/pgm/Daemon.java | 2 + gerrit-server/BUCK | 1 + .../google/gerrit/metrics/CallbackMetric.java | 43 ++++ .../com/google/gerrit/metrics/Counter.java | 41 ++++ .../google/gerrit/metrics/Description.java | 141 ++++++++++++ .../google/gerrit/metrics/MetricMaker.java | 83 +++++++ .../java/com/google/gerrit/metrics/Timer.java | 54 +++++ .../dropwizard/DropWizardMetricMaker.java | 205 ++++++++++++++++++ .../server/git/UploadPackMetricsHook.java | 43 ++++ .../plugins/PluginGuiceEnvironment.java | 14 +- .../server/plugins/PluginMetricMaker.java | 91 ++++++++ .../gerrit/server/plugins/ServerPlugin.java | 2 +- .../plugins/ServerPluginInfoModule.java | 17 +- .../server/query/change/QueryProcessor.java | 28 ++- .../gerrit/testutil/InMemoryModule.java | 2 + gerrit-sshd/BUCK | 1 + .../com/google/gerrit/sshd/SshDaemon.java | 32 ++- .../google/gerrit/sshd/commands/Upload.java | 5 + .../gerrit/httpd/WebAppInitializer.java | 2 + lib/dropwizard/BUCK | 8 + 21 files changed, 817 insertions(+), 6 deletions(-) create mode 100644 gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/metrics/Counter.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/metrics/Timer.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginMetricMaker.java create mode 100644 lib/dropwizard/BUCK 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 1bda39f508..13b1a48e0e 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.GitRepositoryManager; 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 @@ public class GitOverHttpServlet extends GitServlet { static class UploadFactory implements UploadPackFactory { private final TransferConfig config; + private final UploadPackMetricsHook uploadMetrics; private final DynamicSet preUploadHooks; @Inject - UploadFactory(TransferConfig tc, DynamicSet preUploadHooks) { + UploadFactory(TransferConfig tc, + UploadPackMetricsHook uploadMetrics, + DynamicSet preUploadHooks) { this.config = tc; + this.uploadMetrics = uploadMetrics; this.preUploadHooks = preUploadHooks; } @@ -211,6 +216,7 @@ public class GitOverHttpServlet extends GitServlet { 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 5e9c5aa73c..6eee544951 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.auth.openid.OpenIdModule; 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 @@ public class Daemon extends SiteProgram { private Injector createSysInjector() { final List 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 4671f8236f..107b3f1635 100644 --- a/gerrit-server/BUCK +++ b/gerrit-server/BUCK @@ -53,6 +53,7 @@ java_library( '//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 0000000000..5dfa96cf40 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java @@ -0,0 +1,43 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.metrics; + +import com.google.gerrit.extensions.registration.RegistrationHandle; + + +/** + * Metric whose value is supplied when the trigger is invoked. + * + *
+ *   CallbackMetric hits = metricMaker.newCallbackMetric("hits", ...);
+ *   CallbackMetric total = metricMaker.newCallbackMetric("total", ...);
+ *   metricMaker.newTrigger(hits, total, new Runnable() {
+ *     public void run() {
+ *       hits.set(1);
+ *       total.set(5);
+ *     }
+ *   });
+ * 
+ * + * @param type of the metric value, typically Integer or Long. + */ +public abstract class CallbackMetric implements RegistrationHandle { + /** + * Supply the current value of the metric. + * + * @param value current value. + */ + public abstract void set(V value); +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter.java new file mode 100644 index 0000000000..916723fa5e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter.java @@ -0,0 +1,41 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.metrics; + +import com.google.gerrit.extensions.registration.RegistrationHandle; + +/** + * Metric whose value increments during the life of the process. + *

+ * 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. + *

+ * For an instantaneous read of a value that can change over time + * (e.g. "memory in use") use a {@link CallbackMetric}. + */ +public abstract class Counter implements RegistrationHandle { + /** Increment the counter by one event. */ + public void increment() { + incrementBy(1); + } + + /** + * Increment the counter by a specified amount. + * + * @param value value to increment by, must be >= 0. + */ + public abstract void incrementBy(long value); +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java new file mode 100644 index 0000000000..61729a23b8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java @@ -0,0 +1,141 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.metrics; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** Describes a metric created by {@link MetricMaker}. */ +public class Description { + public static final String DESCRIPTION = "DESCRIPTION"; + public static final String UNIT = "UNIT"; + public static final String CUMULATIVE = "CUMULATIVE"; + public static final String RATE = "RATE"; + public static final String GAUGE = "GAUGE"; + public static final String TRUE_VALUE = "1"; + + public static class Units { + public static final String SECONDS = "seconds"; + public static final String MILLISECONDS = "milliseconds"; + public static final String MICROSECONDS = "microseconds"; + public static final String NANOSECONDS = "nanoseconds"; + + public static final String BYTES = "bytes"; + + private Units() { + } + } + + private final Map annotations; + + /** + * Describe a metric. + * + * @param helpText a short one-sentence string explaining the values captured + * by the metric. This may be made available to administrators as + * documentation in the reporting tools. + */ + public Description(String helpText) { + annotations = Maps.newLinkedHashMapWithExpectedSize(4); + annotations.put(DESCRIPTION, helpText); + } + + /** Unit used to describe the value, e.g. "requests", "seconds", etc. */ + public Description setUnit(String unitName) { + annotations.put(UNIT, unitName); + return this; + } + + /** + * Indicates the metric may be usefully interpreted as a count over short + * periods of time, such as request arrival rate. May only be applied to a + * {@link Counter}. + */ + public Description setRate() { + annotations.put(RATE, TRUE_VALUE); + return this; + } + + /** + * Instantaneously sampled value that may increase or decrease at a later + * time. Memory allocated or open network connections are examples of gauges. + */ + public Description setGauge() { + annotations.put(GAUGE, TRUE_VALUE); + return this; + } + + /** + * Indicates the metric accumulates over the lifespan of the process. A + * {@link Counter} like total requests handled accumulates over the process + * and should be {@code setCumulative()}. + */ + public Description setCumulative() { + annotations.put(CUMULATIVE, TRUE_VALUE); + return this; + } + + /** True if the metric may be interpreted as a rate over time. */ + public boolean isRate() { + return TRUE_VALUE.equals(annotations.get(RATE)); + } + + /** True if the metric is an instantaneous sample. */ + public boolean isGauge() { + return TRUE_VALUE.equals(annotations.get(GAUGE)); + } + + /** True if the metric accumulates over the lifespan of the process. */ + public boolean isCumulative() { + return TRUE_VALUE.equals(annotations.get(CUMULATIVE)); + } + + /** + * Decode the unit as a unit of time. + * + * @return valid time unit. + * @throws IllegalStateException if the unit is not a valid unit of time. + */ + public TimeUnit getTimeUnit() { + String unit = annotations.get(UNIT); + if (unit == null) { + throw new IllegalStateException("no unit configured"); + } else if (Units.NANOSECONDS.equals(unit)) { + return TimeUnit.NANOSECONDS; + } else if (Units.MICROSECONDS.equals(unit)) { + return TimeUnit.MICROSECONDS; + } else if (Units.MILLISECONDS.equals(unit)) { + return TimeUnit.MILLISECONDS; + } else if (Units.SECONDS.equals(unit)) { + return TimeUnit.SECONDS; + } else { + throw new IllegalStateException(String.format( + "unit %s not TimeUnit", unit)); + } + } + + /** Immutable copy of all annotations (configurable properties). */ + public ImmutableMap getAnnotations() { + return ImmutableMap.copyOf(annotations); + } + + @Override + public String toString() { + return annotations.toString(); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java new file mode 100644 index 0000000000..2edd1c7a03 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java @@ -0,0 +1,83 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.metrics; + +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.extensions.registration.RegistrationHandle; + +import java.util.Set; + +/** Factory to create metrics for monitoring. */ +public abstract class MetricMaker { + /** Metric whose value increments during the life of the process. */ + public abstract Counter newCounter(String name, Description desc); + + /** Metric recording time spent on an operation. */ + public abstract Timer newTimer(String name, Description desc); + + /** + * Instantaneous reading of a value. + * + *

+   * metricMaker.newCallbackMetric("memory",
+   *     new Description("Total bytes of memory used")
+   *        .setGauge()
+   *        .setUnit(Units.BYTES),
+   *     new Supplier<Long>() {
+   *       public Long get() {
+   *         return Runtime.getRuntime().totalMemory();
+   *       }
+   *     });
+   * 
+ * + * @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 void newCallbackMetric(String name, + Class valueClass, Description desc, final Supplier trigger) { + final CallbackMetric metric = newCallbackMetric(name, valueClass, desc); + newTrigger(metric, new Runnable() { + @Override + public void run() { + metric.set(trigger.get()); + } + }); + } + + /** Instantaneous reading of a particular value. */ + public abstract CallbackMetric newCallbackMetric(String name, + Class valueClass, Description desc); + + /** Connect logic to populate a previously created {@link CallbackMetric}. */ + public RegistrationHandle newTrigger(CallbackMetric metric1, Runnable trigger) { + return newTrigger(ImmutableSet.>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> metrics, + Runnable trigger); +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer.java new file mode 100644 index 0000000000..cde4ea7ead --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer.java @@ -0,0 +1,54 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.metrics; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import com.google.gerrit.extensions.registration.RegistrationHandle; + +import java.util.concurrent.TimeUnit; + +/** + * Records elapsed time for an operation or span. + *

+ * Typical usage in a try-with-resources block: + * + *

+ * try (Timer.Context ctx = timer.start()) {
+ * }
+ * 
+ */ +public abstract class Timer implements RegistrationHandle { + public class Context implements AutoCloseable { + private final long startNanos; + + Context() { + this.startNanos = System.nanoTime(); + } + + @Override + public void close() { + record(System.nanoTime() - startNanos, NANOSECONDS); + } + } + + /** Begin a timer for the current block, value will be recorded when closed. */ + public Context start() { + return new Context(); + } + + /** Record a value in the distribution. */ + public abstract void record(long value, TimeUnit unit); +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java new file mode 100644 index 0000000000..4ecec93f98 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/DropWizardMetricMaker.java @@ -0,0 +1,205 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.metrics.dropwizard; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.gerrit.extensions.registration.RegistrationHandle; +import com.google.gerrit.metrics.CallbackMetric; +import com.google.gerrit.metrics.Counter; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.MetricMaker; +import com.google.gerrit.metrics.Timer; +import com.google.inject.AbstractModule; +import com.google.inject.Inject; +import com.google.inject.Scopes; +import com.google.inject.Singleton; + +import com.codahale.metrics.MetricRegistry; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Connects Gerrit metric package onto DropWizard. + * + * @see DropWizard + */ +@Singleton +public class DropWizardMetricMaker extends MetricMaker { + public static class Module extends AbstractModule { + @Override + protected void configure() { + bind(MetricRegistry.class).in(Scopes.SINGLETON); + bind(MetricMaker.class).to(DropWizardMetricMaker.class); + } + } + + private final MetricRegistry registry; + + @Inject + DropWizardMetricMaker(MetricRegistry registry) { + this.registry = registry; + } + + @Override + public synchronized Counter newCounter(String name, Description desc) { + checkArgument(!desc.isGauge(), "counters must not be gauge"); + checkNotDefined(name); + + if (desc.isRate()) { + final com.codahale.metrics.Meter metric = registry.meter(name); + return new CounterImpl(name) { + @Override + public void incrementBy(long delta) { + checkArgument(delta >= 0, "counter delta must be >= 0"); + metric.mark(delta); + } + }; + } else { + final com.codahale.metrics.Counter metric = registry.counter(name); + return new CounterImpl(name) { + @Override + public void incrementBy(long delta) { + checkArgument(delta >= 0, "counter delta must be >= 0"); + metric.inc(delta); + } + }; + } + } + + @Override + public synchronized Timer newTimer(final String name, Description desc) { + checkArgument(!desc.isGauge(), "timer must not be a gauge"); + checkArgument(!desc.isRate(), "timer must not be a rate"); + checkArgument(desc.isCumulative(), "timer must be cumulative"); + checkArgument(desc.getTimeUnit() != null, "timer must have a unit"); + checkNotDefined(name); + + final com.codahale.metrics.Timer metric = registry.timer(name); + return new Timer() { + @Override + public void record(long value, TimeUnit unit) { + checkArgument(value >= 0, "timer delta must be >= 0"); + metric.update(value, unit); + } + + @Override + public void remove() { + registry.remove(name); + } + }; + } + + @SuppressWarnings("unused") + @Override + public CallbackMetric newCallbackMetric(String name, + Class valueClass, Description desc) { + checkNotDefined(name); + return new CallbackMetricImpl(name, valueClass); + } + + @Override + public synchronized RegistrationHandle newTrigger( + Set> metrics, Runnable trigger) { + for (CallbackMetric m : metrics) { + checkNotDefined(((CallbackMetricImpl) m).name); + } + + final List names = new ArrayList<>(metrics.size()); + for (CallbackMetric m : metrics) { + CallbackMetricImpl metric = (CallbackMetricImpl) m; + registry.register(metric.name, metric.gauge(trigger)); + names.add(metric.name); + } + return new RegistrationHandle() { + @Override + public void remove() { + for (String name : names) { + registry.remove(name); + } + } + }; + } + + private void checkNotDefined(String name) { + if (registry.getNames().contains(name)) { + throw new IllegalStateException(String.format( + "metric %s already defined", name)); + } + } + + private abstract class CounterImpl extends Counter { + private final String name; + + CounterImpl(String name) { + this.name = name; + } + + @Override + public void remove() { + registry.remove(name); + } + } + + private static class CallbackMetricImpl extends CallbackMetric { + private final String name; + private V value; + + @SuppressWarnings("unchecked") + CallbackMetricImpl(String name, Class valueClass) { + this.name = name; + + if (valueClass == Integer.class) { + value = (V) Integer.valueOf(0); + } else if (valueClass == Long.class) { + value = (V) Long.valueOf(0); + } else if (valueClass == Double.class) { + value = (V) Double.valueOf(0); + } else if (valueClass == Float.class) { + value = (V) Float.valueOf(0); + } else if (valueClass == String.class) { + value = (V) ""; + } else if (valueClass == Boolean.class) { + value = (V) Boolean.FALSE; + } else { + throw new IllegalArgumentException("unsupported value type " + + valueClass.getName()); + } + } + + @Override + public void set(V value) { + this.value = value; + } + + @Override + public void remove() { + // Triggers register and remove the metric. + } + + com.codahale.metrics.Gauge gauge(final Runnable trigger) { + return new com.codahale.metrics.Gauge() { + @Override + public V getValue() { + trigger.run(); + return value; + } + }; + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java new file mode 100644 index 0000000000..2f87d38318 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UploadPackMetricsHook.java @@ -0,0 +1,43 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.git; + +import com.google.gerrit.metrics.Counter; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.MetricMaker; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.jgit.storage.pack.PackStatistics; +import org.eclipse.jgit.transport.PostUploadHook; + +@Singleton +public class UploadPackMetricsHook implements PostUploadHook { + private final Counter upload; + + @Inject + UploadPackMetricsHook(MetricMaker metricMaker) { + upload = metricMaker.newCounter( + "git/upload-pack", + new Description("Total number of git-upload-pack requests") + .setRate() + .setUnit("requests")); + } + + @Override + public void onPostUpload(PackStatistics stats) { + upload.increment(); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java index 6b458aa330..5df364fb0d 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.PrivateInternals_DynamicTypes; 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 @@ public class PluginGuiceEnvironment { private final List onStart; private final List onStop; private final List onReload; + private final MetricMaker serverMetrics; private Module sysModule; private Module sshModule; @@ -102,12 +104,14 @@ public class PluginGuiceEnvironment { 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 @@ public class PluginGuiceEnvironment { return srvInfo; } + MetricMaker getServerMetrics() { + return serverMetrics; + } + boolean hasDynamicItem(TypeLiteral type) { return sysItems.containsKey(type) || (sshItems != null && sshItems.containsKey(type)) @@ -424,6 +432,7 @@ public class PluginGuiceEnvironment { } } } + private void reattachItem( ListMultimap, ReloadableRegistrationHandle> oldHandles, Map, DynamicItem> items, @@ -564,6 +573,9 @@ public class PluginGuiceEnvironment { 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 0000000000..7881433f6c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginMetricMaker.java @@ -0,0 +1,91 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.plugins; + +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.registration.RegistrationHandle; +import com.google.gerrit.metrics.CallbackMetric; +import com.google.gerrit.metrics.Counter; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.MetricMaker; +import com.google.gerrit.metrics.Timer; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +class PluginMetricMaker extends MetricMaker implements LifecycleListener { + private final MetricMaker root; + private final String prefix; + private final Set cleanup; + + PluginMetricMaker(MetricMaker root, String pluginName) { + this.root = root; + this.prefix = "plugins/" + pluginName; + cleanup = Collections.synchronizedSet(new HashSet()); + } + + @Override + public Counter newCounter(String name, Description desc) { + Counter m = root.newCounter(prefix + name, desc); + cleanup.add(m); + return m; + } + + @Override + public Timer newTimer(String name, Description desc) { + Timer m = root.newTimer(prefix + name, desc); + cleanup.add(m); + return m; + } + + @Override + public CallbackMetric newCallbackMetric(String name, + Class valueClass, Description desc) { + CallbackMetric m = root.newCallbackMetric(prefix + name, valueClass, desc); + cleanup.add(m); + return m; + } + + @Override + public RegistrationHandle newTrigger(Set> 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 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 14c11854f0..ea96a56a10 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 @@ public class ServerPlugin extends Plugin { 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 b0e9453c3a..a7f0087a1f 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 @@ package com.google.gerrit.server.plugins; 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 @@ class ServerPluginInfoModule extends AbstractModule { 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 @@ class ServerPluginInfoModule extends AbstractModule { 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 a2c8b81981..b0a376cae6 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 static com.google.gerrit.server.query.change.ChangeStatusPredicate.open; import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.MetricMaker; +import com.google.gerrit.metrics.Timer; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.index.IndexConfig; @@ -32,6 +35,7 @@ import com.google.gwtorm.server.OrmException; 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 @@ public class QueryProcessor { 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 @@ public class QueryProcessor { Provider 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 @@ public class QueryProcessor { private List queryChanges(List queryStrings, List> queries) throws OrmException, QueryParseException { + @SuppressWarnings("resource") + Timer.Context context = metrics.executionTime.start(); + Predicate visibleToMe = enforceVisibility ? new IsVisibleToPredicate(db, changeControlFactory, userProvider.get()) : null; @@ -170,6 +180,7 @@ public class QueryProcessor { limits.get(i), matches.get(i).toList())); } + context.close(); // only measure successful queries return out; } @@ -203,4 +214,19 @@ public class QueryProcessor { } return Ordering.natural().min(possibleLimits); } + + @Singleton + static class Metrics { + final Timer executionTime; + + @Inject + Metrics(MetricMaker metricMaker) { + executionTime = metricMaker.newTimer( + "change/query/query_latency", + new Description("Successful change query latency," + + " accumulated over the life of the process") + .setCumulative() + .setUnit(Description.Units.MILLISECONDS)); + } + } } diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java index e5cd619145..7d22c73486 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.ChangeHooks; 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 @@ public class InMemoryModule extends FactoryModule { .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 dcff98e6b2..279b024679 100644 --- a/gerrit-sshd/BUCK +++ b/gerrit-sshd/BUCK @@ -21,6 +21,7 @@ java_library( '//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 3afb208937..37d2cb702c 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.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import com.google.common.base.Strings; +import com.google.common.base.Supplier; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.gerrit.common.Version; import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.metrics.Counter; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.MetricMaker; import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.ssh.SshAdvertisedAddresses; @@ -126,6 +130,7 @@ import java.util.Collections; 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 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener { final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator, @GerritServerConfig final Config cfg, final SshLog sshLog, @SshListenAddresses final List listen, - @SshAdvertisedAddresses final List advertised) { + @SshAdvertisedAddresses final List advertised, + MetricMaker metricMaker) { setPort(IANA_SSH_PORT /* never used */); this.cfg = cfg; @@ -245,10 +251,33 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener { 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() { + @Override + public Integer get() { + return connected.get(); + } + }); + + final Counter sesssionsCreated = metricMaker.newCounter( + "sshd/sessions/created", + new Description("Rate of new SSH sessions") + .setRate() + .setUnit("sessions")); + setSessionFactory(new SessionFactory() { @Override protected AbstractSession createSession(final IoSession io) throws Exception { + connected.incrementAndGet(); + sesssionsCreated.increment(); if (io instanceof MinaSession) { if (((MinaSession) io).getSession() .getConfig() instanceof SocketSessionConfig) { @@ -269,6 +298,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener { s.addCloseSessionListener(new SshFutureListener() { @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 9873c04014..d278f4b467 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.reviewdb.server.ReviewDb; 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 @@ final class Upload extends AbstractGitCommand { @Inject private SshSession session; + @Inject + private UploadPackMetricsHook uploadMetrics; + @Override protected void runImpl() throws IOException, Failure { if (!projectControl.canRunUploadPack()) { @@ -71,6 +75,7 @@ final class Upload extends AbstractGitCommand { } up.setPackConfig(config.getPackConfig()); up.setTimeout(config.getTimeout()); + up.setPostUploadHook(uploadMetrics); List 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 bf90705e55..ec79d83763 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.httpd.plugins.HttpPluginModule; 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 @@ public class WebAppInitializer extends GuiceServletContextListener private Injector createSysInjector() { final List 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 0000000000..de73e13e9a --- /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', +)