package io.dropwizard.metrics.jetty9; import static io.dropwizard.metrics.MetricRegistry.name; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.server.AsyncContextState; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.HandlerWrapper; import io.dropwizard.metrics.Counter; import io.dropwizard.metrics.Meter; import io.dropwizard.metrics.MetricName; import io.dropwizard.metrics.MetricRegistry; import io.dropwizard.metrics.RatioGauge; import io.dropwizard.metrics.Timer; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.concurrent.TimeUnit; /** * A Jetty {@link Handler} which records various metrics about an underlying {@link Handler} * instance. */ public class InstrumentedHandler extends HandlerWrapper { private final MetricRegistry metricRegistry; private String name; private final String prefix; // the requests handled by this handler, excluding active private Timer requests; // the number of dispatches seen by this handler, excluding active private Timer dispatches; // the number of active requests private Counter activeRequests; // the number of active dispatches private Counter activeDispatches; // the number of requests currently suspended. private Counter activeSuspended; // the number of requests that have been asynchronously dispatched private Meter asyncDispatches; // the number of requests that expired while suspended private Meter asyncTimeouts; private Meter[] responses; private Timer getRequests; private Timer postRequests; private Timer headRequests; private Timer putRequests; private Timer deleteRequests; private Timer optionsRequests; private Timer traceRequests; private Timer connectRequests; private Timer moveRequests; private Timer otherRequests; private AsyncListener listener; /** * Create a new instrumented handler using a given metrics registry. * * @param registry the registry for the metrics * */ public InstrumentedHandler(MetricRegistry registry) { this(registry, null); } /** * Create a new instrumented handler using a given metrics registry. * * @param registry the registry for the metrics * @param prefix the prefix to use for the metrics names * */ public InstrumentedHandler(MetricRegistry registry, String prefix) { this.metricRegistry = registry; this.prefix = prefix; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override protected void doStart() throws Exception { super.doStart(); final MetricName prefix = this.prefix == null ? name(getHandler().getClass(), name) : name(this.prefix, name); this.requests = metricRegistry.timer(prefix.resolve("requests")); this.dispatches = metricRegistry.timer(prefix.resolve("dispatches")); this.activeRequests = metricRegistry.counter(prefix.resolve("active-requests")); this.activeDispatches = metricRegistry.counter(prefix.resolve("active-dispatches")); this.activeSuspended = metricRegistry.counter(prefix.resolve("active-suspended")); this.asyncDispatches = metricRegistry.meter(prefix.resolve("async-dispatches")); this.asyncTimeouts = metricRegistry.meter(prefix.resolve("async-timeouts")); this.responses = new Meter[]{ metricRegistry.meter(prefix.resolve("1xx-responses")), // 1xx metricRegistry.meter(prefix.resolve("2xx-responses")), // 2xx metricRegistry.meter(prefix.resolve("3xx-responses")), // 3xx metricRegistry.meter(prefix.resolve("4xx-responses")), // 4xx metricRegistry.meter(prefix.resolve("5xx-responses")) // 5xx }; this.getRequests = metricRegistry.timer(prefix.resolve("get-requests")); this.postRequests = metricRegistry.timer(prefix.resolve("post-requests")); this.headRequests = metricRegistry.timer(prefix.resolve("head-requests")); this.putRequests = metricRegistry.timer(prefix.resolve("put-requests")); this.deleteRequests = metricRegistry.timer(prefix.resolve("delete-requests")); this.optionsRequests = metricRegistry.timer(prefix.resolve("options-requests")); this.traceRequests = metricRegistry.timer(prefix.resolve("trace-requests")); this.connectRequests = metricRegistry.timer(prefix.resolve("connect-requests")); this.moveRequests = metricRegistry.timer(prefix.resolve("move-requests")); this.otherRequests = metricRegistry.timer(prefix.resolve("other-requests")); metricRegistry.register(prefix.resolve("percent-4xx-1m"), new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(responses[3].getOneMinuteRate(), requests.getOneMinuteRate()); } }); metricRegistry.register(prefix.resolve("percent-4xx-5m"), new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(responses[3].getFiveMinuteRate(), requests.getFiveMinuteRate()); } }); metricRegistry.register(prefix.resolve("percent-4xx-15m"), new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(responses[3].getFifteenMinuteRate(), requests.getFifteenMinuteRate()); } }); metricRegistry.register(prefix.resolve("percent-5xx-1m"), new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(responses[4].getOneMinuteRate(), requests.getOneMinuteRate()); } }); metricRegistry.register(prefix.resolve("percent-5xx-5m"), new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(responses[4].getFiveMinuteRate(), requests.getFiveMinuteRate()); } }); metricRegistry.register(prefix.resolve("percent-5xx-15m"), new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(responses[4].getFifteenMinuteRate(), requests.getFifteenMinuteRate()); } }); this.listener = new AsyncListener() { private long startTime; @Override public void onTimeout(AsyncEvent event) throws IOException { asyncTimeouts.mark(); } @Override public void onStartAsync(AsyncEvent event) throws IOException { startTime = System.currentTimeMillis(); event.getAsyncContext().addListener(this); } @Override public void onError(AsyncEvent event) throws IOException { } @Override public void onComplete(AsyncEvent event) throws IOException { final AsyncContextState state = (AsyncContextState) event.getAsyncContext(); final HttpServletRequest request = (HttpServletRequest) state.getRequest(); final HttpServletResponse response = (HttpServletResponse) state.getResponse(); updateResponses(request, response, startTime); if (state.getHttpChannelState().getState() != HttpChannelState.State.DISPATCHED) { activeSuspended.dec(); } } }; } @Override public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException { activeDispatches.inc(); final long start; final HttpChannelState state = request.getHttpChannelState(); if (state.isInitial()) { // new request activeRequests.inc(); start = request.getTimeStamp(); } else { // resumed request start = System.currentTimeMillis(); activeSuspended.dec(); if (state.getState() == HttpChannelState.State.DISPATCHED) { asyncDispatches.mark(); } } try { super.handle(path, request, httpRequest, httpResponse); } finally { final long now = System.currentTimeMillis(); final long dispatched = now - start; activeDispatches.dec(); dispatches.update(dispatched, TimeUnit.MILLISECONDS); if (state.isSuspended()) { if (state.isInitial()) { state.addListener(listener); } activeSuspended.inc(); } else if (state.isInitial()) { updateResponses(httpRequest, httpResponse, start); } // else onCompletion will handle it. } } private Timer requestTimer(String method) { final HttpMethod m = HttpMethod.fromString(method); if (m == null) { return otherRequests; } else { switch (m) { case GET: return getRequests; case POST: return postRequests; case PUT: return putRequests; case HEAD: return headRequests; case DELETE: return deleteRequests; case OPTIONS: return optionsRequests; case TRACE: return traceRequests; case CONNECT: return connectRequests; case MOVE: return moveRequests; default: return otherRequests; } } } private void updateResponses(HttpServletRequest request, HttpServletResponse response, long start) { final int responseStatus = response.getStatus() / 100; if (responseStatus >= 1 && responseStatus <= 5) { responses[responseStatus - 1].mark(); } activeRequests.dec(); final long elapsedTime = System.currentTimeMillis() - start; requests.update(elapsedTime, TimeUnit.MILLISECONDS); requestTimer(request.getMethod()).update(elapsedTime, TimeUnit.MILLISECONDS); } }