diff --git a/metrics-bom/pom.xml b/metrics-bom/pom.xml index 9aa689d9bd..e3c15d6eb9 100644 --- a/metrics-bom/pom.xml +++ b/metrics-bom/pom.xml @@ -130,6 +130,16 @@ metrics-jetty11 ${project.version} + + io.dropwizard.metrics + metrics-jetty12 + ${project.version} + + + io.dropwizard.metrics + metrics-jetty12-ee10 + ${project.version} + io.dropwizard.metrics metrics-jmx diff --git a/metrics-jetty12-ee10/pom.xml b/metrics-jetty12-ee10/pom.xml new file mode 100644 index 0000000000..3c2893b69d --- /dev/null +++ b/metrics-jetty12-ee10/pom.xml @@ -0,0 +1,138 @@ + + + 4.0.0 + + + io.dropwizard.metrics + metrics-parent + 4.2.20-SNAPSHOT + + + metrics-jetty12-ee10 + Metrics Integration for Jetty 12.x and higher with Jakarta EE 10 + bundle + + A set of extensions for Jetty 12.x and higher which provide instrumentation of thread pools, connector + metrics, and application latency and utilization. This module uses the Servlet API from Jakarta EE 10. + + + + io.dropwizard.metrics.jetty12.ee10 + + 17 + + 2.0.7 + + + + + + io.dropwizard.metrics + metrics-bom + ${project.version} + pom + import + + + org.eclipse.jetty + jetty-bom + ${jetty12.version} + pom + import + + + org.eclipse.jetty.ee10 + jetty-ee10-bom + ${jetty12.version} + pom + import + + + org.slf4j + slf4j-api + ${slf4j.version} + + + net.bytebuddy + byte-buddy + ${byte-buddy.version} + + + jakarta.servlet + jakarta.servlet-api + ${servlet6.version} + + + + + + + io.dropwizard.metrics + metrics-core + + + io.dropwizard.metrics + metrics-annotation + + + io.dropwizard.metrics + metrics-jetty12 + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-util + + + org.eclipse.jetty.ee10 + jetty-ee10-servlet + + + jakarta.servlet + jakarta.servlet-api + + + org.slf4j + slf4j-api + ${slf4j.version} + runtime + + + junit + junit + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.eclipse.jetty + jetty-client + test + + + org.eclipse.jetty + jetty-http + test + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + diff --git a/metrics-jetty12-ee10/src/main/java/io/dropwizard/metrics/jetty12/ee10/InstrumentedEE10Handler.java b/metrics-jetty12-ee10/src/main/java/io/dropwizard/metrics/jetty12/ee10/InstrumentedEE10Handler.java new file mode 100644 index 0000000000..58a271703b --- /dev/null +++ b/metrics-jetty12-ee10/src/main/java/io/dropwizard/metrics/jetty12/ee10/InstrumentedEE10Handler.java @@ -0,0 +1,173 @@ +package io.dropwizard.metrics.jetty12.ee10; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.annotation.ResponseMeteredLevel; +import io.dropwizard.metrics.jetty12.AbstractInstrumentedHandler; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import org.eclipse.jetty.ee10.servlet.AsyncContextState; +import org.eclipse.jetty.ee10.servlet.ServletApiRequest; +import org.eclipse.jetty.ee10.servlet.ServletApiResponse; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.ee10.servlet.ServletRequestState; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static com.codahale.metrics.annotation.ResponseMeteredLevel.COARSE; + +/** + * A Jetty {@link Handler} which records various metrics about an underlying {@link Handler} + * instance. This {@link Handler} requires a {@link org.eclipse.jetty.ee10.servlet.ServletContextHandler} to be present. + * For correct behaviour, the {@link org.eclipse.jetty.ee10.servlet.ServletContextHandler} should be before this handler + * in the handler chain. To achieve this, one can use + * {@link org.eclipse.jetty.ee10.servlet.ServletContextHandler#insertHandler(Singleton)}. + */ +public class InstrumentedEE10Handler extends AbstractInstrumentedHandler { + private AsyncListener listener; + + /** + * Create a new instrumented handler using a given metrics registry. + * + * @param registry the registry for the metrics + */ + public InstrumentedEE10Handler(MetricRegistry registry) { + super(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 InstrumentedEE10Handler(MetricRegistry registry, String prefix) { + super(registry, prefix, COARSE); + } + + /** + * 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 + * @param responseMeteredLevel the level to determine individual/aggregate response codes that are instrumented + */ + public InstrumentedEE10Handler(MetricRegistry registry, String prefix, ResponseMeteredLevel responseMeteredLevel) { + super(registry, prefix, responseMeteredLevel); + } + + @Override + protected void doStart() throws Exception { + super.doStart(); + + this.listener = new AsyncAttachingListener(); + } + + @Override + protected void doStop() throws Exception { + super.doStop(); + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class); + + // only handle servlet requests with the InstrumentedHandler + // because it depends on the ServletRequestState + if (servletContextRequest == null) { + return super.handle(request, response, callback); + } + + activeDispatches.inc(); + + final long start; + final ServletRequestState state = servletContextRequest.getServletRequestState(); + if (state.isInitial()) { + // new request + activeRequests.inc(); + start = Request.getTimeStamp(request); + state.addListener(listener); + } else { + // resumed request + start = System.currentTimeMillis(); + activeSuspended.dec(); + if (state.getState() == ServletRequestState.State.HANDLING) { + asyncDispatches.mark(); + } + } + + boolean handled = false; + + try { + handled = super.handle(request, response, callback); + } finally { + final long now = System.currentTimeMillis(); + final long dispatched = now - start; + + activeDispatches.dec(); + dispatches.update(dispatched, TimeUnit.MILLISECONDS); + + if (state.isSuspended()) { + activeSuspended.inc(); + } else if (state.isInitial()) { + updateResponses(request, response, start, handled); + } + // else onCompletion will handle it. + } + + return handled; + } + + private class AsyncAttachingListener implements AsyncListener { + + @Override + public void onTimeout(AsyncEvent event) throws IOException {} + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + event.getAsyncContext().addListener(new InstrumentedAsyncListener()); + } + + @Override + public void onError(AsyncEvent event) throws IOException {} + + @Override + public void onComplete(AsyncEvent event) throws IOException {} + }; + + private class InstrumentedAsyncListener implements AsyncListener { + private final long startTime; + + InstrumentedAsyncListener() { + this.startTime = System.currentTimeMillis(); + } + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + asyncTimeouts.mark(); + } + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + } + + @Override + public void onError(AsyncEvent event) throws IOException { + } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + final AsyncContextState state = (AsyncContextState) event.getAsyncContext(); + final ServletApiRequest request = (ServletApiRequest) state.getRequest(); + final ServletApiResponse response = (ServletApiResponse) state.getResponse(); + updateResponses(request.getRequest(), response.getResponse(), startTime, true); + if (!state.getServletChannelState().isSuspended()) { + activeSuspended.dec(); + } + } + } +} diff --git a/metrics-jetty12-ee10/src/test/java/io/dropwizard/metrics/jetty12/ee10/InstrumentedEE10HandlerTest.java b/metrics-jetty12-ee10/src/test/java/io/dropwizard/metrics/jetty12/ee10/InstrumentedEE10HandlerTest.java new file mode 100644 index 0000000000..37ff03d654 --- /dev/null +++ b/metrics-jetty12-ee10/src/test/java/io/dropwizard/metrics/jetty12/ee10/InstrumentedEE10HandlerTest.java @@ -0,0 +1,251 @@ +package io.dropwizard.metrics.jetty12.ee10; + +import com.codahale.metrics.MetricRegistry; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.ee10.servlet.ServletHandler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.Callback; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +import static com.codahale.metrics.annotation.ResponseMeteredLevel.ALL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +public class InstrumentedEE10HandlerTest { + private final HttpClient client = new HttpClient(); + private final MetricRegistry registry = new MetricRegistry(); + private final Server server = new Server(); + private final ServerConnector connector = new ServerConnector(server); + private final InstrumentedEE10Handler handler = new InstrumentedEE10Handler(registry, null, ALL); + + @Before + public void setUp() throws Exception { + handler.setName("handler"); + + TestHandler testHandler = new TestHandler(); + // a servlet handler needs a servlet mapping, else the request will be short-circuited + // so use the DefaultServlet here + testHandler.addServletWithMapping(DefaultServlet.class, "/"); + + // builds the following handler chain: + // ServletContextHandler -> InstrumentedHandler -> TestHandler + // the ServletContextHandler is needed to utilize servlet related classes + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setHandler(testHandler); + servletContextHandler.insertHandler(handler); + server.setHandler(servletContextHandler); + + server.addConnector(connector); + server.start(); + client.start(); + } + + @After + public void tearDown() throws Exception { + server.stop(); + client.stop(); + } + + @Test + public void hasAName() throws Exception { + assertThat(handler.getName()) + .isEqualTo("handler"); + } + + @Test + public void createsAndRemovesMetricsForTheHandler() throws Exception { + final ContentResponse response = client.GET(uri("/hello")); + + assertThat(response.getStatus()) + .isEqualTo(404); + + assertThat(registry.getNames()) + .containsOnly( + MetricRegistry.name(TestHandler.class, "handler.1xx-responses"), + MetricRegistry.name(TestHandler.class, "handler.2xx-responses"), + MetricRegistry.name(TestHandler.class, "handler.3xx-responses"), + MetricRegistry.name(TestHandler.class, "handler.4xx-responses"), + MetricRegistry.name(TestHandler.class, "handler.404-responses"), + MetricRegistry.name(TestHandler.class, "handler.5xx-responses"), + MetricRegistry.name(TestHandler.class, "handler.percent-4xx-1m"), + MetricRegistry.name(TestHandler.class, "handler.percent-4xx-5m"), + MetricRegistry.name(TestHandler.class, "handler.percent-4xx-15m"), + MetricRegistry.name(TestHandler.class, "handler.percent-5xx-1m"), + MetricRegistry.name(TestHandler.class, "handler.percent-5xx-5m"), + MetricRegistry.name(TestHandler.class, "handler.percent-5xx-15m"), + MetricRegistry.name(TestHandler.class, "handler.requests"), + MetricRegistry.name(TestHandler.class, "handler.active-suspended"), + MetricRegistry.name(TestHandler.class, "handler.async-dispatches"), + MetricRegistry.name(TestHandler.class, "handler.async-timeouts"), + MetricRegistry.name(TestHandler.class, "handler.get-requests"), + MetricRegistry.name(TestHandler.class, "handler.put-requests"), + MetricRegistry.name(TestHandler.class, "handler.active-dispatches"), + MetricRegistry.name(TestHandler.class, "handler.trace-requests"), + MetricRegistry.name(TestHandler.class, "handler.other-requests"), + MetricRegistry.name(TestHandler.class, "handler.connect-requests"), + MetricRegistry.name(TestHandler.class, "handler.dispatches"), + MetricRegistry.name(TestHandler.class, "handler.head-requests"), + MetricRegistry.name(TestHandler.class, "handler.post-requests"), + MetricRegistry.name(TestHandler.class, "handler.options-requests"), + MetricRegistry.name(TestHandler.class, "handler.active-requests"), + MetricRegistry.name(TestHandler.class, "handler.delete-requests"), + MetricRegistry.name(TestHandler.class, "handler.move-requests") + ); + + server.stop(); + + assertThat(registry.getNames()) + .isEmpty(); + } + + @Test + public void responseTimesAreRecordedForBlockingResponses() throws Exception { + + final ContentResponse response = client.GET(uri("/blocking")); + + assertThat(response.getStatus()) + .isEqualTo(200); + + assertResponseTimesValid(); + } + + @Test + public void doStopDoesNotThrowNPE() throws Exception { + InstrumentedEE10Handler handler = new InstrumentedEE10Handler(registry, null, ALL); + handler.setHandler(new TestHandler()); + + assertThatCode(handler::doStop).doesNotThrowAnyException(); + } + + @Test + @Ignore("flaky on virtual machines") + public void responseTimesAreRecordedForAsyncResponses() throws Exception { + + final ContentResponse response = client.GET(uri("/async")); + + assertThat(response.getStatus()) + .isEqualTo(200); + + assertResponseTimesValid(); + } + + private void assertResponseTimesValid() { + assertThat(registry.getMeters().get(metricName() + ".2xx-responses") + .getCount()).isGreaterThan(0L); + assertThat(registry.getMeters().get(metricName() + ".200-responses") + .getCount()).isGreaterThan(0L); + + + assertThat(registry.getTimers().get(metricName() + ".get-requests") + .getSnapshot().getMedian()).isGreaterThan(0.0).isLessThan(TimeUnit.SECONDS.toNanos(1)); + + assertThat(registry.getTimers().get(metricName() + ".requests") + .getSnapshot().getMedian()).isGreaterThan(0.0).isLessThan(TimeUnit.SECONDS.toNanos(1)); + } + + private String uri(String path) { + return "http://localhost:" + connector.getLocalPort() + path; + } + + private String metricName() { + return MetricRegistry.name(TestHandler.class.getName(), "handler"); + } + + /** + * test handler. + *

+ * Supports + *

+ * /blocking - uses the standard servlet api + * /async - uses the 3.1 async api to complete the request + *

+ * all other requests will return 404 + */ + private static class TestHandler extends ServletHandler { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class); + if (servletContextRequest == null) { + return false; + } + + HttpServletRequest httpServletRequest = servletContextRequest.getServletApiRequest(); + HttpServletResponse httpServletResponse = servletContextRequest.getHttpServletResponse(); + + String path = request.getHttpURI().getPath(); + switch (path) { + case "/blocking": + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + httpServletResponse.setStatus(200); + httpServletResponse.setContentType("text/plain"); + httpServletResponse.getWriter().write("some content from the blocking request\n"); + callback.succeeded(); + return true; + case "/async": + servletContextRequest.getState().handling(); + final AsyncContext context = httpServletRequest.startAsync(); + Thread t = new Thread(() -> { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + httpServletResponse.setStatus(200); + httpServletResponse.setContentType("text/plain"); + final ServletOutputStream servletOutputStream; + try { + servletOutputStream = httpServletResponse.getOutputStream(); + servletOutputStream.setWriteListener( + new WriteListener() { + @Override + public void onWritePossible() throws IOException { + servletOutputStream.write("some content from the async\n" + .getBytes(StandardCharsets.UTF_8)); + context.complete(); + servletContextRequest.getServletChannel().sendResponseAndComplete(); + } + + @Override + public void onError(Throwable throwable) { + context.complete(); + servletContextRequest.getServletChannel().sendResponseAndComplete(); + } + } + ); + servletContextRequest.getHttpOutput().run(); + } catch (IOException e) { + context.complete(); + servletContextRequest.getServletChannel().sendResponseAndComplete(); + } + }); + t.start(); + return true; + default: + return false; + } + } + } +} diff --git a/metrics-jetty12/pom.xml b/metrics-jetty12/pom.xml new file mode 100644 index 0000000000..1f31705e72 --- /dev/null +++ b/metrics-jetty12/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + + io.dropwizard.metrics + metrics-parent + 4.2.20-SNAPSHOT + + + metrics-jetty12 + Metrics Integration for Jetty 12.x and higher + bundle + + A set of extensions for Jetty 12.x and higher which provide instrumentation of thread pools, connector + metrics, and application latency and utilization. + + + + io.dropwizard.metrics.jetty12 + + 17 + + 2.0.7 + + + + + + io.dropwizard.metrics + metrics-bom + ${project.version} + pom + import + + + org.eclipse.jetty + jetty-bom + ${jetty12.version} + pom + import + + + org.slf4j + slf4j-api + ${slf4j.version} + + + net.bytebuddy + byte-buddy + ${byte-buddy.version} + + + + + + + io.dropwizard.metrics + metrics-core + + + io.dropwizard.metrics + metrics-annotation + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-http + + + org.eclipse.jetty + jetty-io + + + org.eclipse.jetty + jetty-util + + + org.slf4j + slf4j-api + ${slf4j.version} + runtime + + + junit + junit + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.eclipse.jetty + jetty-client + test + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + diff --git a/metrics-jetty12/src/main/java/io/dropwizard/metrics/jetty12/AbstractInstrumentedHandler.java b/metrics-jetty12/src/main/java/io/dropwizard/metrics/jetty12/AbstractInstrumentedHandler.java new file mode 100644 index 0000000000..9cbaeac725 --- /dev/null +++ b/metrics-jetty12/src/main/java/io/dropwizard/metrics/jetty12/AbstractInstrumentedHandler.java @@ -0,0 +1,336 @@ +package io.dropwizard.metrics.jetty12; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.RatioGauge; +import com.codahale.metrics.Timer; +import com.codahale.metrics.annotation.ResponseMeteredLevel; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import static com.codahale.metrics.MetricRegistry.name; +import static com.codahale.metrics.annotation.ResponseMeteredLevel.ALL; +import static com.codahale.metrics.annotation.ResponseMeteredLevel.COARSE; +import static com.codahale.metrics.annotation.ResponseMeteredLevel.DETAILED; + +/** + * An abstract base class of a Jetty {@link Handler} which records various metrics about an underlying {@link Handler} + * instance. + */ +public abstract class AbstractInstrumentedHandler extends Handler.Wrapper { + protected static final String NAME_REQUESTS = "requests"; + protected static final String NAME_DISPATCHES = "dispatches"; + protected static final String NAME_ACTIVE_REQUESTS = "active-requests"; + protected static final String NAME_ACTIVE_DISPATCHES = "active-dispatches"; + protected static final String NAME_ACTIVE_SUSPENDED = "active-suspended"; + protected static final String NAME_ASYNC_DISPATCHES = "async-dispatches"; + protected static final String NAME_ASYNC_TIMEOUTS = "async-timeouts"; + protected static final String NAME_1XX_RESPONSES = "1xx-responses"; + protected static final String NAME_2XX_RESPONSES = "2xx-responses"; + protected static final String NAME_3XX_RESPONSES = "3xx-responses"; + protected static final String NAME_4XX_RESPONSES = "4xx-responses"; + protected static final String NAME_5XX_RESPONSES = "5xx-responses"; + protected static final String NAME_GET_REQUESTS = "get-requests"; + protected static final String NAME_POST_REQUESTS = "post-requests"; + protected static final String NAME_HEAD_REQUESTS = "head-requests"; + protected static final String NAME_PUT_REQUESTS = "put-requests"; + protected static final String NAME_DELETE_REQUESTS = "delete-requests"; + protected static final String NAME_OPTIONS_REQUESTS = "options-requests"; + protected static final String NAME_TRACE_REQUESTS = "trace-requests"; + protected static final String NAME_CONNECT_REQUESTS = "connect-requests"; + protected static final String NAME_MOVE_REQUESTS = "move-requests"; + protected static final String NAME_OTHER_REQUESTS = "other-requests"; + protected static final String NAME_PERCENT_4XX_1M = "percent-4xx-1m"; + protected static final String NAME_PERCENT_4XX_5M = "percent-4xx-5m"; + protected static final String NAME_PERCENT_4XX_15M = "percent-4xx-15m"; + protected static final String NAME_PERCENT_5XX_1M = "percent-5xx-1m"; + protected static final String NAME_PERCENT_5XX_5M = "percent-5xx-5m"; + protected static final String NAME_PERCENT_5XX_15M = "percent-5xx-15m"; + protected static final Set COARSE_METER_LEVELS = EnumSet.of(COARSE, ALL); + protected static final Set DETAILED_METER_LEVELS = EnumSet.of(DETAILED, ALL); + + protected final MetricRegistry metricRegistry; + + private String name; + protected final String prefix; + + // the requests handled by this handler, excluding active + protected Timer requests; + + // the number of dispatches seen by this handler, excluding active + protected Timer dispatches; + + // the number of active requests + protected Counter activeRequests; + + // the number of active dispatches + protected Counter activeDispatches; + + // the number of requests currently suspended. + protected Counter activeSuspended; + + // the number of requests that have been asynchronously dispatched + protected Meter asyncDispatches; + + // the number of requests that expired while suspended + protected Meter asyncTimeouts; + + protected final ResponseMeteredLevel responseMeteredLevel; + protected List responses; + protected Map responseCodeMeters; + + protected Timer getRequests; + protected Timer postRequests; + protected Timer headRequests; + protected Timer putRequests; + protected Timer deleteRequests; + protected Timer optionsRequests; + protected Timer traceRequests; + protected Timer connectRequests; + protected Timer moveRequests; + protected Timer otherRequests; + + /** + * Create a new instrumented handler using a given metrics registry. + * + * @param registry the registry for the metrics + */ + protected AbstractInstrumentedHandler(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 + */ + protected AbstractInstrumentedHandler(MetricRegistry registry, String prefix) { + this(registry, prefix, COARSE); + } + + /** + * 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 + * @param responseMeteredLevel the level to determine individual/aggregate response codes that are instrumented + */ + protected AbstractInstrumentedHandler(MetricRegistry registry, String prefix, ResponseMeteredLevel responseMeteredLevel) { + this.responseMeteredLevel = responseMeteredLevel; + 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 String prefix = getMetricPrefix(); + + this.requests = metricRegistry.timer(name(prefix, NAME_REQUESTS)); + this.dispatches = metricRegistry.timer(name(prefix, NAME_DISPATCHES)); + + this.activeRequests = metricRegistry.counter(name(prefix, NAME_ACTIVE_REQUESTS)); + this.activeDispatches = metricRegistry.counter(name(prefix, NAME_ACTIVE_DISPATCHES)); + this.activeSuspended = metricRegistry.counter(name(prefix, NAME_ACTIVE_SUSPENDED)); + + this.asyncDispatches = metricRegistry.meter(name(prefix, NAME_ASYNC_DISPATCHES)); + this.asyncTimeouts = metricRegistry.meter(name(prefix, NAME_ASYNC_TIMEOUTS)); + + this.responseCodeMeters = DETAILED_METER_LEVELS.contains(responseMeteredLevel) ? new ConcurrentHashMap<>() : Collections.emptyMap(); + this.responses = COARSE_METER_LEVELS.contains(responseMeteredLevel) ? + Collections.unmodifiableList(Arrays.asList( + metricRegistry.meter(name(prefix, NAME_1XX_RESPONSES)), // 1xx + metricRegistry.meter(name(prefix, NAME_2XX_RESPONSES)), // 2xx + metricRegistry.meter(name(prefix, NAME_3XX_RESPONSES)), // 3xx + metricRegistry.meter(name(prefix, NAME_4XX_RESPONSES)), // 4xx + metricRegistry.meter(name(prefix, NAME_5XX_RESPONSES)) // 5xx + )) : Collections.emptyList(); + + this.getRequests = metricRegistry.timer(name(prefix, NAME_GET_REQUESTS)); + this.postRequests = metricRegistry.timer(name(prefix, NAME_POST_REQUESTS)); + this.headRequests = metricRegistry.timer(name(prefix, NAME_HEAD_REQUESTS)); + this.putRequests = metricRegistry.timer(name(prefix, NAME_PUT_REQUESTS)); + this.deleteRequests = metricRegistry.timer(name(prefix, NAME_DELETE_REQUESTS)); + this.optionsRequests = metricRegistry.timer(name(prefix, NAME_OPTIONS_REQUESTS)); + this.traceRequests = metricRegistry.timer(name(prefix, NAME_TRACE_REQUESTS)); + this.connectRequests = metricRegistry.timer(name(prefix, NAME_CONNECT_REQUESTS)); + this.moveRequests = metricRegistry.timer(name(prefix, NAME_MOVE_REQUESTS)); + this.otherRequests = metricRegistry.timer(name(prefix, NAME_OTHER_REQUESTS)); + + metricRegistry.register(name(prefix, NAME_PERCENT_4XX_1M), new RatioGauge() { + @Override + protected Ratio getRatio() { + return Ratio.of(responses.get(3).getOneMinuteRate(), + requests.getOneMinuteRate()); + } + }); + + metricRegistry.register(name(prefix, NAME_PERCENT_4XX_5M), new RatioGauge() { + @Override + protected Ratio getRatio() { + return Ratio.of(responses.get(3).getFiveMinuteRate(), + requests.getFiveMinuteRate()); + } + }); + + metricRegistry.register(name(prefix, NAME_PERCENT_4XX_15M), new RatioGauge() { + @Override + protected Ratio getRatio() { + return Ratio.of(responses.get(3).getFifteenMinuteRate(), + requests.getFifteenMinuteRate()); + } + }); + + metricRegistry.register(name(prefix, NAME_PERCENT_5XX_1M), new RatioGauge() { + @Override + protected Ratio getRatio() { + return Ratio.of(responses.get(4).getOneMinuteRate(), + requests.getOneMinuteRate()); + } + }); + + metricRegistry.register(name(prefix, NAME_PERCENT_5XX_5M), new RatioGauge() { + @Override + protected Ratio getRatio() { + return Ratio.of(responses.get(4).getFiveMinuteRate(), + requests.getFiveMinuteRate()); + } + }); + + metricRegistry.register(name(prefix, NAME_PERCENT_5XX_15M), new RatioGauge() { + @Override + public Ratio getRatio() { + return Ratio.of(responses.get(4).getFifteenMinuteRate(), + requests.getFifteenMinuteRate()); + } + }); + } + + @Override + protected void doStop() throws Exception { + final String prefix = getMetricPrefix(); + + metricRegistry.remove(name(prefix, NAME_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_DISPATCHES)); + metricRegistry.remove(name(prefix, NAME_ACTIVE_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_ACTIVE_DISPATCHES)); + metricRegistry.remove(name(prefix, NAME_ACTIVE_SUSPENDED)); + metricRegistry.remove(name(prefix, NAME_ASYNC_DISPATCHES)); + metricRegistry.remove(name(prefix, NAME_ASYNC_TIMEOUTS)); + metricRegistry.remove(name(prefix, NAME_1XX_RESPONSES)); + metricRegistry.remove(name(prefix, NAME_2XX_RESPONSES)); + metricRegistry.remove(name(prefix, NAME_3XX_RESPONSES)); + metricRegistry.remove(name(prefix, NAME_4XX_RESPONSES)); + metricRegistry.remove(name(prefix, NAME_5XX_RESPONSES)); + metricRegistry.remove(name(prefix, NAME_GET_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_POST_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_HEAD_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_PUT_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_DELETE_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_OPTIONS_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_TRACE_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_CONNECT_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_MOVE_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_OTHER_REQUESTS)); + metricRegistry.remove(name(prefix, NAME_PERCENT_4XX_1M)); + metricRegistry.remove(name(prefix, NAME_PERCENT_4XX_5M)); + metricRegistry.remove(name(prefix, NAME_PERCENT_4XX_15M)); + metricRegistry.remove(name(prefix, NAME_PERCENT_5XX_1M)); + metricRegistry.remove(name(prefix, NAME_PERCENT_5XX_5M)); + metricRegistry.remove(name(prefix, NAME_PERCENT_5XX_15M)); + + if (responseCodeMeters != null) { + responseCodeMeters.keySet().stream() + .map(sc -> name(getMetricPrefix(), String.format("%d-responses", sc))) + .forEach(metricRegistry::remove); + } + super.doStop(); + } + + protected 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; + } + } + } + + protected void updateResponses(Request request, Response response, long start, boolean isHandled) { + if (isHandled) { + mark(response.getStatus()); + } else { + mark(404);; // will end up with a 404 response sent by HttpChannel.handle + } + activeRequests.dec(); + final long elapsedTime = System.currentTimeMillis() - start; + requests.update(elapsedTime, TimeUnit.MILLISECONDS); + requestTimer(request.getMethod()).update(elapsedTime, TimeUnit.MILLISECONDS); + } + + protected void mark(int statusCode) { + if (DETAILED_METER_LEVELS.contains(responseMeteredLevel)) { + getResponseCodeMeter(statusCode).mark(); + } + + if (COARSE_METER_LEVELS.contains(responseMeteredLevel)) { + final int responseStatus = statusCode / 100; + if (responseStatus >= 1 && responseStatus <= 5) { + responses.get(responseStatus - 1).mark(); + } + } + } + + protected Meter getResponseCodeMeter(int statusCode) { + return responseCodeMeters + .computeIfAbsent(statusCode, sc -> metricRegistry + .meter(name(getMetricPrefix(), String.format("%d-responses", sc)))); + } + + protected String getMetricPrefix() { + return this.prefix == null ? name(getHandler().getClass(), name) : name(this.prefix, name); + } +} diff --git a/metrics-jetty12/src/main/java/io/dropwizard/metrics/jetty12/InstrumentedConnectionFactory.java b/metrics-jetty12/src/main/java/io/dropwizard/metrics/jetty12/InstrumentedConnectionFactory.java new file mode 100644 index 0000000000..679d310f4f --- /dev/null +++ b/metrics-jetty12/src/main/java/io/dropwizard/metrics/jetty12/InstrumentedConnectionFactory.java @@ -0,0 +1,63 @@ +package io.dropwizard.metrics.jetty12; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Timer; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.util.component.ContainerLifeCycle; + +import java.util.List; + +public class InstrumentedConnectionFactory extends ContainerLifeCycle implements ConnectionFactory { + private final ConnectionFactory connectionFactory; + private final Timer timer; + private final Counter counter; + + public InstrumentedConnectionFactory(ConnectionFactory connectionFactory, Timer timer) { + this(connectionFactory, timer, null); + } + + public InstrumentedConnectionFactory(ConnectionFactory connectionFactory, Timer timer, Counter counter) { + this.connectionFactory = connectionFactory; + this.timer = timer; + this.counter = counter; + addBean(connectionFactory); + } + + @Override + public String getProtocol() { + return connectionFactory.getProtocol(); + } + + @Override + public List getProtocols() { + return connectionFactory.getProtocols(); + } + + @Override + public Connection newConnection(Connector connector, EndPoint endPoint) { + final Connection connection = connectionFactory.newConnection(connector, endPoint); + connection.addEventListener(new Connection.Listener() { + private Timer.Context context; + + @Override + public void onOpened(Connection connection) { + this.context = timer.time(); + if (counter != null) { + counter.inc(); + } + } + + @Override + public void onClosed(Connection connection) { + context.stop(); + if (counter != null) { + counter.dec(); + } + } + }); + return connection; + } +} diff --git a/metrics-jetty12/src/main/java/io/dropwizard/metrics/jetty12/InstrumentedQueuedThreadPool.java b/metrics-jetty12/src/main/java/io/dropwizard/metrics/jetty12/InstrumentedQueuedThreadPool.java new file mode 100644 index 0000000000..cfcccb0b7f --- /dev/null +++ b/metrics-jetty12/src/main/java/io/dropwizard/metrics/jetty12/InstrumentedQueuedThreadPool.java @@ -0,0 +1,159 @@ +package io.dropwizard.metrics.jetty12; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.RatioGauge; +import org.eclipse.jetty.util.annotation.Name; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadFactory; + +import static com.codahale.metrics.MetricRegistry.name; + +public class InstrumentedQueuedThreadPool extends QueuedThreadPool { + private static final String NAME_UTILIZATION = "utilization"; + private static final String NAME_UTILIZATION_MAX = "utilization-max"; + private static final String NAME_SIZE = "size"; + private static final String NAME_JOBS = "jobs"; + private static final String NAME_JOBS_QUEUE_UTILIZATION = "jobs-queue-utilization"; + + private final MetricRegistry metricRegistry; + private String prefix; + + public InstrumentedQueuedThreadPool(@Name("registry") MetricRegistry registry) { + this(registry, 200); + } + + public InstrumentedQueuedThreadPool(@Name("registry") MetricRegistry registry, + @Name("maxThreads") int maxThreads) { + this(registry, maxThreads, 8); + } + + public InstrumentedQueuedThreadPool(@Name("registry") MetricRegistry registry, + @Name("maxThreads") int maxThreads, + @Name("minThreads") int minThreads) { + this(registry, maxThreads, minThreads, 60000); + } + + public InstrumentedQueuedThreadPool(@Name("registry") MetricRegistry registry, + @Name("maxThreads") int maxThreads, + @Name("minThreads") int minThreads, + @Name("queue") BlockingQueue queue) { + this(registry, maxThreads, minThreads, 60000, queue); + } + + public InstrumentedQueuedThreadPool(@Name("registry") MetricRegistry registry, + @Name("maxThreads") int maxThreads, + @Name("minThreads") int minThreads, + @Name("idleTimeout") int idleTimeout) { + this(registry, maxThreads, minThreads, idleTimeout, null); + } + + public InstrumentedQueuedThreadPool(@Name("registry") MetricRegistry registry, + @Name("maxThreads") int maxThreads, + @Name("minThreads") int minThreads, + @Name("idleTimeout") int idleTimeout, + @Name("queue") BlockingQueue queue) { + this(registry, maxThreads, minThreads, idleTimeout, queue, null); + } + + public InstrumentedQueuedThreadPool(@Name("registry") MetricRegistry registry, + @Name("maxThreads") int maxThreads, + @Name("minThreads") int minThreads, + @Name("idleTimeout") int idleTimeout, + @Name("queue") BlockingQueue queue, + @Name("threadGroup") ThreadGroup threadGroup) { + this(registry, maxThreads, minThreads, idleTimeout, -1, queue, threadGroup); + } + + public InstrumentedQueuedThreadPool(@Name("registry") MetricRegistry registry, + @Name("maxThreads") int maxThreads, + @Name("minThreads") int minThreads, + @Name("idleTimeout") int idleTimeout, + @Name("reservedThreads") int reservedThreads, + @Name("queue") BlockingQueue queue, + @Name("threadGroup") ThreadGroup threadGroup) { + this(registry, maxThreads, minThreads, idleTimeout, reservedThreads, queue, threadGroup, null); + } + + public InstrumentedQueuedThreadPool(@Name("registry") MetricRegistry registry, + @Name("maxThreads") int maxThreads, + @Name("minThreads") int minThreads, + @Name("idleTimeout") int idleTimeout, + @Name("reservedThreads") int reservedThreads, + @Name("queue") BlockingQueue queue, + @Name("threadGroup") ThreadGroup threadGroup, + @Name("threadFactory") ThreadFactory threadFactory) { + this(registry, maxThreads, minThreads, idleTimeout, reservedThreads, queue, threadGroup, threadFactory, null); + } + + public InstrumentedQueuedThreadPool(@Name("registry") MetricRegistry registry, + @Name("maxThreads") int maxThreads, + @Name("minThreads") int minThreads, + @Name("idleTimeout") int idleTimeout, + @Name("reservedThreads") int reservedThreads, + @Name("queue") BlockingQueue queue, + @Name("threadGroup") ThreadGroup threadGroup, + @Name("threadFactory") ThreadFactory threadFactory, + @Name("prefix") String prefix) { + super(maxThreads, minThreads, idleTimeout, reservedThreads, queue, threadGroup, threadFactory); + this.metricRegistry = registry; + this.prefix = prefix; + } + + public String getPrefix() { + return prefix; + } + + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + @Override + protected void doStart() throws Exception { + super.doStart(); + + final String prefix = getMetricPrefix(); + + metricRegistry.register(name(prefix, NAME_UTILIZATION), new RatioGauge() { + @Override + protected Ratio getRatio() { + return Ratio.of(getThreads() - getIdleThreads(), getThreads()); + } + }); + metricRegistry.register(name(prefix, NAME_UTILIZATION_MAX), new RatioGauge() { + @Override + protected Ratio getRatio() { + return Ratio.of(getThreads() - getIdleThreads(), getMaxThreads()); + } + }); + metricRegistry.registerGauge(name(prefix, NAME_SIZE), this::getThreads); + // This assumes the QueuedThreadPool is using a BlockingArrayQueue or + // ArrayBlockingQueue for its queue, and is therefore a constant-time operation. + metricRegistry.registerGauge(name(prefix, NAME_JOBS), () -> getQueue().size()); + metricRegistry.register(name(prefix, NAME_JOBS_QUEUE_UTILIZATION), new RatioGauge() { + @Override + protected Ratio getRatio() { + BlockingQueue queue = getQueue(); + return Ratio.of(queue.size(), queue.size() + queue.remainingCapacity()); + } + }); + } + + @Override + protected void doStop() throws Exception { + final String prefix = getMetricPrefix(); + + metricRegistry.remove(name(prefix, NAME_UTILIZATION)); + metricRegistry.remove(name(prefix, NAME_UTILIZATION_MAX)); + metricRegistry.remove(name(prefix, NAME_SIZE)); + metricRegistry.remove(name(prefix, NAME_JOBS)); + metricRegistry.remove(name(prefix, NAME_JOBS_QUEUE_UTILIZATION)); + + super.doStop(); + } + + private String getMetricPrefix() { + return this.prefix == null ? name(QueuedThreadPool.class, getName()) : name(this.prefix, getName()); + } +} diff --git a/metrics-jetty12/src/test/java/io/dropwizard/metrics/jetty12/InstrumentedConnectionFactoryTest.java b/metrics-jetty12/src/test/java/io/dropwizard/metrics/jetty12/InstrumentedConnectionFactoryTest.java new file mode 100644 index 0000000000..a988de2de0 --- /dev/null +++ b/metrics-jetty12/src/test/java/io/dropwizard/metrics/jetty12/InstrumentedConnectionFactoryTest.java @@ -0,0 +1,86 @@ +package io.dropwizard.metrics.jetty12; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.Callback; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InstrumentedConnectionFactoryTest { + private final MetricRegistry registry = new MetricRegistry(); + private final Server server = new Server(); + private final ServerConnector connector = + new ServerConnector(server, new InstrumentedConnectionFactory(new HttpConnectionFactory(), + registry.timer("http.connections"), + registry.counter("http.active-connections"))); + private final HttpClient client = new HttpClient(); + + @Before + public void setUp() throws Exception { + server.setHandler(new Handler.Abstract() { + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + Content.Sink.write(response, true, "OK", callback); + return true; + } + }); + + server.addConnector(connector); + server.start(); + + client.start(); + } + + @After + public void tearDown() throws Exception { + server.stop(); + client.stop(); + } + + @Test + public void instrumentsConnectionTimes() throws Exception { + final ContentResponse response = client.GET("http://localhost:" + connector.getLocalPort() + "/hello"); + assertThat(response.getStatus()) + .isEqualTo(200); + + client.stop(); // close the connection + + Thread.sleep(100); // make sure the connection is closed + + final Timer timer = registry.timer(MetricRegistry.name("http.connections")); + assertThat(timer.getCount()) + .isEqualTo(1); + } + + @Test + public void instrumentsActiveConnections() throws Exception { + final Counter counter = registry.counter("http.active-connections"); + + final ContentResponse response = client.GET("http://localhost:" + connector.getLocalPort() + "/hello"); + assertThat(response.getStatus()) + .isEqualTo(200); + + assertThat(counter.getCount()) + .isEqualTo(1); + + client.stop(); // close the connection + + Thread.sleep(100); // make sure the connection is closed + + assertThat(counter.getCount()) + .isEqualTo(0); + } +} diff --git a/metrics-jetty12/src/test/java/io/dropwizard/metrics/jetty12/InstrumentedQueuedThreadPoolTest.java b/metrics-jetty12/src/test/java/io/dropwizard/metrics/jetty12/InstrumentedQueuedThreadPoolTest.java new file mode 100644 index 0000000000..5a4e4afcf3 --- /dev/null +++ b/metrics-jetty12/src/test/java/io/dropwizard/metrics/jetty12/InstrumentedQueuedThreadPoolTest.java @@ -0,0 +1,49 @@ +package io.dropwizard.metrics.jetty12; + +import com.codahale.metrics.MetricRegistry; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InstrumentedQueuedThreadPoolTest { + private static final String PREFIX = "prefix"; + + private MetricRegistry metricRegistry; + private InstrumentedQueuedThreadPool iqtp; + + @Before + public void setUp() { + metricRegistry = new MetricRegistry(); + iqtp = new InstrumentedQueuedThreadPool(metricRegistry); + } + + @Test + public void customMetricsPrefix() throws Exception { + iqtp.setPrefix(PREFIX); + iqtp.start(); + + assertThat(metricRegistry.getNames()) + .overridingErrorMessage("Custom metrics prefix doesn't match") + .allSatisfy(name -> assertThat(name).startsWith(PREFIX)); + + iqtp.stop(); + assertThat(metricRegistry.getMetrics()) + .overridingErrorMessage("The default metrics prefix was changed") + .isEmpty(); + } + + @Test + public void metricsPrefixBackwardCompatible() throws Exception { + iqtp.start(); + assertThat(metricRegistry.getNames()) + .overridingErrorMessage("The default metrics prefix was changed") + .allSatisfy(name -> assertThat(name).startsWith(QueuedThreadPool.class.getName())); + + iqtp.stop(); + assertThat(metricRegistry.getMetrics()) + .overridingErrorMessage("The default metrics prefix was changed") + .isEmpty(); + } +} diff --git a/pom.xml b/pom.xml index b795a445b9..7b7a324360 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,7 @@ 9.4.51.v20230217 10.0.15 11.0.15 + 12.0.0 1.7.36 3.24.2 1.14.6 @@ -69,6 +70,7 @@ 3.11.0 2.21.1 9+181-r4173-1 + 6.0.0 dropwizard_metrics dropwizard @@ -161,6 +163,10 @@ [17,) + + metrics-jetty12 + metrics-jetty12-ee10 +