package com.google.sitebricks.stat; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.sitebricks.stat.StatsServlet.DEFAULT_FORMAT; import com.google.inject.matcher.Matchers; import com.google.inject.multibindings.MapBinder; import com.google.inject.servlet.ServletModule; import com.google.sitebricks.stat.StatsPublishers.HtmlStatsPublisher; import com.google.sitebricks.stat.StatsPublishers.JsonStatsPublisher; import com.google.sitebricks.stat.StatsPublishers.TextStatsPublisher; /** * This module enables publishing values annotated with {@link Stat} to a given * servlet path. * <p> * <h3>Example of Use</h3> * As an example, consider the following class: * <pre><code> * class QueryServlet extends HttpServlet { * {@literal @}Stat("search-hits") * private final AtomicInteger hits = new AtomicInteger(0); * * {@literal @}Inject QueryServlet(....) { } * * {@literal @}Override void doGet( * HttpServletRequest req, HttpServletResponse resp) { * .... * String searchTerm = req.getParameter("q"); * SearchResult result = searchService.searchFor(searchTerm); * if (result.hasHits()) { * hits.incrementAndGet(); * } * ... * } * } * </pre></code> * <p> * This class registers a stat called {@code search-hits}. To configure the * server to publish this stat, install a {@link StatModule}, such as: * <pre><code> * public class YourServerModule extends AbstractModule { * {@literal @}Override protected void configure() { * install(new StatsModule("/stats"); * ... * } * } * </code></pre> * <p> * Then, to query the server for its stats, hit the url that was registered * with the module (which was {@code /stats}, in the example above). * * <h3>Registering Stats</h3> * The simplest way of registering a stat is to use the <code>@Stat</code> * annotation. Members of a class annotated by <code>@Stat</code> are * registered automatically when an instance of the class is created by Guice. * The value of the member is read when a snapshot of the stats is requested, * most likely by the {@link StatsServlet} upon a request to {@code /stats}. * <p> * At times it is convenient to "manually" register a stat. To do this, * inject an instance of {@link StatRegistrar} and use it to register a stat. * For example: * <pre><code> * class RegistersLocalVariableAsStat { * * private final StatRegistrar statRegistrar; * * {@literal @}Inject RegistersLocalVariableAsStat( * StatRegistrar statRegistrar) { * this.statRegistrar = statRegistrar; * } * * void initialize() { * long start = System.currentTimeMillis(); * doInitialization(); * statRegistrar.registerSingleStat( * "init-time-in-ms", * "Initialization time of a class", * System.currentTimeMillis() - start); * } * } * </code></pre> * There are other convenience methods on {@link StatRegistrar} to facilitate * registering annotated static members on classes and registering all * annotated members on instances as well. * * <h3>Exposing Stats</h3> * It's important to consider, if only to be careful, how to prevent a mutable * reference of a stat from leaking into the stat publishing logic. For * instance, if you were to publish a deeply mutable reference to a * <code>List</code>, then a stat publisher could inadvertently (or purposely) * mutate it. * <p> * It is the role of a {@link StatExposer} to guard against such leaks: An * exposer is given the raw value of a stat, and should return a safe view of * it. This view is then passed to the {@link StatsPublishers publishers}. * <p> * By default, a {@link StatExposers.InferenceExposer} is used to guard stats * registered via {@link Stat <code>@Stat</code>}. This implementation should * handle the majority of common use cases. If, however, you want to use a * different {@link StatExposer} for your stat, then you may do so by * specifying its class within the {@link Stat <code>@Stat</code>} annotation. * For example: * <pre><code> * class ServiceStat implements Cloneable { * int calls; * AtomicLong<Long> latencyInMs; * * {@literal @}Override protected Object clone() { * return new ServiceStat(calls, latencyInMs); * } * } * * class ServiceStatExposer implements StatExposer<ServiceStat> { * {@literal @}Override Object expose(ServiceStat serviceStat) { * return serviceStat.clone(); * } * } * * class Service { * {@literal @}Stat(value = "service-stat", exposer = ServiceStatExposer.class) * private final ServiceStat serviceStat; * * ... * } * </pre></code> * * <h3>Published Formats</h3> * By default, published stats are available in several formats: * <ul> * <li>html - a formatted html page * <li>json - well formed json * <li>text - simple plaintext page * </ul> * To request stats in a given format, include a value for the * {@value StatsServlet#DEFAULT_FORMAT} parameter in the {@code /stats} request. * For the formats above, the value for this parameter should correspond to the * type of output (i.e., pass "html", "json", or "text"). If no parameter is * given, then html is returned. * <p> * <h3>Extensions</h3> * You may extend the default set of publishers by adding and binding another * implementation of {@link StatsPublisher}. To add your implementation, * add a binding to a {@link MapBinder MapBinder<String, StatsPublisher>}. * For example: * <pre><code> * public class CustomPublisherModule extends AbstractModule { * {@literal @}Override protected void configure() { * MapBinder<String, StatsPublisher> mapBinder = * MapBinder.newMapBinder(binder(), String.class, StatsPublisher.class); * mapBinder.addBinding("custom").to(CustomStatsPublisher.class); * } * } * </code></pre> * You can then retrieve stats from your custom publisher by hitting * {@code /stats?format=custom}. * * @author dhanji@gmail.com (Dhanji R. Prasanna) * @author ffaber@gmail.com (Fred Faber) */ public class StatModule extends ServletModule { private final String uriPath; public StatModule(String uriPath) { checkArgument(!isNullOrEmpty(uriPath), "URI path must be a valid non-empty servlet path mapping (example: /stats)"); this.uriPath = uriPath; } @Override protected void configureServlets() { // Manual bootstrapping is needed to instantiated a well-formed listener. Stats stats = new Stats(); bind(Stats.class).toInstance(stats); requestInjection(stats); StatRegistrar statRegistrar = new StatRegistrar(stats); bind(StatRegistrar.class).toInstance(statRegistrar); StatAnnotatedTypeListener listener = new StatAnnotatedTypeListener(statRegistrar); bindListener(Matchers.any(), listener); serve(uriPath).with(StatsServlet.class); MapBinder<String, StatsPublisher> publisherBinder = MapBinder.newMapBinder(binder(), String.class, StatsPublisher.class); publisherBinder.addBinding(DEFAULT_FORMAT).to(HtmlStatsPublisher.class); publisherBinder.addBinding("html").to(HtmlStatsPublisher.class); publisherBinder.addBinding("json").to(JsonStatsPublisher.class); publisherBinder.addBinding("text").to(TextStatsPublisher.class); } }