blob: 2956a3ef78c7636d974ebf8522293babeb85b28d [file] [log] [blame]
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.metrics.dropwizard;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.metrics.dropwizard.MetricResource.METRIC_KIND;
import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.gerrit.metrics.CallbackMetric;
import com.google.gerrit.metrics.CallbackMetric0;
import com.google.gerrit.metrics.CallbackMetric1;
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.Description.FieldOrdering;
import com.google.gerrit.metrics.Field;
import com.google.gerrit.metrics.Histogram0;
import com.google.gerrit.metrics.Histogram1;
import com.google.gerrit.metrics.Histogram2;
import com.google.gerrit.metrics.Histogram3;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.metrics.MetricsReservoirConfig;
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.proc.JGitMetricModule;
import com.google.gerrit.metrics.proc.ProcMetricModule;
import com.google.gerrit.server.cache.CacheMetrics;
import com.google.gerrit.server.config.MetricsReservoirConfigImpl;
import com.google.inject.Inject;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Connects Gerrit metric package onto DropWizard.
*
* @see <a href="http://www.dropwizard.io/">DropWizard</a>
*/
@Singleton
public class DropWizardMetricMaker extends MetricMaker {
public static class ApiModule extends RestApiModule {
private final Optional<MetricsReservoirConfig> metricsReservoirConfig;
public ApiModule(MetricsReservoirConfig metricsReservoirConfig) {
this.metricsReservoirConfig = Optional.of(metricsReservoirConfig);
}
public ApiModule() {
this.metricsReservoirConfig = Optional.empty();
}
@Override
protected void configure() {
if (metricsReservoirConfig.isPresent()) {
bind(MetricsReservoirConfig.class).toInstance(metricsReservoirConfig.get());
} else {
bind(MetricsReservoirConfig.class)
.to(MetricsReservoirConfigImpl.class)
.in(Scopes.SINGLETON);
}
bind(MetricRegistry.class).in(Scopes.SINGLETON);
bind(DropWizardMetricMaker.class).in(Scopes.SINGLETON);
bind(MetricMaker.class).to(DropWizardMetricMaker.class);
install(new ProcMetricModule());
install(new JGitMetricModule());
}
}
public static class RestModule extends RestApiModule {
@Override
protected void configure() {
DynamicMap.mapOf(binder(), METRIC_KIND);
child(CONFIG_KIND, "metrics").to(MetricsCollection.class);
get(METRIC_KIND).to(GetMetric.class);
bind(CacheMetrics.class);
}
}
private final MetricRegistry registry;
private final Map<String, BucketedMetric> bucketed;
private final Map<String, ImmutableMap<String, String>> descriptions;
private final MetricsReservoirConfig reservoirConfig;
@Inject
DropWizardMetricMaker(MetricRegistry registry, MetricsReservoirConfig reservoirConfig) {
this.registry = registry;
this.bucketed = new ConcurrentHashMap<>();
this.descriptions = new ConcurrentHashMap<>();
this.reservoirConfig = reservoirConfig;
}
Iterable<String> getMetricNames() {
return descriptions.keySet();
}
/** Get the underlying metric implementation. */
public Metric getMetric(String name) {
Metric m = bucketed.get(name);
return m != null ? m : registry.getMetrics().get(name);
}
/** Lookup annotations from a metric's {@link Description}. */
public ImmutableMap<String, String> getAnnotations(String name) {
return descriptions.get(name);
}
@Override
public synchronized Counter0 newCounter(String name, Description desc) {
checkCounterDescription(name, desc);
define(name, desc);
return newCounterImpl(name, desc.isRate());
}
@Override
public synchronized <F1> Counter1<F1> newCounter(
String name, Description desc, Field<F1> field1) {
checkCounterDescription(name, desc);
CounterImpl1<F1> m = new CounterImpl1<>(this, name, desc, field1);
define(name, desc);
bucketed.put(name, m);
return m.counter();
}
@Override
public synchronized <F1, F2> Counter2<F1, F2> newCounter(
String name, Description desc, Field<F1> field1, Field<F2> field2) {
checkCounterDescription(name, desc);
CounterImplN m = new CounterImplN(this, name, desc, field1, field2);
define(name, desc);
bucketed.put(name, m);
return m.counter2();
}
@Override
public synchronized <F1, F2, F3> Counter3<F1, F2, F3> newCounter(
String name, Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
checkCounterDescription(name, 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(String name, Description desc) {
checkMetricName(name);
checkArgument(!desc.isConstant(), "counter must not be constant");
checkArgument(!desc.isGauge(), "counter must not be gauge");
}
CounterImpl newCounterImpl(String name, boolean isRate) {
if (isRate) {
final com.codahale.metrics.Meter m = registry.meter(name);
return new CounterImpl(name, m) {
@Override
public void incrementBy(long delta) {
checkArgument(delta >= 0, "counter delta must be >= 0");
m.mark(delta);
}
};
}
final com.codahale.metrics.Counter m = registry.counter(name);
return new CounterImpl(name, m) {
@Override
public void incrementBy(long delta) {
checkArgument(delta >= 0, "counter delta must be >= 0");
m.inc(delta);
}
};
}
@Override
public synchronized Timer0 newTimer(String name, Description desc) {
checkTimerDescription(name, desc);
define(name, desc);
return newTimerImpl(name);
}
@Override
public synchronized <F1> Timer1<F1> newTimer(String name, Description desc, Field<F1> field1) {
checkTimerDescription(name, desc);
TimerImpl1<F1> m = new TimerImpl1<>(this, name, desc, field1);
define(name, desc);
bucketed.put(name, m);
return m.timer();
}
@Override
public synchronized <F1, F2> Timer2<F1, F2> newTimer(
String name, Description desc, Field<F1> field1, Field<F2> field2) {
checkTimerDescription(name, desc);
TimerImplN m = new TimerImplN(this, name, desc, field1, field2);
define(name, desc);
bucketed.put(name, m);
return m.timer2();
}
@Override
public synchronized <F1, F2, F3> Timer3<F1, F2, F3> newTimer(
String name, Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
checkTimerDescription(name, 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(String name, Description desc) {
checkMetricName(name);
checkArgument(!desc.isConstant(), "timer must not be constant");
checkArgument(!desc.isGauge(), "timer must not be a gauge");
checkArgument(!desc.isRate(), "timer must not be a rate");
checkArgument(desc.isCumulative(), "timer must be cumulative");
checkArgument(desc.getTimeUnit() != null, "timer must have a unit");
}
TimerImpl newTimerImpl(String name) {
return new TimerImpl(
name,
registry.timer(name, () -> new Timer(DropWizardReservoirProvider.get(reservoirConfig))));
}
@Override
public synchronized Histogram0 newHistogram(String name, Description desc) {
checkHistogramDescription(name, desc);
define(name, desc);
return newHistogramImpl(name);
}
@Override
public synchronized <F1> Histogram1<F1> newHistogram(
String name, Description desc, Field<F1> field1) {
checkHistogramDescription(name, desc);
HistogramImpl1<F1> m = new HistogramImpl1<>(this, name, desc, field1);
define(name, desc);
bucketed.put(name, m);
return m.histogram1();
}
@Override
public synchronized <F1, F2> Histogram2<F1, F2> newHistogram(
String name, Description desc, Field<F1> field1, Field<F2> field2) {
checkHistogramDescription(name, desc);
HistogramImplN m = new HistogramImplN(this, name, desc, field1, field2);
define(name, desc);
bucketed.put(name, m);
return m.histogram2();
}
@Override
public synchronized <F1, F2, F3> Histogram3<F1, F2, F3> newHistogram(
String name, Description desc, Field<F1> field1, Field<F2> field2, Field<F3> field3) {
checkHistogramDescription(name, desc);
HistogramImplN m = new HistogramImplN(this, name, desc, field1, field2, field3);
define(name, desc);
bucketed.put(name, m);
return m.histogram3();
}
private static void checkHistogramDescription(String name, Description desc) {
checkMetricName(name);
checkArgument(!desc.isConstant(), "histogram must not be constant");
checkArgument(!desc.isGauge(), "histogram must not be a gauge");
checkArgument(!desc.isRate(), "histogram must not be a rate");
checkArgument(desc.isCumulative(), "histogram must be cumulative");
}
HistogramImpl newHistogramImpl(String name) {
return new HistogramImpl(
name,
registry.histogram(
name, () -> new Histogram(DropWizardReservoirProvider.get(reservoirConfig))));
}
@Override
public <V> CallbackMetric0<V> newCallbackMetric(
String name, Class<V> valueClass, Description desc) {
checkMetricName(name);
define(name, desc);
return new CallbackMetricImpl0<>(this, registry, name, valueClass);
}
@Override
public <F1, V> CallbackMetric1<F1, V> newCallbackMetric(
String name, Class<V> valueClass, Description desc, Field<F1> field1) {
checkMetricName(name);
CallbackMetricImpl1<F1, V> m =
new CallbackMetricImpl1<>(this, registry, name, valueClass, desc, field1);
define(name, desc);
bucketed.put(name, m);
return m.create();
}
@Override
public synchronized RegistrationHandle newTrigger(
Set<CallbackMetric<?>> metrics, Runnable trigger) {
ImmutableSet<CallbackMetricGlue> all =
FluentIterable.from(metrics).transform(m -> (CallbackMetricGlue) m).toSet();
trigger = new CallbackGroup(trigger, all);
for (CallbackMetricGlue m : all) {
m.register(trigger);
}
trigger.run();
return () -> all.forEach(CallbackMetricGlue::remove);
}
synchronized void remove(String name) {
bucketed.remove(name);
descriptions.remove(name);
}
private synchronized void define(String name, Description desc) {
if (descriptions.containsKey(name)) {
ImmutableMap<String, String> annotations = descriptions.get(name);
if (!desc.getAnnotations()
.get(Description.DESCRIPTION)
.equals(annotations.get(Description.DESCRIPTION))) {
throw new IllegalStateException(String.format("metric '%s' already defined", name));
}
} else {
descriptions.put(name, desc.getAnnotations());
}
}
private static final Pattern METRIC_NAME_PATTERN =
Pattern.compile("[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*");
private static final Pattern INVALID_CHAR_PATTERN = Pattern.compile("[^\\w-/]");
private static final String REPLACEMENT_PREFIX = "_0x";
private static void checkMetricName(String name) {
checkArgument(
METRIC_NAME_PATTERN.matcher(name).matches(),
"invalid metric name '%s': must match pattern '%s'",
name,
METRIC_NAME_PATTERN.pattern());
}
/**
* Ensures that the sanitized metric name doesn't contain invalid characters and removes the risk
* of collision (between the sanitized metric names). Modifications to the input metric name:
*
* <ul>
* <li/>leading <code>/</code> is replaced with <code>_</code>
* <li/>doubled (or repeated more times) <code>/</code> are reduced to a single <code>/</code>
* <li/>ending <code>/</code> is removed
* <li/>all characters that are not <code>/a-zA-Z0-9_-</code> are replaced with <code>
* _0x[HEX CODE]_</code> (code is capitalized)
* <li/>the replacement prefix <code>_0x</code> is prepended with another replacement prefix
* </ul>
*
* @param name name of the metric to sanitize
* @return sanitized metric name
*/
@Override
public String sanitizeMetricName(String name) {
if (METRIC_NAME_PATTERN.matcher(name).matches() && !name.contains(REPLACEMENT_PREFIX)) {
return name;
}
StringBuilder sanitizedName =
new StringBuilder(name.substring(0, 1).replaceFirst("[^\\w-]", "_"));
if (name.length() == 1) {
return sanitizedName.toString();
}
String slashSanitizedName = name.substring(1).replaceAll("/[/]+", "/");
if (slashSanitizedName.endsWith("/")) {
slashSanitizedName = slashSanitizedName.substring(0, slashSanitizedName.length() - 1);
}
String replacementPrefixSanitizedName =
slashSanitizedName.replaceAll(REPLACEMENT_PREFIX, REPLACEMENT_PREFIX + REPLACEMENT_PREFIX);
for (int i = 0; i < replacementPrefixSanitizedName.length(); i++) {
Character c = replacementPrefixSanitizedName.charAt(i);
Matcher matcher = INVALID_CHAR_PATTERN.matcher(c.toString());
if (matcher.matches()) {
sanitizedName.append(REPLACEMENT_PREFIX);
sanitizedName.append(Integer.toHexString(c).toUpperCase());
sanitizedName.append('_');
} else {
sanitizedName.append(c);
}
}
return sanitizedName.toString();
}
static String name(Description.FieldOrdering ordering, String codeName, String fieldValues) {
if (ordering == FieldOrdering.PREFIX_FIELDS_BASENAME) {
int s = codeName.lastIndexOf('/');
if (s > 0) {
String prefix = codeName.substring(0, s);
String metric = codeName.substring(s + 1);
return prefix + '/' + fieldValues + '/' + metric;
}
}
return codeName + '/' + fieldValues;
}
abstract class CounterImpl extends Counter0 {
private final String name;
final Metric metric;
CounterImpl(String name, Metric metric) {
this.name = name;
this.metric = metric;
}
@Override
public void remove() {
descriptions.remove(name);
registry.remove(name);
}
}
class TimerImpl extends Timer0 {
final com.codahale.metrics.Timer metric;
private TimerImpl(String name, com.codahale.metrics.Timer metric) {
super(name);
this.metric = metric;
}
@Override
protected void doRecord(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);
}
}
class HistogramImpl extends Histogram0 {
private final String name;
final com.codahale.metrics.Histogram metric;
private HistogramImpl(String name, com.codahale.metrics.Histogram metric) {
this.name = name;
this.metric = metric;
}
@Override
public void record(long value) {
metric.update(value);
}
@Override
public void remove() {
descriptions.remove(name);
registry.remove(name);
}
}
}