asyncResponse = completableResponseListener.send();
+ assertThatNoException().isThrownBy(() -> {
+ await()
+ .atMost(750, TimeUnit.MILLISECONDS)
+ .until(() -> activeSuspended.getCount() == 1L);
+ asyncResponse.get();
+ });
+ assertThat(activeSuspended.getCount()).isEqualTo(0L);
+ }
+
+ @Test
+ public void testAsyncDispatches() throws Exception {
+ servletContextHandler.addFilter((request, response, chain) -> {
+ if (!(request instanceof HttpServletRequest)) {
+ throw new IllegalStateException("Expecting ServletRequest to be an instance of HttpServletRequest");
+ }
+ HttpServletRequest httpServletRequest = (HttpServletRequest) request;
+ if ("/".equals(httpServletRequest.getRequestURI())) {
+ AsyncContext asyncContext = request.startAsync();
+ asyncContext.dispatch("/dispatch");
+ return;
+ }
+ if ("/dispatch".equals(httpServletRequest.getRequestURI())) {
+ AsyncContext asyncContext = request.startAsync();
+ if (!(response instanceof HttpServletResponse)) {
+ throw new IllegalStateException("Expecting ServletResponse to be an instance of HttpServletResponse");
+ }
+ HttpServletResponse httpServletResponse = (HttpServletResponse) response;
+ httpServletResponse.setStatus(204);
+ asyncContext.complete();
+ return;
+ }
+ throw new UnsupportedOperationException("Only '/' and '/dispatch' are valid paths");
+ }, "/*", EnumSet.allOf(DispatcherType.class));
+
+ ContentResponse contentResponse = client.GET(uri("/"));
+ assertThat(contentResponse).isNotNull().extracting(Response::getStatus).isEqualTo(204);
+ Meter asyncDispatches = registry.meter(MetricRegistry.name(ServletHandler.class, "handler.async-dispatches"));
+ assertThat(asyncDispatches.getCount()).isEqualTo(1L);
+ }
+}
diff --git a/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/InstrumentedEE11HandlerTest.java b/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/InstrumentedEE11HandlerTest.java
new file mode 100644
index 0000000000..7bfddbc524
--- /dev/null
+++ b/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/InstrumentedEE11HandlerTest.java
@@ -0,0 +1,239 @@
+package io.dropwizard.metrics.jetty12.ee11;
+
+import com.codahale.metrics.MetricRegistry;
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.ee11.servlet.DefaultServlet;
+import org.eclipse.jetty.ee11.servlet.ServletContextRequest;
+import org.eclipse.jetty.ee11.servlet.ServletHandler;
+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 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 com.codahale.metrics.annotation.ResponseMeteredLevel.COARSE;
+import static com.codahale.metrics.annotation.ResponseMeteredLevel.DETAILED;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+public class InstrumentedEE11HandlerTest extends AbstractIntegrationTest {
+
+ @Override
+ protected Handler getHandler() {
+ InstrumentedEE11HandlerTest.TestHandler testHandler = new InstrumentedEE11HandlerTest.TestHandler();
+ // a servlet handler needs a servlet mapping, else the request will be short-circuited
+ // so use the DefaultServlet here
+ testHandler.addServletWithMapping(DefaultServlet.class, "/");
+ return testHandler;
+ }
+
+ @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
+ @Ignore("flaky on virtual machines")
+ public void responseTimesAreRecordedForBlockingResponses() throws Exception {
+
+ final ContentResponse response = client.GET(uri("/blocking"));
+
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+
+ assertResponseTimesValid();
+ }
+
+ @Test
+ public void doStopDoesNotThrowNPE() throws Exception {
+ InstrumentedEE11Handler handler = new InstrumentedEE11Handler(registry, null, ALL);
+ handler.setHandler(new TestHandler());
+
+ assertThatCode(handler::doStop).doesNotThrowAnyException();
+ }
+
+ @Test
+ public void gaugesAreRegisteredWithResponseMeteredLevelCoarse() throws Exception {
+ InstrumentedEE11Handler handler = new InstrumentedEE11Handler(registry, "coarse", COARSE);
+ handler.setHandler(new TestHandler());
+ handler.setName("handler");
+ handler.doStart();
+ assertThat(registry.getGauges()).containsKey("coarse.handler.percent-4xx-1m");
+ }
+
+ @Test
+ public void gaugesAreNotRegisteredWithResponseMeteredLevelDetailed() throws Exception {
+ InstrumentedEE11Handler handler = new InstrumentedEE11Handler(registry, "detailed", DETAILED);
+ handler.setHandler(new TestHandler());
+ handler.setName("handler");
+ handler.doStart();
+ assertThat(registry.getGauges()).doesNotContainKey("coarse.handler.percent-4xx-1m");
+ }
+
+ @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 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().handle();
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ context.complete();
+ servletContextRequest.getServletChannel().handle();
+ }
+ }
+ );
+ servletContextRequest.getHttpOutput().writeCallback();
+ } catch (IOException e) {
+ context.complete();
+ servletContextRequest.getServletChannel().handle();
+ }
+ });
+ t.start();
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+}
diff --git a/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/ResponseStatusTest.java b/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/ResponseStatusTest.java
new file mode 100644
index 0000000000..8b57cf7ba2
--- /dev/null
+++ b/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/ResponseStatusTest.java
@@ -0,0 +1,60 @@
+package io.dropwizard.metrics.jetty12.ee11;
+
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import org.eclipse.jetty.client.ContentResponse;
+import org.eclipse.jetty.client.StringRequestContent;
+import org.eclipse.jetty.ee11.servlet.DefaultServlet;
+import org.eclipse.jetty.ee11.servlet.ServletHandler;
+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 org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ResponseStatusTest extends AbstractIntegrationTest {
+
+ @Override
+ protected Handler getHandler() {
+ ServletHandler servletHandler = new ResponseStatusHandler();
+ servletHandler.addServletWithMapping(DefaultServlet.class, "/");
+ return servletHandler;
+ }
+
+ @Test
+ public void testResponseCodes() throws Exception {
+
+ for (int i = 2; i <= 5; i++) {
+ String status = String.format("%d00", i);
+ ContentResponse contentResponse = client.POST(uri("/"))
+ .body(new StringRequestContent(status))
+ .headers(headers -> headers.add("Content-Type", "text/plain"))
+ .send();
+ assertThat(contentResponse).isNotNull().satisfies(response ->
+ assertThat(response.getStatus()).hasToString(status));
+
+ Meter meter = registry.meter(MetricRegistry.name(ResponseStatusHandler.class, String.format("handler.%dxx-responses", i)));
+ assertThat(meter.getCount()).isEqualTo(1L);
+ }
+ }
+
+ private static class ResponseStatusHandler extends ServletHandler {
+ @Override
+ public boolean handle(Request request, Response response, Callback callback) throws Exception {
+ try (InputStream inputStream = Request.asInputStream(request);
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
+ String status = bufferedReader.readLine();
+ int statusCode = Integer.parseInt(status);
+ response.setStatus(statusCode);
+ callback.succeeded();
+ return true;
+ }
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 589c17ace6..201b959162 100644
--- a/pom.xml
+++ b/pom.xml
@@ -161,6 +161,7 @@
metrics-jetty12
metrics-jetty12-ee10
+ metrics-jetty12-ee11