/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.jooby.metrics; import static java.util.Objects.requireNonNull; import java.io.Closeable; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import org.jooby.Env; import org.jooby.Jooby; import org.jooby.Router; import org.jooby.internal.metrics.HealthCheckRegistryInitializer; import org.jooby.internal.metrics.MetricRegistryInitializer; import com.codahale.metrics.Metric; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.Reporter; import com.codahale.metrics.health.HealthCheck; import com.codahale.metrics.health.HealthCheckRegistry; import com.google.inject.Binder; import com.google.inject.multibindings.MapBinder; import com.google.inject.multibindings.Multibinder; import com.typesafe.config.Config; /** * <h1>metrics</h1> * <p> * Metrics provides a powerful toolkit of ways to measure the behavior of critical components in * your production environment. * </p> * * <h2>usage</h2> * <pre> * { * use(new Metrics() * .request() * .threadDump() * .ping() * .healthCheck("db", new DatabaseHealthCheck()) * .metric("memory", new MemoryUsageGaugeSet()) * .metric("threads", new ThreadStatesGaugeSet()) * .metric("gc", new GarbageCollectorMetricSet()) * .metric("fs", new FileDescriptorRatioGauge()) * ); * } * </pre> * <p> * Let's see what all these means. * </p> * * <h2>metrics</h2> * <p> * Metrics are available at <code>/sys/metrics</code> or <code>/sys/metrics/:type</code> via: * </p> * <pre> * use(new Metrics() * .metric("memory", new MemoryUsageGaugeSet()) * .metric("threads", new ThreadStatesGaugeSet()) * .metric("gc", new GarbageCollectorMetricSet()) * .metric("fs", new FileDescriptorRatioGauge())); * </pre> * <p> * The <code>/:type</code> parameter is optional and let you filter metrics by type * <code>counters</code>, <code>guages</code>, etc.. * </p> * <p> * There is a <code>name</code> filter too: <code>/sys/metrics?name=memory</code> or * <code>/sys/metrics/guages?name=memory</code>. The <code>name</code> parameter filter all the * metrics where the name starts with the given <code>name</code>. * </p> * * <h2>health checks</h2> * <p> * Health checks are available at <code>/sys/healthCheck</code> via: * </p> * <pre> * use(new Metrics() * .healthCheck("db", new DatabaseHealthCheck())); * </pre> * * <h2>instrumented requests</h2> * <p> * Captures request information (like active requests or min/mean/max execution time) and a * breakdown of the response codes being returned: {@link InstrumentedHandler}. * </p> * <pre> * use(new Metrics() * .request()); * </pre> * * <h2>thread dump</h2> * <p> * A thread dump is available at <code>/sys/threadDump</code> via: * </p> * <pre> * use(new Metrics() * .threadDump()); * </pre> * * <h2>reporting</h2> * <p> * Reporters are appended via a callback API: * </p> * <pre> * { * use(new Metrics() * .reporter(registry {@literal ->} { * ConsoleReporter reporter = ConsoleReporter.forRegistry(registry).build(); * reporter.start(1, TimeUnit.MINUTES); * return reporter; * }); * } * </pre> * <p> * You can add all the reporters you want. Keep in mind you have to start them (if need it), but you * don't have to stop them as long they implements the {@link Closeable} interface. * </p> * * <p> * That's all folks! * </p> * * @author edgar * @since 0.13.0 */ public class Metrics implements Jooby.Module { static interface Bindings { void bind(Binder binder, Router routes, Config conf); } private String pattern; private List<Bindings> bindings = new ArrayList<>(); private List<Consumer<Router>> routes = new ArrayList<>(); private Set<BiFunction<MetricRegistry, Config, Reporter>> reporters = new LinkedHashSet<>(); private MetricRegistry metricRegistry; private HealthCheckRegistry healthCheckRegistry; /** * Creates a new {@link Metric} module. * * @param metricRegistry Use the given metricRegistry. * @param healthCheckRegistry Use the given healthCheckRegistry. * @param pattern A root pattern where to publish all the services. Default is: <code>/sys</code>. */ public Metrics(final MetricRegistry metricRegistry, final HealthCheckRegistry healthCheckRegistry, final String pattern) { this.metricRegistry = requireNonNull(metricRegistry, "Metric registry is required."); this.healthCheckRegistry = requireNonNull(healthCheckRegistry, "Health check registry is required."); this.pattern = requireNonNull(pattern, "A pattern is required."); } /** * Creates a new {@link Metric} module. * * @param metricRegistry Use the given metricRegistry. * @param pattern A root pattern where to publish all the services. Default is: <code>/sys</code>. */ public Metrics(final MetricRegistry metricRegistry, final String pattern) { this(metricRegistry, new HealthCheckRegistry(), pattern); } /** * Creates a new {@link Metric} module. * * @param pattern A root pattern where to publish all the services. Default is: <code>/sys</code>. */ public Metrics(final String pattern) { this(new MetricRegistry(), pattern); } /** * Creates a new {@link Metric} module. Services will be available at: <code>/sys</code>. * * @param healthCheckRegistry Use the given healthCheckRegistry. */ public Metrics(final HealthCheckRegistry healthCheckRegistry) { this(new MetricRegistry(), healthCheckRegistry, "/sys"); } /** * Creates a new {@link Metric} module. Services will be available at: <code>/sys</code>. * * @param metricRegistry Use the given metricRegistry. */ public Metrics(final MetricRegistry metricRegistry) { this(metricRegistry, "/sys"); } /** * Creates a new {@link Metric} module. Services will be available at: <code>/sys</code>. */ public Metrics() { this("/sys"); } /** * Instrument request using {@link InstrumentedHandler}. * * @param method Method to filter for. Default is: <code>GET</code>. * @param pattern A pattern to filter for. Default is: <code>*</code> (all the requests). * @return This metrics module. */ public Metrics request(final String method, final String pattern) { routes.add(r -> r.use(method, pattern, new InstrumentedHandler())); return this; } /** * Instrument request using {@link InstrumentedHandler}. * * @param pattern A pattern to filter for. Default is: <code>*</code> (all the requests). * @return This metrics module. */ public Metrics request(final String pattern) { return request("GET", pattern); } /** * Instrument request using {@link InstrumentedHandler}. It will intercept all the * <code>GET</code> calls. * * @return This metrics module. */ public Metrics request() { return request("*"); } /** * Append a simple ping handler that results in a <code>200</code> responses with a * <code>pong</code> body. See {@link PingHandler} * * @return This metrics module. */ public Metrics ping() { bindings.add((binder, routes, conf) -> { routes.use("GET", this.pattern + "/ping", new PingHandler()); }); return this; } /** * Append a handler that prints thread states (a.k.a thread dump). See {@link ThreadDumpHandler}. * * @return This metrics module. */ public Metrics threadDump() { bindings.add((binder, routes, conf) -> { routes.use("GET", this.pattern + "/thread-dump", new ThreadDumpHandler()); }); return this; } /** * Append a metric to the {@link MetricRegistry}, this call is identical to * {@link MetricRegistry#register(String, Metric)}. * * @param name Name of the metric. * @param metric A metric object * @return This metrics module. */ public Metrics metric(final String name, final Metric metric) { bindings.add((binder, routes, conf) -> { MapBinder.newMapBinder(binder, String.class, Metric.class).addBinding(name) .toInstance(metric); }); return this; } /** * Append a metric to the {@link MetricRegistry}. The metric will be resolved by Guice. This call * is identical to {@link MetricRegistry#register(String, Metric)}. * * @param name Name of the metric. * @param metric A metric object. * @param <M> Metric type. * @return This metrics module. */ public <M extends Metric> Metrics metric(final String name, final Class<M> metric) { bindings.add((binder, routes, conf) -> { MapBinder.newMapBinder(binder, String.class, Metric.class).addBinding(name) .to(metric); }); return this; } /** * Append a health check to the {@link HealthCheckRegistry}. This call is identical to * {@link HealthCheckRegistry#register(String, HealthCheck)}. * * @param name Name of the check. * @param check A check object. * @return This metrics module. */ public Metrics healthCheck(final String name, final HealthCheck check) { bindings.add((binder, routes, conf) -> { MapBinder.newMapBinder(binder, String.class, HealthCheck.class).addBinding(name) .toInstance(check); }); return this; } /** * Append a health check to the {@link HealthCheckRegistry}. The metric will be resolved by Guice. * This call is identical to {@link HealthCheckRegistry#register(String, HealthCheck)}. * * @param name Name of the check. * @param check A check object. * @param <H> {@link HealthCheck} type. * @return This metrics module. */ public <H extends HealthCheck> Metrics healthCheck(final String name, final Class<H> check) { bindings.add((binder, routes, conf) -> { MapBinder.newMapBinder(binder, String.class, HealthCheck.class) .addBinding(name) .to(check); }); return this; } /** * Append a {@link Reporter} to the {@link MetricRegistry}. * * @param callback Reporter callback. * @return This metrics module. */ public Metrics reporter(final BiFunction<MetricRegistry, Config, Reporter> callback) { this.reporters.add(requireNonNull(callback, "Callback is required.")); return this; } /** * Append a {@link Reporter} to the {@link MetricRegistry}. * * @param callback Reporter callback. * @return This metrics module. */ public Metrics reporter(final Function<MetricRegistry, Reporter> callback) { return reporter((registry, conf) -> callback.apply(registry)); } @Override public void configure(final Env env, final Config conf, final Binder binder) { // empty metric & checks MapBinder.newMapBinder(binder, String.class, Metric.class); MapBinder.newMapBinder(binder, String.class, HealthCheck.class); Router routes = env.router(); MetricHandler mhandler = new MetricHandler(); routes.use("GET", this.pattern + "/metrics", mhandler); routes.use("GET", this.pattern + "/metrics/:type", mhandler); routes.use("GET", this.pattern + "/healthcheck", new HealthCheckHandler()); Multibinder<Reporter> reporters = Multibinder.newSetBinder(binder, Reporter.class); binder.bind(MetricRegistry.class).toInstance(metricRegistry); this.reporters.forEach(it -> reporters.addBinding().toInstance(it.apply(metricRegistry, conf))); binder.bind(MetricRegistryInitializer.class).asEagerSingleton(); env.onStop(app -> app.require(MetricRegistryInitializer.class).close()); binder.bind(HealthCheckRegistry.class).toInstance(healthCheckRegistry); binder.bind(HealthCheckRegistryInitializer.class).asEagerSingleton(); bindings.forEach(it -> it.bind(binder, routes, conf)); this.routes.forEach(it -> it.accept(routes)); } }