diff --git a/metrics-bom/pom.xml b/metrics-bom/pom.xml index 8639cff93e..f29d73fb53 100644 --- a/metrics-bom/pom.xml +++ b/metrics-bom/pom.xml @@ -140,6 +140,11 @@ metrics-jetty12-ee10 ${project.version} + + io.dropwizard.metrics + metrics-jetty12-ee11 + ${project.version} + io.dropwizard.metrics metrics-jmx diff --git a/metrics-jetty12-ee11/pom.xml b/metrics-jetty12-ee11/pom.xml new file mode 100644 index 0000000000..24bf88ecfd --- /dev/null +++ b/metrics-jetty12-ee11/pom.xml @@ -0,0 +1,145 @@ + + + 4.0.0 + + + io.dropwizard.metrics + metrics-parent + 4.2.34-SNAPSHOT + + + metrics-jetty12-ee11 + Metrics Integration for Jetty 12.x and higher with Jakarta EE 11 + 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 11. + + + + io.dropwizard.metrics.jetty12.ee11 + + 17 + + 2.0.17 + + + + + + io.dropwizard.metrics + metrics-bom + ${project.version} + pom + import + + + org.eclipse.jetty + jetty-bom + ${jetty12.version} + pom + import + + + org.eclipse.jetty.ee11 + jetty-ee11-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 + provided + + + org.eclipse.jetty.ee11 + jetty-ee11-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 + + + org.awaitility + awaitility + ${awaitility.version} + test + + + diff --git a/metrics-jetty12-ee11/src/main/java/io/dropwizard/metrics/jetty12/ee11/InstrumentedEE11Handler.java b/metrics-jetty12-ee11/src/main/java/io/dropwizard/metrics/jetty12/ee11/InstrumentedEE11Handler.java new file mode 100644 index 0000000000..dc94893fb8 --- /dev/null +++ b/metrics-jetty12-ee11/src/main/java/io/dropwizard/metrics/jetty12/ee11/InstrumentedEE11Handler.java @@ -0,0 +1,154 @@ +package io.dropwizard.metrics.jetty12.ee11; + +import com.codahale.metrics.Meter; +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 jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; +import org.eclipse.jetty.ee11.servlet.ServletApiRequest; +import org.eclipse.jetty.ee11.servlet.ServletChannelState; +import org.eclipse.jetty.ee11.servlet.ServletContextHandler; +import org.eclipse.jetty.ee11.servlet.ServletContextRequest; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; + +import java.io.IOException; + +/** + * A Jetty {@link Handler} which records various metrics about an underlying {@link Handler} + * instance. This {@link Handler} requires a {@link org.eclipse.jetty.ee11.servlet.ServletContextHandler} to be present. + * For correct behaviour, the {@link org.eclipse.jetty.ee11.servlet.ServletContextHandler} should be before this handler + * in the handler chain. To achieve this, one can use + * {@link org.eclipse.jetty.ee11.servlet.ServletContextHandler#insertHandler(Singleton)}. + */ +public class InstrumentedEE11Handler extends AbstractInstrumentedHandler { + private AsyncDispatchesAwareServletRequestListener asyncDispatchesAwareServletRequestListener; + + /** + * Create a new instrumented handler using a given metrics registry. + * + * @param registry the registry for the metrics + */ + public InstrumentedEE11Handler(MetricRegistry registry) { + super(registry); + } + + /** + * 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 InstrumentedEE11Handler(MetricRegistry registry, String prefix) { + super(registry, prefix); + } + + /** + * 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 InstrumentedEE11Handler(MetricRegistry registry, String prefix, ResponseMeteredLevel responseMeteredLevel) { + super(registry, prefix, responseMeteredLevel); + } + + @Override + protected void doStart() throws Exception { + super.doStart(); + asyncDispatchesAwareServletRequestListener = new AsyncDispatchesAwareServletRequestListener(getAsyncDispatches()); + } + + @Override + protected void doStop() throws Exception { + super.doStop(); + } + + @Override + protected void setupServletListeners(Request request, Response response) { + ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class); + if (servletContextRequest == null) { + return; + } + + ServletChannelState servletChannelState = servletContextRequest.getServletRequestState(); + // the ServletChannelState gets recycled after handling, so add a new listener for every request + servletChannelState.addListener(new InstrumentedAsyncListener(getAsyncTimeouts())); + + ServletContextHandler servletContextHandler = servletContextRequest.getServletContextHandler(); + // addEventListener checks for duplicates, so we can try to add the listener for every request + servletContextHandler.addEventListener(asyncDispatchesAwareServletRequestListener); + } + + @Override + protected boolean isSuspended(Request request, Response response) { + ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class); + if (servletContextRequest == null) { + return false; + } + + ServletChannelState servletChannelState = servletContextRequest.getServletRequestState(); + if (servletChannelState == null) { + return false; + } + + return servletChannelState.isSuspended(); + } + + private static class AsyncDispatchesAwareServletRequestListener implements ServletRequestListener { + private final Meter asyncDispatches; + + private AsyncDispatchesAwareServletRequestListener(Meter asyncDispatches) { + this.asyncDispatches = asyncDispatches; + } + + @Override + public void requestInitialized(ServletRequestEvent sre) { + ServletRequest servletRequest = sre.getServletRequest(); + if (!(servletRequest instanceof ServletApiRequest)) { + return; + } + + ServletApiRequest servletApiRequest = (ServletApiRequest) servletRequest; + + ServletContextHandler.ServletRequestInfo servletRequestInfo = servletApiRequest.getServletRequestInfo(); + + ServletChannelState servletChannelState = servletRequestInfo.getServletRequestState(); + + // if the request isn't 'initial', the request was re-dispatched + if (servletChannelState.isAsync() && !servletChannelState.isInitial()) { + asyncDispatches.mark(); + } + } + } + + private static class InstrumentedAsyncListener implements AsyncListener { + private final Meter asyncTimeouts; + + private InstrumentedAsyncListener(Meter asyncTimeouts) { + this.asyncTimeouts = asyncTimeouts; + } + + @Override + public void onComplete(AsyncEvent event) throws IOException {} + + @Override + public void onTimeout(AsyncEvent event) throws IOException { + asyncTimeouts.mark(); + } + + @Override + public void onError(AsyncEvent event) throws IOException {} + + @Override + public void onStartAsync(AsyncEvent event) throws IOException { + event.getAsyncContext().addListener(this); + } + } +} diff --git a/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/AbstractIntegrationTest.java b/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/AbstractIntegrationTest.java new file mode 100644 index 0000000000..9c8782c66c --- /dev/null +++ b/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/AbstractIntegrationTest.java @@ -0,0 +1,50 @@ +package io.dropwizard.metrics.jetty12.ee11; + +import com.codahale.metrics.MetricRegistry; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.ee11.servlet.ServletContextHandler; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.junit.After; +import org.junit.Before; + +import static com.codahale.metrics.annotation.ResponseMeteredLevel.ALL; + +abstract class AbstractIntegrationTest { + + protected final HttpClient client = new HttpClient(); + protected final MetricRegistry registry = new MetricRegistry(); + protected final Server server = new Server(); + protected final ServerConnector connector = new ServerConnector(server); + protected final InstrumentedEE11Handler handler = new InstrumentedEE11Handler(registry, null, ALL); + protected final ServletContextHandler servletContextHandler = new ServletContextHandler(); + + @Before + public void setUp() throws Exception { + handler.setName("handler"); + + // builds the following handler chain: + // ServletContextHandler -> InstrumentedHandler -> TestHandler + // the ServletContextHandler is needed to utilize servlet related classes + servletContextHandler.setHandler(getHandler()); + servletContextHandler.insertHandler(handler); + server.setHandler(servletContextHandler); + + server.addConnector(connector); + server.start(); + client.start(); + } + + @After + public void tearDown() throws Exception { + server.stop(); + client.stop(); + } + + protected String uri(String path) { + return "http://localhost:" + connector.getLocalPort() + path; + } + + protected abstract Handler getHandler(); +} diff --git a/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/AsyncTest.java b/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/AsyncTest.java new file mode 100644 index 0000000000..4b672354fe --- /dev/null +++ b/metrics-jetty12-ee11/src/test/java/io/dropwizard/metrics/jetty12/ee11/AsyncTest.java @@ -0,0 +1,105 @@ +package io.dropwizard.metrics.jetty12.ee11; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.client.CompletableResponseListener; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.Request; +import org.eclipse.jetty.client.Response; +import org.eclipse.jetty.ee11.servlet.ServletHandler; +import org.eclipse.jetty.server.Handler; +import org.junit.Test; + +import java.util.EnumSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.awaitility.Awaitility.await; + +public class AsyncTest extends AbstractIntegrationTest { + + @Override + protected Handler getHandler() { + return new ServletHandler(); + } + + @Test + public void testAsyncTimeout() throws Exception { + servletContextHandler.addFilter((request, response, chain) -> { + AsyncContext asyncContext = request.startAsync(); + asyncContext.setTimeout(1); + }, "/*", EnumSet.allOf(DispatcherType.class)); + + client.GET(uri("/")); + Meter asyncTimeouts = registry.meter(MetricRegistry.name(ServletHandler.class, "handler.async-timeouts")); + assertThat(asyncTimeouts.getCount()).isEqualTo(1L); + + client.GET(uri("/")); + assertThat(asyncTimeouts.getCount()).isEqualTo(2L); + } + + @Test + public void testActiveSuspended() { + servletContextHandler.addFilter((request, response, chain) -> { + AsyncContext asyncContext = request.startAsync(); + asyncContext.start(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + } + asyncContext.complete(); + }); + }, "/*", EnumSet.allOf(DispatcherType.class)); + + Counter activeSuspended = registry.counter(MetricRegistry.name(ServletHandler.class, "handler.active-suspended")); + Request request = client.POST(uri("/")); + CompletableResponseListener completableResponseListener = new CompletableResponseListener(request); + CompletableFuture 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