Skip to content

InstrumentedEE10Handler not recording metrics on Jetty 12 with Jersey CompletableFuture #3917

@mihalyr

Description

@mihalyr

Hi,

I upgraded from Jetty 11 to 12 and now I have to use InstrumentedEE10Handler for recording request metrics. I'm running into a problem with this setup where the active request start is recorded, but the request finish is never and thus most metrics aren't being collected.

Could you please take a look at the below minimal reproducer to check if I'm doing something wrong? Please note that this used to work with Jetty 11 before with InstrumentedHttpChannelListener, but that class is not available anymore for Jetty 12 and I have to use InstrumentedEE10Handler which I cannot figure out how to configure properly.

Please consider this minimal reproducer with two classes, a Jersey resource and a JUnit 5 test class (I am using JDK21 locally on Linux - Fedora 38):

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.concurrent.CompletableFuture;

@Path("")
public class PingResource {
  @GET
  public CompletableFuture<String> ping() {
    return CompletableFuture.completedFuture("pong");
  }
}
import static com.codahale.metrics.MetricRegistry.name;
import static org.junit.jupiter.api.Assertions.assertEquals;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import io.dropwizard.metrics.jetty12.ee10.InstrumentedEE10Handler;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHandler;
import org.eclipse.jetty.server.Server;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.junit.jupiter.api.Test;

class Jetty12DropwizardTest {
  @Test
  void test() throws Exception {
    // Jetty Server on random free port
    var server = new Server(0);

    // Setup Jersey with our simple async resource returning CompletableFuture
    var jerseyConfig = new ResourceConfig()
        .registerClasses(PingResource.class);
    var jersey = new ServletContainer(jerseyConfig);

    // Configure the handler
    var contextHandler = new ServletContextHandler();
    contextHandler.setContextPath("/");
    contextHandler.addServlet(jersey, "/*");

    // Add handler instrumentation
    var metrics = new MetricRegistry();
    var instrumentedHandler = new InstrumentedEE10Handler(metrics);
    contextHandler.insertHandler(instrumentedHandler);

    // Tell the server to use our handler and start it
    server.setHandler(contextHandler);
    server.start();
    try (var client = HttpClient.newHttpClient()) {
      // Ping the server and wait for the response
      var response = client.send(
          HttpRequest.newBuilder().uri(server.getURI()).GET().build(),
          HttpResponse.BodyHandlers.ofString());
      assertEquals(200, response.statusCode(), "response code");
      assertEquals("pong", response.body(), "response body");

      // Print metric counts
      metrics.getMetrics().forEach((name, metric) -> System.out.println(name + ": " + printCount(metric)));

      // No active requests after the request succeeded
      var activeRequestsCounter = metrics.counter(name(ServletHandler.class, "active-requests"));
      assertEquals(0, activeRequestsCounter.getCount(), "active requests");

      // request recorded
      var requestsTimer = metrics.timer(name(ServletHandler.class, "requests"));
      assertEquals(1, requestsTimer.getCount(), "requests");

      // 200 response recorded
      var response200Meter = metrics.meter(name(ServletHandler.class, "2xx-responses"));
      assertEquals(1, response200Meter.getCount(), "2xx responses");
    } finally {
      server.stop();
    }
  }

  String printCount(Metric metric) {
    return switch (metric) {
      case Counter c -> "count=" + c.getCount();
      case Meter m -> "meter=" + m.getCount();
      case Timer t -> "timer=" + t.getCount();
      case Histogram h -> "histogram=" + h.getCount();
      case Gauge<?> g -> "gauge=" + g.getValue();
      default -> metric.toString();
    };
  }
}

I expect to see 1xx-responses and all the other stats like requests recorded just like with InstrumentedHttpChannelListener on Jetty 11, but metrics are not being recorded except for an always increasing active-requests metric, which should be 0 in case there are no active requests.

From a brief debugging session I can see that AbstractInstrumentedHandler#updateResponses, the method responsible for collecting metrics after each request, is never called. In InstrumentedEE10Handler there are two places that invoke this method. One is after a synchronous request, which is invoked on state.isInitial() but this is not called, because the state is completed at that point. The another one is from the InstrumentedAsyncListener#onComplete callback, which is again not invoked for some reason.

I'm not sure if I am missing something or the new Dropwizard instrumentation is just not covering this case of async processing with CompletableFutures returned from Jersey resources.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions