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 index 5dfa96cf40..21e869bba9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/CallbackMetric.java @@ -20,24 +20,8 @@ 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);
- *     }
- *   });
- * 
- * + * @see CallbackMetric0 * @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); +public interface CallbackMetric 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 0000000000..043e25fdab --- /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. + * + *
+ *   CallbackMetric0 hits = metricMaker.newCallbackMetric("hits", ...);
+ *   CallbackMetric0 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 CallbackMetric0 implements CallbackMetric { + /** + * 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/Counter0.java similarity index 95% rename from gerrit-server/src/main/java/com/google/gerrit/metrics/Counter.java rename to gerrit-server/src/main/java/com/google/gerrit/metrics/Counter0.java index 916723fa5e..c1d213fce3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Counter0.java @@ -26,7 +26,7 @@ import com.google.gerrit.extensions.registration.RegistrationHandle; * 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 { +public abstract class Counter0 implements RegistrationHandle { /** Increment the counter by one event. */ public void increment() { incrementBy(1); 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 0000000000..3477280a08 --- /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. + *

+ * 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}. + * + * @param type of the field. + */ +public abstract class Counter1 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 0000000000..4bef791429 --- /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. + *

+ * 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}. + * + * @param type of the field. + * @param type of the field. + */ +public abstract class Counter2 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 0000000000..391e7e0291 --- /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. + *

+ * 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}. + * + * @param type of the field. + * @param type of the field. + * @param type of the field. + */ +public abstract class Counter3 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 index d344edefa0..fe244ed4fc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Description.java @@ -28,6 +28,7 @@ public class Description { 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 { @@ -42,6 +43,19 @@ public class Description { } } + 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 annotations; /** @@ -65,7 +79,7 @@ public class Description { /** * 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}. + * {@link Counter0}. */ public Description setRate() { annotations.put(RATE, TRUE_VALUE); @@ -83,7 +97,7 @@ public class Description { /** * Indicates the metric accumulates over the lifespan of the process. A - * {@link Counter} like total requests handled accumulates over the process + * {@link Counter0} like total requests handled accumulates over the process * and should be {@code setCumulative()}. */ public Description setCumulative() { @@ -91,6 +105,12 @@ public class Description { 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)); @@ -106,6 +126,12 @@ public class Description { 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. * 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 0000000000..a91e428070 --- /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 { + /** Break down metrics by boolean true/false. */ + public static Field ofBoolean(String name) { + return ofBoolean(name, null); + } + + /** Break down metrics by boolean true/false. */ + public static Field ofBoolean(String name, String description) { + return new Field<>(name, Boolean.class, description); + } + + /** Break down metrics by cases of an enum. */ + public static > Field ofEnum(Class enumType, + String name) { + return ofEnum(enumType, name, null); + } + + /** Break down metrics by cases of an enum. */ + public static > Field ofEnum(Class enumType, + String name, String description) { + return new Field<>(name, enumType, description); + } + + /** + * Break down metrics by string. + *

+ * Each unique string will allocate a new submetric. Do not use user + * content as a field value as field values are never reclaimed. + */ + public static Field ofString(String name) { + return ofString(name, null); + } + + /** + * Break down metrics by string. + *

+ * Each unique string will allocate a new submetric. Do not use user + * content as a field value as field values are never reclaimed. + */ + public static Field ofString(String name, String description) { + return new Field<>(name, String.class, description); + } + + /** + * Break down metrics by integer. + *

+ * Each unique integer will allocate a new submetric. Do not use user + * content as a field value as field values are never reclaimed. + */ + public static Field ofInteger(String name) { + return ofInteger(name, null); + } + + /** + * Break down metrics by integer. + *

+ * Each unique integer will allocate a new submetric. Do not use user + * content as a field value as field values are never reclaimed. + */ + public static Field ofInteger(String name, String description) { + return new Field<>(name, Integer.class, description); + } + + private final String name; + private final Class keyType; + private final Function formatter; + private final String description; + + private Field(String name, Class 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 getType() { + return keyType; + } + + /** Description text for the field explaining its range of values. */ + public String getDescription() { + return description; + } + + public Function formatter() { + return formatter; + } + + @SuppressWarnings("unchecked") + private static Function initFormatter(Class keyType) { + if (keyType == String.class) { + return (Function) Functions. identity(); + + } else if (keyType == Integer.class || keyType == Boolean.class) { + return (Function) Functions.toStringFunction(); + + } else if (Enum.class.isAssignableFrom(keyType)) { + return new Function() { + @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 index 2edd1c7a03..55844b165c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java @@ -23,10 +23,28 @@ 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); + public abstract Counter0 newCounter(String name, Description desc); + public abstract Counter1 newCounter( + String name, Description desc, + Field field1); + public abstract Counter2 newCounter( + String name, Description desc, + Field field1, Field field2); + public abstract Counter3 newCounter( + String name, Description desc, + Field field1, Field field2, Field field3); /** Metric recording time spent on an operation. */ - public abstract Timer newTimer(String name, Description desc); + public abstract Timer0 newTimer(String name, Description desc); + public abstract Timer1 newTimer( + String name, Description desc, + Field field1); + public abstract Timer2 newTimer( + String name, Description desc, + Field field1, Field field2); + public abstract Timer3 newTimer( + String name, Description desc, + Field field1, Field field2, Field field3); /** * Instantaneous reading of a value. @@ -50,7 +68,7 @@ public abstract class MetricMaker { */ public void newCallbackMetric(String name, Class valueClass, Description desc, final Supplier trigger) { - final CallbackMetric metric = newCallbackMetric(name, valueClass, desc); + final CallbackMetric0 metric = newCallbackMetric(name, valueClass, desc); newTrigger(metric, new Runnable() { @Override public void run() { @@ -59,9 +77,9 @@ public abstract class MetricMaker { }); } - /** Instantaneous reading of a particular value. */ - public abstract CallbackMetric newCallbackMetric(String name, - Class valueClass, Description desc); + /** Instantaneous reading of a single value. */ + public abstract CallbackMetric0 newCallbackMetric( + String name, Class valueClass, Description desc); /** Connect logic to populate a previously created {@link CallbackMetric}. */ public RegistrationHandle newTrigger(CallbackMetric metric1, 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/Timer0.java similarity index 96% rename from gerrit-server/src/main/java/com/google/gerrit/metrics/Timer.java rename to gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java index cde4ea7ead..fc8cef3e76 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/Timer0.java @@ -30,7 +30,7 @@ import java.util.concurrent.TimeUnit; * } * */ -public abstract class Timer implements RegistrationHandle { +public abstract class Timer0 implements RegistrationHandle { public class Context implements AutoCloseable { private final long startNanos; 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 0000000000..88576f26c3 --- /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. + *

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

+ * try (Timer1.Context ctx = timer.start(field)) {
+ * }
+ * 
+ * + * @param type of the field. + */ +public abstract class Timer1 implements RegistrationHandle { + public static class Context implements AutoCloseable { + private final Timer1 timer; + private final Object field1; + private final long startNanos; + + @SuppressWarnings("unchecked") + Context(Timer1 timer, F1 field1) { + this.timer = (Timer1) 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 0000000000..f4ffebd801 --- /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. + *

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

+ * try (Timer2.Context ctx = timer.start(field)) {
+ * }
+ * 
+ * + * @param type of the field. + * @param type of the field. + */ +public abstract class Timer2 implements RegistrationHandle { + public static class Context implements AutoCloseable { + private final Timer2 timer; + private final Object field1; + private final Object field2; + private final long startNanos; + + @SuppressWarnings("unchecked") + Context(Timer2 timer, F1 field1, F2 field2) { + this.timer = (Timer2) 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 0000000000..60f0d5a884 --- /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. + *

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

+ * try (Timer3.Context ctx = timer.start(field)) {
+ * }
+ * 
+ * + * @param type of the field. + * @param type of the field. + * @param type of the field. + */ +public abstract class Timer3 implements RegistrationHandle { + public static class Context implements AutoCloseable { + private final Timer3 timer; + private final Object field1; + private final Object field2; + private final Object field3; + private final long startNanos; + + @SuppressWarnings("unchecked") + Context(Timer3 timer, F1 f1, F2 f2, F3 f3) { + this.timer = (Timer3) 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 0000000000..22af5cac02 --- /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 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 getCells() { + return Maps.transformValues( + cells, + new Function () { + @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 0000000000..799e59449b --- /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 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 0000000000..ec12e00e64 --- /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 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 getCells() { + return Maps.transformValues( + cells, + new Function () { + @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 0000000000..94bbf7f633 --- /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 0000000000..7ad2970b99 --- /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 extends CallbackMetric0 { + @SuppressWarnings("unchecked") + static V zeroFor(Class 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 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 gauge(final Runnable trigger) { + return new com.codahale.metrics.Gauge() { + @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 0000000000..25647ef1e2 --- /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 extends BucketedCounter { + CounterImpl1(DropWizardMetricMaker metrics, String name, Description desc, + Field field1) { + super(metrics, name, desc, field1); + } + + Counter1 counter() { + return new Counter1() { + @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 fmt = + (Function) 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 0000000000..a2f1f8447d --- /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); + } + + Counter2 counter2() { + return new Counter2() { + @Override + public void incrementBy(F1 field1, F2 field2, long value) { + total.incrementBy(value); + forceCreate(field1, field2).incrementBy(value); + } + + @Override + public void remove() { + doRemove(); + } + }; + } + + Counter3 counter3() { + return new Counter3() { + @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 keyList = (ImmutableList) key; + String[] parts = new String[fields.length]; + for (int i = 0; i < fields.length; i++) { + Function fmt = + (Function) 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 index fec923808c..336bf9eb06 100644 --- 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 @@ -23,10 +23,19 @@ 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.Counter; +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.Timer; +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; @@ -35,10 +44,10 @@ import com.codahale.metrics.Metric; import com.codahale.metrics.MetricRegistry; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** @@ -62,102 +71,174 @@ public class DropWizardMetricMaker extends MetricMaker { } private final MetricRegistry registry; + private final Map bucketed; private final Map> descriptions; @Inject DropWizardMetricMaker(MetricRegistry registry) { this.registry = registry; - this.descriptions = new HashMap<>(); + this.bucketed = new ConcurrentHashMap<>(); + this.descriptions = new ConcurrentHashMap<>(); } - Map getMetricMap() { - return registry.getMetrics(); + Iterable getMetricNames() { + return descriptions.keySet(); } /** Get the underlying metric implementation. */ public Metric getMetric(String name) { - return registry.getMetrics().get(name); + Metric m = bucketed.get(name); + return m != null ? m : registry.getMetrics().get(name); } - /** Lookup annotations from a metric's {@link Description}. */ + /** Lookup annotations from a metric's {@link Description}. */ public ImmutableMap getAnnotations(String name) { return descriptions.get(name); } @Override - public synchronized Counter newCounter(String name, Description desc) { - checkArgument(!desc.isGauge(), "counters must not be gauge"); - checkNotDefined(name); - descriptions.put(name, desc.getAnnotations()); + public synchronized Counter0 newCounter(String name, Description desc) { + checkCounterDescription(desc); + define(name, desc); + return newCounterImpl(name, desc.isRate()); + } - if (desc.isRate()) { - final com.codahale.metrics.Meter metric = registry.meter(name); - return new CounterImpl(name) { + @Override + public synchronized Counter1 newCounter( + String name, Description desc, + Field field1) { + checkCounterDescription(desc); + CounterImpl1 m = new CounterImpl1<>(this, name, desc, field1); + define(name, desc); + bucketed.put(name, m); + return m.counter(); + } + + @Override + public synchronized Counter2 newCounter( + String name, Description desc, + Field field1, Field 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 Counter3 newCounter( + String name, Description desc, + Field field1, Field field2, Field 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"); - metric.mark(delta); + m.mark(delta); } }; } else { - final com.codahale.metrics.Counter metric = registry.counter(name); - return new CounterImpl(name) { + 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"); - metric.inc(delta); + m.inc(delta); } }; } } @Override - public synchronized Timer newTimer(final String name, Description desc) { + public synchronized Timer0 newTimer(String name, Description desc) { + checkTimerDescription(desc); + define(name, desc); + return newTimerImpl(name); + } + + @Override + public synchronized Timer1 newTimer(String name, Description desc, Field field1) { + checkTimerDescription(desc); + TimerImpl1 m = new TimerImpl1<>(this, name, desc, field1); + define(name, desc); + bucketed.put(name, m); + return m.timer(); + } + + @Override + public synchronized Timer2 newTimer(String name, Description desc, + Field field1, Field 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 Timer3 newTimer( + String name, Description desc, + Field field1, Field field2, Field 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"); - checkNotDefined(name); - descriptions.put(name, desc.getAnnotations()); - - 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() { - descriptions.remove(name); - registry.remove(name); - } - }; } - @SuppressWarnings("unused") + TimerImpl newTimerImpl(String name) { + return new TimerImpl(name, registry.timer(name)); + } + @Override - public CallbackMetric newCallbackMetric(String name, - Class valueClass, Description desc) { - checkNotDefined(name); - descriptions.put(name, desc.getAnnotations()); - return new CallbackMetricImpl(name, valueClass); + public CallbackMetric0 newCallbackMetric( + String name, Class valueClass, Description desc) { + define(name, desc); + return new CallbackMetricImpl0<>(name, valueClass); } @Override public synchronized RegistrationHandle newTrigger( Set> metrics, Runnable trigger) { + if (metrics.size() > 1) { + trigger = new CallbackGroup(trigger); + } + for (CallbackMetric m : metrics) { - checkNotDefined(((CallbackMetricImpl) m).name); + CallbackMetricImpl0 metric = (CallbackMetricImpl0) m; + if (registry.getMetrics().containsKey(metric.name)) { + throw new IllegalStateException(String.format( + "metric %s already configured", metric.name)); + } } final List names = new ArrayList<>(metrics.size()); for (CallbackMetric m : metrics) { - CallbackMetricImpl metric = (CallbackMetricImpl) m; + CallbackMetricImpl0 metric = (CallbackMetricImpl0) m; registry.register(metric.name, metric.gauge(trigger)); names.add(metric.name); } + return new RegistrationHandle() { @Override public void remove() { @@ -169,18 +250,40 @@ public class DropWizardMetricMaker extends MetricMaker { }; } - private void checkNotDefined(String name) { - if (registry.getNames().contains(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()); } - private abstract class CounterImpl extends Counter { - private final String name; + 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; + } - CounterImpl(String name) { + abstract class CounterImpl extends Counter0 { + private final String name; + final Metric metric; + + CounterImpl(String name, Metric metric) { this.name = name; + this.metric = metric; } @Override @@ -190,50 +293,25 @@ public class DropWizardMetricMaker extends MetricMaker { } } - private static class CallbackMetricImpl extends CallbackMetric { + class TimerImpl extends Timer0 { private final String name; - private V value; + final com.codahale.metrics.Timer metric; - @SuppressWarnings("unchecked") - CallbackMetricImpl(String name, Class valueClass) { + private TimerImpl(String name, com.codahale.metrics.Timer metric) { 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()); - } + this.metric = metric; } @Override - public void set(V value) { - this.value = value; + public void record(long value, TimeUnit unit) { + checkArgument(value >= 0, "timer delta must be >= 0"); + metric.update(value, unit); } @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; - } - }; + descriptions.remove(name); + registry.remove(name); } } } 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 index 986adc591c..04d10a25dc 100644 --- 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 @@ -55,14 +55,12 @@ class ListMetrics implements RestReadView { } SortedMap out = new TreeMap<>(); - Map all = metrics.getMetricMap(); - List prefixes = new ArrayList<>(query.size()); for (String q : query) { if (q.endsWith("/")) { prefixes.add(q); } else { - Metric m = all.get(q); + Metric m = metrics.getMetric(q); if (m != null) { out.put(q, toJson(q, m)); } @@ -70,10 +68,9 @@ class ListMetrics implements RestReadView { } if (query.isEmpty() || !prefixes.isEmpty()) { - for (Map.Entry e : all.entrySet()) { - String name = e.getKey(); + for (String name : metrics.getMetricNames()) { if (include(prefixes, name)) { - out.put(name, toJson(name, e.getValue())); + out.put(name, toJson(name, metrics.getMetric(name))); } } } 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 index 625de6958a..f43dd6f899 100644 --- 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 @@ -14,8 +14,11 @@ 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; @@ -24,6 +27,11 @@ 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; @@ -50,6 +58,9 @@ class MetricJson { Double max; Double std_dev; + List fields; + Map buckets; + MetricJson(Metric metric, ImmutableMap atts, boolean dataOnly) { if (!dataOnly) { description = atts.get(Description.DESCRIPTION); @@ -62,7 +73,20 @@ class MetricJson { } private void init(Metric metric, ImmutableMap atts) { - if (metric instanceof Counter) { + 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(); @@ -103,4 +127,61 @@ class MetricJson { private static Boolean toBool(ImmutableMap atts, String key) { return Description.TRUE_VALUE.equals(atts.get(key)) ? true : null; } + + @SuppressWarnings("unchecked") + private static Map makeBuckets( + Field[] fields, + Map metrics, + ImmutableMap atts) { + if (fields.length == 1) { + Function fmt = + (Function) fields[0].formatter(); + Map out = new TreeMap<>(); + for (Map.Entry e : metrics.entrySet()) { + out.put( + fmt.apply(e.getKey()), + new MetricJson(e.getValue(), atts, true)); + } + return out; + } + + Map out = new TreeMap<>(); + for (Map.Entry e : metrics.entrySet()) { + ImmutableList keys = (ImmutableList) e.getKey(); + Map dst = out; + + for (int i = 0; i < fields.length - 1; i++) { + Function fmt = + (Function) fields[i].formatter(); + String key = fmt.apply(keys.get(i)); + Map t = (Map) dst.get(key); + if (t == null) { + t = new TreeMap<>(); + dst.put(key, t); + } + dst = t; + } + + Function fmt = + (Function) 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/TimerImpl1.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/TimerImpl1.java new file mode 100644 index 0000000000..0164f6f5b5 --- /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 extends BucketedTimer implements BucketedMetric { + TimerImpl1(DropWizardMetricMaker metrics, String name, + Description desc, Field field1) { + super(metrics, name, desc, field1); + } + + Timer1 timer() { + return new Timer1() { + @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 fmt = + (Function) 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 0000000000..49c9f14e58 --- /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); + } + + Timer2 timer2() { + return new Timer2() { + @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(); + } + }; + } + + Timer3 timer3() { + return new Timer3() { + @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 keyList = (ImmutableList) key; + String[] parts = new String[fields.length]; + for (int i = 0; i < fields.length; i++) { + Function fmt = + (Function) 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 index 2f87d38318..cbaca6b381 100644 --- 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 @@ -14,8 +14,9 @@ package com.google.gerrit.server.git; -import com.google.gerrit.metrics.Counter; +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; @@ -25,7 +26,12 @@ import org.eclipse.jgit.transport.PostUploadHook; @Singleton public class UploadPackMetricsHook implements PostUploadHook { - private final Counter upload; + enum Operation { + CLONE, + FETCH; + } + + private final Counter1 upload; @Inject UploadPackMetricsHook(MetricMaker metricMaker) { @@ -33,11 +39,17 @@ public class UploadPackMetricsHook implements PostUploadHook { "git/upload-pack", new Description("Total number of git-upload-pack requests") .setRate() - .setUnit("requests")); + .setUnit("requests"), + Field.ofEnum(Operation.class, "operation")); } @Override public void onPostUpload(PackStatistics stats) { - upload.increment(); + 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/PluginMetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginMetricMaker.java index 7881433f6c..8bf78b5508 100644 --- 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 @@ -17,10 +17,18 @@ 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.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.Timer; +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; @@ -39,23 +47,79 @@ class PluginMetricMaker extends MetricMaker implements LifecycleListener { } @Override - public Counter newCounter(String name, Description desc) { - Counter m = root.newCounter(prefix + name, desc); + public Counter0 newCounter(String name, Description desc) { + Counter0 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); + public Counter1 newCounter( + String name, Description desc, + Field field1) { + Counter1 m = root.newCounter(prefix + name, desc, field1); cleanup.add(m); return m; } @Override - public CallbackMetric newCallbackMetric(String name, - Class valueClass, Description desc) { - CallbackMetric m = root.newCallbackMetric(prefix + name, valueClass, desc); + public Counter2 newCounter( + String name, Description desc, + Field field1, Field field2) { + Counter2 m = root.newCounter(prefix + name, desc, field1, field2); + cleanup.add(m); + return m; + } + + @Override + public Counter3 newCounter( + String name, Description desc, + Field field1, Field field2, Field field3) { + Counter3 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 Timer1 newTimer( + String name, Description desc, + Field field1) { + Timer1 m = root.newTimer(prefix + name, desc, field1); + cleanup.add(m); + return m; + } + + @Override + public Timer2 newTimer( + String name, Description desc, + Field field1, Field field2) { + Timer2 m = root.newTimer(prefix + name, desc, field1, field2); + cleanup.add(m); + return m; + } + + @Override + public Timer3 newTimer( + String name, Description desc, + Field field1, Field field2, Field field3) { + Timer3 m = + root.newTimer(prefix + name, desc, field1, field2, field3); + cleanup.add(m); + return m; + } + + @Override + public CallbackMetric0 newCallbackMetric( + String name, Class valueClass, Description desc) { + CallbackMetric0 m = root.newCallbackMetric(prefix + name, valueClass, desc); cleanup.add(m); return m; } 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 b0a376cae6..c3c70b379d 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 @@ -22,7 +22,7 @@ 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.metrics.Timer0; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.index.IndexConfig; @@ -122,7 +122,7 @@ public class QueryProcessor { List> queries) throws OrmException, QueryParseException { @SuppressWarnings("resource") - Timer.Context context = metrics.executionTime.start(); + Timer0.Context context = metrics.executionTime.start(); Predicate visibleToMe = enforceVisibility ? new IsVisibleToPredicate(db, changeControlFactory, userProvider.get()) @@ -217,7 +217,7 @@ public class QueryProcessor { @Singleton static class Metrics { - final Timer executionTime; + final Timer0 executionTime; @Inject Metrics(MetricMaker metricMaker) { 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 37d2cb702c..6bbc5abfc4 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 @@ -24,7 +24,7 @@ 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.Counter0; import com.google.gerrit.metrics.Description; import com.google.gerrit.metrics.MetricMaker; import com.google.gerrit.server.config.ConfigUtil; @@ -266,7 +266,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener { } }); - final Counter sesssionsCreated = metricMaker.newCounter( + final Counter0 sesssionsCreated = metricMaker.newCounter( "sshd/sessions/created", new Description("Rate of new SSH sessions") .setRate()