entry : meterNamesByStatusCode.entrySet()) {
+ metersByStatusCode.put(entry.getKey(),
+ metricsRegistry.meter(name(metricName, entry.getValue())));
+ }
+ this.otherMeter = metricsRegistry.meter(name(metricName, otherMetricName));
+ this.timeoutsMeter = metricsRegistry.meter(name(metricName, "timeouts"));
+ this.errorsMeter = metricsRegistry.meter(name(metricName, "errors"));
+ this.activeRequests = metricsRegistry.counter(name(metricName, "activeRequests"));
+ this.requestTimer = metricsRegistry.timer(name(metricName, "requests"));
+
+ }
+
+ private MetricRegistry getMetricsFactory(FilterConfig filterConfig) {
+ final MetricRegistry metricsRegistry;
+
+ final Object o = filterConfig.getServletContext().getAttribute(this.registryAttribute);
+ if (o instanceof MetricRegistry) {
+ metricsRegistry = (MetricRegistry) o;
+ } else {
+ metricsRegistry = new MetricRegistry();
+ }
+ return metricsRegistry;
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+
+ @Override
+ public void doFilter(ServletRequest request,
+ ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ final StatusExposingServletResponse wrappedResponse =
+ new StatusExposingServletResponse((HttpServletResponse) response);
+ activeRequests.inc();
+ final Timer.Context context = requestTimer.time();
+ boolean error = false;
+ try {
+ chain.doFilter(request, wrappedResponse);
+ } catch (IOException | RuntimeException | ServletException e) {
+ error = true;
+ throw e;
+ } finally {
+ if (!error && request.isAsyncStarted()) {
+ request.getAsyncContext().addListener(new AsyncResultListener(context));
+ } else {
+ context.stop();
+ activeRequests.dec();
+ if (error) {
+ errorsMeter.mark();
+ } else {
+ markMeterForStatusCode(wrappedResponse.getStatus());
+ }
+ }
+ }
+ }
+
+ private void markMeterForStatusCode(int status) {
+ final Meter metric = metersByStatusCode.get(status);
+ if (metric != null) {
+ metric.mark();
+ } else {
+ otherMeter.mark();
+ }
+ }
+
+ private static class StatusExposingServletResponse extends HttpServletResponseWrapper {
+ // The Servlet spec says: calling setStatus is optional, if no status is set, the default is 200.
+ private int httpStatus = 200;
+
+ public StatusExposingServletResponse(HttpServletResponse response) {
+ super(response);
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException {
+ httpStatus = sc;
+ super.sendError(sc);
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException {
+ httpStatus = sc;
+ super.sendError(sc, msg);
+ }
+
+ @Override
+ public void setStatus(int sc) {
+ httpStatus = sc;
+ super.setStatus(sc);
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void setStatus(int sc, String sm) {
+ httpStatus = sc;
+ super.setStatus(sc, sm);
+ }
+
+ @Override
+ public int getStatus() {
+ return httpStatus;
+ }
+ }
+
+ private class AsyncResultListener implements AsyncListener {
+ private Timer.Context context;
+ private boolean done = false;
+
+ public AsyncResultListener(Timer.Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException {
+ if (!done) {
+ HttpServletResponse suppliedResponse = (HttpServletResponse) event.getSuppliedResponse();
+ context.stop();
+ activeRequests.dec();
+ markMeterForStatusCode(suppliedResponse.getStatus());
+ }
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException {
+ context.stop();
+ activeRequests.dec();
+ timeoutsMeter.mark();
+ done = true;
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException {
+ context.stop();
+ activeRequests.dec();
+ errorsMeter.mark();
+ done = true;
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException {
+
+ }
+ }
+}
diff --git a/metrics-jakarta-servlet/src/main/java/io/dropwizard/metrics/servlet/InstrumentedFilter.java b/metrics-jakarta-servlet/src/main/java/io/dropwizard/metrics/servlet/InstrumentedFilter.java
new file mode 100644
index 0000000000..17538af50b
--- /dev/null
+++ b/metrics-jakarta-servlet/src/main/java/io/dropwizard/metrics/servlet/InstrumentedFilter.java
@@ -0,0 +1,48 @@
+package io.dropwizard.metrics.servlet;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Implementation of the {@link AbstractInstrumentedFilter} which provides a default set of response codes
+ * to capture information about. Use it in your servlet.xml like this:
+ *
{@code
+ *
+ * instrumentedFilter
+ * io.dropwizard.metrics.servlet.InstrumentedFilter
+ *
+ *
+ * instrumentedFilter
+ * /*
+ *
+ * }
+ */
+public class InstrumentedFilter extends AbstractInstrumentedFilter {
+ public static final String REGISTRY_ATTRIBUTE = InstrumentedFilter.class.getName() + ".registry";
+
+ private static final String NAME_PREFIX = "responseCodes.";
+ private static final int OK = 200;
+ private static final int CREATED = 201;
+ private static final int NO_CONTENT = 204;
+ private static final int BAD_REQUEST = 400;
+ private static final int NOT_FOUND = 404;
+ private static final int SERVER_ERROR = 500;
+
+ /**
+ * Creates a new instance of the filter.
+ */
+ public InstrumentedFilter() {
+ super(REGISTRY_ATTRIBUTE, createMeterNamesByStatusCode(), NAME_PREFIX + "other");
+ }
+
+ private static Map createMeterNamesByStatusCode() {
+ final Map meterNamesByStatusCode = new HashMap<>(6);
+ meterNamesByStatusCode.put(OK, NAME_PREFIX + "ok");
+ meterNamesByStatusCode.put(CREATED, NAME_PREFIX + "created");
+ meterNamesByStatusCode.put(NO_CONTENT, NAME_PREFIX + "noContent");
+ meterNamesByStatusCode.put(BAD_REQUEST, NAME_PREFIX + "badRequest");
+ meterNamesByStatusCode.put(NOT_FOUND, NAME_PREFIX + "notFound");
+ meterNamesByStatusCode.put(SERVER_ERROR, NAME_PREFIX + "serverError");
+ return meterNamesByStatusCode;
+ }
+}
diff --git a/metrics-jakarta-servlet/src/main/java/io/dropwizard/metrics/servlet/InstrumentedFilterContextListener.java b/metrics-jakarta-servlet/src/main/java/io/dropwizard/metrics/servlet/InstrumentedFilterContextListener.java
new file mode 100644
index 0000000000..04d7b6540c
--- /dev/null
+++ b/metrics-jakarta-servlet/src/main/java/io/dropwizard/metrics/servlet/InstrumentedFilterContextListener.java
@@ -0,0 +1,26 @@
+package io.dropwizard.metrics.servlet;
+
+import com.codahale.metrics.MetricRegistry;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+
+/**
+ * A listener implementation which injects a {@link MetricRegistry} instance into the servlet
+ * context. Implement {@link #getMetricRegistry()} to return the {@link MetricRegistry} for your
+ * application.
+ */
+public abstract class InstrumentedFilterContextListener implements ServletContextListener {
+ /**
+ * @return the {@link MetricRegistry} to inject into the servlet context.
+ */
+ protected abstract MetricRegistry getMetricRegistry();
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+ sce.getServletContext().setAttribute(InstrumentedFilter.REGISTRY_ATTRIBUTE, getMetricRegistry());
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce) {
+ }
+}
diff --git a/metrics-jakarta-servlet/src/test/java/io/dropwizard/metrics/servlet/InstrumentedFilterContextListenerTest.java b/metrics-jakarta-servlet/src/test/java/io/dropwizard/metrics/servlet/InstrumentedFilterContextListenerTest.java
new file mode 100644
index 0000000000..b586a8d9f2
--- /dev/null
+++ b/metrics-jakarta-servlet/src/test/java/io/dropwizard/metrics/servlet/InstrumentedFilterContextListenerTest.java
@@ -0,0 +1,32 @@
+package io.dropwizard.metrics.servlet;
+
+import com.codahale.metrics.MetricRegistry;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class InstrumentedFilterContextListenerTest {
+ private final MetricRegistry registry = mock(MetricRegistry.class);
+ private final InstrumentedFilterContextListener listener = new InstrumentedFilterContextListener() {
+ @Override
+ protected MetricRegistry getMetricRegistry() {
+ return registry;
+ }
+ };
+
+ @Test
+ public void injectsTheMetricRegistryIntoTheServletContext() {
+ final ServletContext context = mock(ServletContext.class);
+
+ final ServletContextEvent event = mock(ServletContextEvent.class);
+ when(event.getServletContext()).thenReturn(context);
+
+ listener.contextInitialized(event);
+
+ verify(context).setAttribute("io.dropwizard.metrics.servlet.InstrumentedFilter.registry", registry);
+ }
+}
diff --git a/metrics-jakarta-servlets/pom.xml b/metrics-jakarta-servlets/pom.xml
new file mode 100644
index 0000000000..4bf2ef2557
--- /dev/null
+++ b/metrics-jakarta-servlets/pom.xml
@@ -0,0 +1,132 @@
+
+
+ 4.0.0
+
+
+ io.dropwizard.metrics
+ metrics-parent
+ 4.2.0-SNAPSHOT
+
+
+ metrics-jakarta-servlets
+ Metrics Utility Jakarta Servlets
+ bundle
+
+ A set of utility servlets for Metrics, allowing you to expose valuable information about
+ your production environment.
+
+
+
+ io.dropwizard.metrics.servlets
+ 1.1.1
+ 5.0.0
+ 2.12.0
+ 2.0.0-alpha1
+
+
+
+
+
+ io.dropwizard.metrics
+ metrics-bom
+ ${project.version}
+ pom
+ import
+
+
+ org.eclipse.jetty
+ jetty-bom
+ ${jetty11.version}
+ pom
+ import
+
+
+
+
+
+
+ io.dropwizard.metrics
+ metrics-core
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ io.dropwizard.metrics
+ metrics-healthchecks
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ io.dropwizard.metrics
+ metrics-json
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ io.dropwizard.metrics
+ metrics-jvm
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ com.helger
+ profiler
+ ${papertrail.profiler.version}
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ ${servlet.version}
+ provided
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
+ test
+
+
+ org.eclipse.jetty
+ jetty-servlet
+ test
+
+
+ org.eclipse.jetty
+ jetty-http
+ test
+
+
+ io.dropwizard.metrics
+ metrics-jetty11
+ test
+
+
+
diff --git a/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/AdminServlet.java b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/AdminServlet.java
new file mode 100755
index 0000000000..c02dac623d
--- /dev/null
+++ b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/AdminServlet.java
@@ -0,0 +1,126 @@
+package io.dropwizard.metrics.servlets;
+
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.text.MessageFormat;
+
+public class AdminServlet extends HttpServlet {
+ public static final String DEFAULT_HEALTHCHECK_URI = "/healthcheck";
+ public static final String DEFAULT_METRICS_URI = "/metrics";
+ public static final String DEFAULT_PING_URI = "/ping";
+ public static final String DEFAULT_THREADS_URI = "/threads";
+ public static final String DEFAULT_CPU_PROFILE_URI = "/pprof";
+
+ public static final String METRICS_URI_PARAM_KEY = "metrics-uri";
+ public static final String PING_URI_PARAM_KEY = "ping-uri";
+ public static final String THREADS_URI_PARAM_KEY = "threads-uri";
+ public static final String HEALTHCHECK_URI_PARAM_KEY = "healthcheck-uri";
+ public static final String SERVICE_NAME_PARAM_KEY = "service-name";
+ public static final String CPU_PROFILE_URI_PARAM_KEY = "cpu-profile-uri";
+
+ private static final String TEMPLATE = String.format(
+ "%n" +
+ "%n" +
+ "%n" +
+ " Metrics{10}%n" +
+ "%n" +
+ "%n" +
+ " Operational Menu{10}
%n" +
+ " %n" +
+ "%n" +
+ ""
+ );
+ private static final String CONTENT_TYPE = "text/html";
+ private static final long serialVersionUID = -2850794040708785318L;
+
+ private transient HealthCheckServlet healthCheckServlet;
+ private transient MetricsServlet metricsServlet;
+ private transient PingServlet pingServlet;
+ private transient ThreadDumpServlet threadDumpServlet;
+ private transient CpuProfileServlet cpuProfileServlet;
+ private transient String metricsUri;
+ private transient String pingUri;
+ private transient String threadsUri;
+ private transient String healthcheckUri;
+ private transient String cpuprofileUri;
+ private transient String serviceName;
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ super.init(config);
+
+ this.healthCheckServlet = new HealthCheckServlet();
+ healthCheckServlet.init(config);
+
+ this.metricsServlet = new MetricsServlet();
+ metricsServlet.init(config);
+
+ this.pingServlet = new PingServlet();
+ pingServlet.init(config);
+
+ this.threadDumpServlet = new ThreadDumpServlet();
+ threadDumpServlet.init(config);
+
+ this.cpuProfileServlet = new CpuProfileServlet();
+ cpuProfileServlet.init(config);
+
+ this.metricsUri = getParam(config.getInitParameter(METRICS_URI_PARAM_KEY), DEFAULT_METRICS_URI);
+ this.pingUri = getParam(config.getInitParameter(PING_URI_PARAM_KEY), DEFAULT_PING_URI);
+ this.threadsUri = getParam(config.getInitParameter(THREADS_URI_PARAM_KEY), DEFAULT_THREADS_URI);
+ this.healthcheckUri = getParam(config.getInitParameter(HEALTHCHECK_URI_PARAM_KEY), DEFAULT_HEALTHCHECK_URI);
+ this.cpuprofileUri = getParam(config.getInitParameter(CPU_PROFILE_URI_PARAM_KEY), DEFAULT_CPU_PROFILE_URI);
+ this.serviceName = getParam(config.getInitParameter(SERVICE_NAME_PARAM_KEY), null);
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ final String path = req.getContextPath() + req.getServletPath();
+
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setHeader("Cache-Control", "must-revalidate,no-cache,no-store");
+ resp.setContentType(CONTENT_TYPE);
+ try (PrintWriter writer = resp.getWriter()) {
+ writer.println(MessageFormat.format(TEMPLATE, path, metricsUri, path, pingUri, path,
+ threadsUri, path, healthcheckUri, path, cpuprofileUri,
+ serviceName == null ? "" : " (" + serviceName + ")"));
+ }
+ }
+
+ @Override
+ protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ final String uri = req.getPathInfo();
+ if (uri == null || uri.equals("/")) {
+ super.service(req, resp);
+ } else if (uri.equals(healthcheckUri)) {
+ healthCheckServlet.service(req, resp);
+ } else if (uri.startsWith(metricsUri)) {
+ metricsServlet.service(req, resp);
+ } else if (uri.equals(pingUri)) {
+ pingServlet.service(req, resp);
+ } else if (uri.equals(threadsUri)) {
+ threadDumpServlet.service(req, resp);
+ } else if (uri.equals(cpuprofileUri)) {
+ cpuProfileServlet.service(req, resp);
+ } else {
+ resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+ }
+
+ private static String getParam(String initParam, String defaultValue) {
+ return initParam == null ? defaultValue : initParam;
+ }
+}
diff --git a/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/CpuProfileServlet.java b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/CpuProfileServlet.java
new file mode 100644
index 0000000000..3e05af6285
--- /dev/null
+++ b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/CpuProfileServlet.java
@@ -0,0 +1,79 @@
+package io.dropwizard.metrics.servlets;
+
+import com.papertrail.profiler.CpuProfile;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.time.Duration;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * An HTTP servlets which outputs a pprof parseable response.
+ */
+public class CpuProfileServlet extends HttpServlet {
+ private static final long serialVersionUID = -668666696530287501L;
+ private static final String CONTENT_TYPE = "pprof/raw";
+ private static final String CACHE_CONTROL = "Cache-Control";
+ private static final String NO_CACHE = "must-revalidate,no-cache,no-store";
+ private final Lock lock = new ReentrantLock();
+
+ @Override
+ protected void doGet(HttpServletRequest req,
+ HttpServletResponse resp) throws ServletException, IOException {
+
+ int duration = 10;
+ if (req.getParameter("duration") != null) {
+ try {
+ duration = Integer.parseInt(req.getParameter("duration"));
+ } catch (NumberFormatException e) {
+ duration = 10;
+ }
+ }
+
+ int frequency = 100;
+ if (req.getParameter("frequency") != null) {
+ try {
+ frequency = Integer.parseInt(req.getParameter("frequency"));
+ frequency = Math.min(Math.max(frequency, 1), 1000);
+ } catch (NumberFormatException e) {
+ frequency = 100;
+ }
+ }
+
+ final Thread.State state;
+ if ("blocked".equalsIgnoreCase(req.getParameter("state"))) {
+ state = Thread.State.BLOCKED;
+ } else {
+ state = Thread.State.RUNNABLE;
+ }
+
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setHeader(CACHE_CONTROL, NO_CACHE);
+ resp.setContentType(CONTENT_TYPE);
+ try (OutputStream output = resp.getOutputStream()) {
+ doProfile(output, duration, frequency, state);
+ }
+ }
+
+ protected void doProfile(OutputStream out, int duration, int frequency, Thread.State state) throws IOException {
+ if (lock.tryLock()) {
+ try {
+ CpuProfile profile = CpuProfile.record(Duration.ofSeconds(duration),
+ frequency, state);
+ if (profile == null) {
+ throw new RuntimeException("could not create CpuProfile");
+ }
+ profile.writeGoogleProfile(out);
+ return;
+ } finally {
+ lock.unlock();
+ }
+ }
+ throw new RuntimeException("Only one profile request may be active at a time");
+ }
+}
diff --git a/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/HealthCheckServlet.java b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/HealthCheckServlet.java
new file mode 100644
index 0000000000..d31f02faff
--- /dev/null
+++ b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/HealthCheckServlet.java
@@ -0,0 +1,161 @@
+package io.dropwizard.metrics.servlets;
+
+import com.codahale.metrics.health.HealthCheck;
+import com.codahale.metrics.health.HealthCheckFilter;
+import com.codahale.metrics.health.HealthCheckRegistry;
+import com.codahale.metrics.json.HealthCheckModule;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.concurrent.ExecutorService;
+
+public class HealthCheckServlet extends HttpServlet {
+ public static abstract class ContextListener implements ServletContextListener {
+ /**
+ * @return the {@link HealthCheckRegistry} to inject into the servlet context.
+ */
+ protected abstract HealthCheckRegistry getHealthCheckRegistry();
+
+ /**
+ * @return the {@link ExecutorService} to inject into the servlet context, or {@code null}
+ * if the health checks should be run in the servlet worker thread.
+ */
+ protected ExecutorService getExecutorService() {
+ // don't use a thread pool by default
+ return null;
+ }
+
+ /**
+ * @return the {@link HealthCheckFilter} that shall be used to filter health checks,
+ * or {@link HealthCheckFilter#ALL} if the default should be used.
+ */
+ protected HealthCheckFilter getHealthCheckFilter() {
+ return HealthCheckFilter.ALL;
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent event) {
+ final ServletContext context = event.getServletContext();
+ context.setAttribute(HEALTH_CHECK_REGISTRY, getHealthCheckRegistry());
+ context.setAttribute(HEALTH_CHECK_EXECUTOR, getExecutorService());
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent event) {
+ // no-op
+ }
+ }
+
+ public static final String HEALTH_CHECK_REGISTRY = HealthCheckServlet.class.getCanonicalName() + ".registry";
+ public static final String HEALTH_CHECK_EXECUTOR = HealthCheckServlet.class.getCanonicalName() + ".executor";
+ public static final String HEALTH_CHECK_FILTER = HealthCheckServlet.class.getCanonicalName() + ".healthCheckFilter";
+
+ private static final long serialVersionUID = -8432996484889177321L;
+ private static final String CONTENT_TYPE = "application/json";
+
+ private transient HealthCheckRegistry registry;
+ private transient ExecutorService executorService;
+ private transient HealthCheckFilter filter;
+ private transient ObjectMapper mapper;
+
+ public HealthCheckServlet() {
+ }
+
+ public HealthCheckServlet(HealthCheckRegistry registry) {
+ this.registry = registry;
+ }
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ super.init(config);
+
+ final ServletContext context = config.getServletContext();
+ if (null == registry) {
+ final Object registryAttr = context.getAttribute(HEALTH_CHECK_REGISTRY);
+ if (registryAttr instanceof HealthCheckRegistry) {
+ this.registry = (HealthCheckRegistry) registryAttr;
+ } else {
+ throw new ServletException("Couldn't find a HealthCheckRegistry instance.");
+ }
+ }
+
+ final Object executorAttr = context.getAttribute(HEALTH_CHECK_EXECUTOR);
+ if (executorAttr instanceof ExecutorService) {
+ this.executorService = (ExecutorService) executorAttr;
+ }
+
+
+ final Object filterAttr = context.getAttribute(HEALTH_CHECK_FILTER);
+ if (filterAttr instanceof HealthCheckFilter) {
+ filter = (HealthCheckFilter) filterAttr;
+ }
+ if (filter == null) {
+ filter = HealthCheckFilter.ALL;
+ }
+
+ this.mapper = new ObjectMapper().registerModule(new HealthCheckModule());
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ registry.shutdown();
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req,
+ HttpServletResponse resp) throws ServletException, IOException {
+ final SortedMap results = runHealthChecks();
+ resp.setContentType(CONTENT_TYPE);
+ resp.setHeader("Cache-Control", "must-revalidate,no-cache,no-store");
+ if (results.isEmpty()) {
+ resp.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
+ } else {
+ if (isAllHealthy(results)) {
+ resp.setStatus(HttpServletResponse.SC_OK);
+ } else {
+ resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ try (OutputStream output = resp.getOutputStream()) {
+ getWriter(req).writeValue(output, results);
+ }
+ }
+
+ private ObjectWriter getWriter(HttpServletRequest request) {
+ final boolean prettyPrint = Boolean.parseBoolean(request.getParameter("pretty"));
+ if (prettyPrint) {
+ return mapper.writerWithDefaultPrettyPrinter();
+ }
+ return mapper.writer();
+ }
+
+ private SortedMap runHealthChecks() {
+ if (executorService == null) {
+ return registry.runHealthChecks(filter);
+ }
+ return registry.runHealthChecks(executorService, filter);
+ }
+
+ private static boolean isAllHealthy(Map results) {
+ for (HealthCheck.Result result : results.values()) {
+ if (!result.isHealthy()) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/MetricsServlet.java b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/MetricsServlet.java
new file mode 100644
index 0000000000..a248dd8140
--- /dev/null
+++ b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/MetricsServlet.java
@@ -0,0 +1,198 @@
+package io.dropwizard.metrics.servlets;
+
+import com.codahale.metrics.MetricFilter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.json.MetricsModule;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.util.JSONPObject;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A servlet which returns the metrics in a given registry as an {@code application/json} response.
+ */
+public class MetricsServlet extends HttpServlet {
+ /**
+ * An abstract {@link ServletContextListener} which allows you to programmatically inject the
+ * {@link MetricRegistry}, rate and duration units, and allowed origin for
+ * {@link MetricsServlet}.
+ */
+ public static abstract class ContextListener implements ServletContextListener {
+ /**
+ * @return the {@link MetricRegistry} to inject into the servlet context.
+ */
+ protected abstract MetricRegistry getMetricRegistry();
+
+ /**
+ * @return the {@link TimeUnit} to which rates should be converted, or {@code null} if the
+ * default should be used.
+ */
+ protected TimeUnit getRateUnit() {
+ // use the default
+ return null;
+ }
+
+ /**
+ * @return the {@link TimeUnit} to which durations should be converted, or {@code null} if
+ * the default should be used.
+ */
+ protected TimeUnit getDurationUnit() {
+ // use the default
+ return null;
+ }
+
+ /**
+ * @return the {@code Access-Control-Allow-Origin} header value, if any.
+ */
+ protected String getAllowedOrigin() {
+ // use the default
+ return null;
+ }
+
+ /**
+ * Returns the name of the parameter used to specify the jsonp callback, if any.
+ */
+ protected String getJsonpCallbackParameter() {
+ return null;
+ }
+
+ /**
+ * Returns the {@link MetricFilter} that shall be used to filter metrics, or {@link MetricFilter#ALL} if
+ * the default should be used.
+ */
+ protected MetricFilter getMetricFilter() {
+ // use the default
+ return MetricFilter.ALL;
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent event) {
+ final ServletContext context = event.getServletContext();
+ context.setAttribute(METRICS_REGISTRY, getMetricRegistry());
+ context.setAttribute(METRIC_FILTER, getMetricFilter());
+ if (getDurationUnit() != null) {
+ context.setInitParameter(MetricsServlet.DURATION_UNIT, getDurationUnit().toString());
+ }
+ if (getRateUnit() != null) {
+ context.setInitParameter(MetricsServlet.RATE_UNIT, getRateUnit().toString());
+ }
+ if (getAllowedOrigin() != null) {
+ context.setInitParameter(MetricsServlet.ALLOWED_ORIGIN, getAllowedOrigin());
+ }
+ if (getJsonpCallbackParameter() != null) {
+ context.setAttribute(CALLBACK_PARAM, getJsonpCallbackParameter());
+ }
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent event) {
+ // no-op
+ }
+ }
+
+ public static final String RATE_UNIT = MetricsServlet.class.getCanonicalName() + ".rateUnit";
+ public static final String DURATION_UNIT = MetricsServlet.class.getCanonicalName() + ".durationUnit";
+ public static final String SHOW_SAMPLES = MetricsServlet.class.getCanonicalName() + ".showSamples";
+ public static final String METRICS_REGISTRY = MetricsServlet.class.getCanonicalName() + ".registry";
+ public static final String ALLOWED_ORIGIN = MetricsServlet.class.getCanonicalName() + ".allowedOrigin";
+ public static final String METRIC_FILTER = MetricsServlet.class.getCanonicalName() + ".metricFilter";
+ public static final String CALLBACK_PARAM = MetricsServlet.class.getCanonicalName() + ".jsonpCallback";
+
+ private static final long serialVersionUID = 1049773947734939602L;
+ private static final String CONTENT_TYPE = "application/json";
+
+ protected String allowedOrigin;
+ protected String jsonpParamName;
+ protected transient MetricRegistry registry;
+ protected transient ObjectMapper mapper;
+
+ public MetricsServlet() {
+ }
+
+ public MetricsServlet(MetricRegistry registry) {
+ this.registry = registry;
+ }
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ super.init(config);
+
+ final ServletContext context = config.getServletContext();
+ if (null == registry) {
+ final Object registryAttr = context.getAttribute(METRICS_REGISTRY);
+ if (registryAttr instanceof MetricRegistry) {
+ this.registry = (MetricRegistry) registryAttr;
+ } else {
+ throw new ServletException("Couldn't find a MetricRegistry instance.");
+ }
+ }
+ this.allowedOrigin = context.getInitParameter(ALLOWED_ORIGIN);
+ this.jsonpParamName = context.getInitParameter(CALLBACK_PARAM);
+
+ setupMetricsModule(context);
+ }
+
+ protected void setupMetricsModule(ServletContext context) {
+ final TimeUnit rateUnit = parseTimeUnit(context.getInitParameter(RATE_UNIT),
+ TimeUnit.SECONDS);
+ final TimeUnit durationUnit = parseTimeUnit(context.getInitParameter(DURATION_UNIT),
+ TimeUnit.SECONDS);
+ final boolean showSamples = Boolean.parseBoolean(context.getInitParameter(SHOW_SAMPLES));
+ MetricFilter filter = (MetricFilter) context.getAttribute(METRIC_FILTER);
+ if (filter == null) {
+ filter = MetricFilter.ALL;
+ }
+
+ this.mapper = new ObjectMapper().registerModule(new MetricsModule(rateUnit,
+ durationUnit,
+ showSamples,
+ filter));
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req,
+ HttpServletResponse resp) throws ServletException, IOException {
+ resp.setContentType(CONTENT_TYPE);
+ if (allowedOrigin != null) {
+ resp.setHeader("Access-Control-Allow-Origin", allowedOrigin);
+ }
+ resp.setHeader("Cache-Control", "must-revalidate,no-cache,no-store");
+ resp.setStatus(HttpServletResponse.SC_OK);
+
+ try (OutputStream output = resp.getOutputStream()) {
+ if (jsonpParamName != null && req.getParameter(jsonpParamName) != null) {
+ getWriter(req).writeValue(output, new JSONPObject(req.getParameter(jsonpParamName), registry));
+ } else {
+ getWriter(req).writeValue(output, registry);
+ }
+ }
+ }
+
+ protected ObjectWriter getWriter(HttpServletRequest request) {
+ final boolean prettyPrint = Boolean.parseBoolean(request.getParameter("pretty"));
+ if (prettyPrint) {
+ return mapper.writerWithDefaultPrettyPrinter();
+ }
+ return mapper.writer();
+ }
+
+ protected TimeUnit parseTimeUnit(String value, TimeUnit defaultValue) {
+ try {
+ return TimeUnit.valueOf(String.valueOf(value).toUpperCase(Locale.US));
+ } catch (IllegalArgumentException e) {
+ return defaultValue;
+ }
+ }
+}
diff --git a/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/PingServlet.java b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/PingServlet.java
new file mode 100644
index 0000000000..74bacec059
--- /dev/null
+++ b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/PingServlet.java
@@ -0,0 +1,31 @@
+package io.dropwizard.metrics.servlets;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * An HTTP servlets which outputs a {@code text/plain} {@code "pong"} response.
+ */
+public class PingServlet extends HttpServlet {
+ private static final long serialVersionUID = 3772654177231086757L;
+ private static final String CONTENT_TYPE = "text/plain";
+ private static final String CONTENT = "pong";
+ private static final String CACHE_CONTROL = "Cache-Control";
+ private static final String NO_CACHE = "must-revalidate,no-cache,no-store";
+
+ @Override
+ protected void doGet(HttpServletRequest req,
+ HttpServletResponse resp) throws ServletException, IOException {
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setHeader(CACHE_CONTROL, NO_CACHE);
+ resp.setContentType(CONTENT_TYPE);
+ try (PrintWriter writer = resp.getWriter()) {
+ writer.println(CONTENT);
+ }
+ }
+}
diff --git a/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/ThreadDumpServlet.java b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/ThreadDumpServlet.java
new file mode 100644
index 0000000000..ee1fea3f74
--- /dev/null
+++ b/metrics-jakarta-servlets/src/main/java/io/dropwizard/metrics/servlets/ThreadDumpServlet.java
@@ -0,0 +1,55 @@
+package io.dropwizard.metrics.servlets;
+
+import com.codahale.metrics.jvm.ThreadDump;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.management.ManagementFactory;
+
+/**
+ * An HTTP servlets which outputs a {@code text/plain} dump of all threads in
+ * the VM. Only responds to {@code GET} requests.
+ */
+public class ThreadDumpServlet extends HttpServlet {
+
+ private static final long serialVersionUID = -2690343532336103046L;
+ private static final String CONTENT_TYPE = "text/plain";
+
+ private transient ThreadDump threadDump;
+
+ @Override
+ public void init() throws ServletException {
+ try {
+ // Some PaaS like Google App Engine blacklist java.lang.managament
+ this.threadDump = new ThreadDump(ManagementFactory.getThreadMXBean());
+ } catch (NoClassDefFoundError ncdfe) {
+ this.threadDump = null; // we won't be able to provide thread dump
+ }
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req,
+ HttpServletResponse resp) throws ServletException, IOException {
+ final boolean includeMonitors = getParam(req.getParameter("monitors"), true);
+ final boolean includeSynchronizers = getParam(req.getParameter("synchronizers"), true);
+
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentType(CONTENT_TYPE);
+ resp.setHeader("Cache-Control", "must-revalidate,no-cache,no-store");
+ if (threadDump == null) {
+ resp.getWriter().println("Sorry your runtime environment does not allow to dump threads.");
+ return;
+ }
+ try (OutputStream output = resp.getOutputStream()) {
+ threadDump.dump(includeMonitors, includeSynchronizers, output);
+ }
+ }
+
+ private static Boolean getParam(String initParam, boolean defaultValue) {
+ return initParam == null ? defaultValue : Boolean.parseBoolean(initParam);
+ }
+}
diff --git a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/AbstractServletTest.java b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/AbstractServletTest.java
new file mode 100644
index 0000000000..3afb8bdfbb
--- /dev/null
+++ b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/AbstractServletTest.java
@@ -0,0 +1,29 @@
+package io.dropwizard.metrics.servlets;
+
+import org.eclipse.jetty.http.HttpTester;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.After;
+import org.junit.Before;
+
+public abstract class AbstractServletTest {
+ private final ServletTester tester = new ServletTester();
+ protected final HttpTester.Request request = HttpTester.newRequest();
+ protected HttpTester.Response response;
+
+ @Before
+ public void setUpTester() throws Exception {
+ setUp(tester);
+ tester.start();
+ }
+
+ protected abstract void setUp(ServletTester tester);
+
+ @After
+ public void tearDownTester() throws Exception {
+ tester.stop();
+ }
+
+ protected void processRequest() throws Exception {
+ this.response = HttpTester.parseResponse(tester.getResponses(request.generate()));
+ }
+}
diff --git a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/AdminServletTest.java b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/AdminServletTest.java
new file mode 100755
index 0000000000..102e0a83ad
--- /dev/null
+++ b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/AdminServletTest.java
@@ -0,0 +1,62 @@
+package io.dropwizard.metrics.servlets;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.health.HealthCheckRegistry;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AdminServletTest extends AbstractServletTest {
+ private final MetricRegistry registry = new MetricRegistry();
+ private final HealthCheckRegistry healthCheckRegistry = new HealthCheckRegistry();
+
+ @Override
+ protected void setUp(ServletTester tester) {
+ tester.setContextPath("/context");
+
+ tester.setAttribute("io.dropwizard.metrics.servlets.MetricsServlet.registry", registry);
+ tester.setAttribute("io.dropwizard.metrics.servlets.HealthCheckServlet.registry", healthCheckRegistry);
+ tester.addServlet(AdminServlet.class, "/admin");
+ }
+
+ @Before
+ public void setUp() {
+ request.setMethod("GET");
+ request.setURI("/context/admin");
+ request.setVersion("HTTP/1.0");
+ }
+
+ @Test
+ public void returnsA200() throws Exception {
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ assertThat(response.getContent())
+ .isEqualTo(String.format(
+ "%n" +
+ "%n" +
+ "%n" +
+ " Metrics%n" +
+ "%n" +
+ "%n" +
+ " Operational Menu
%n" +
+ " %n" +
+ "%n" +
+ "%n"
+ ));
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("text/html;charset=UTF-8");
+ }
+}
diff --git a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/CpuProfileServletTest.java b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/CpuProfileServletTest.java
new file mode 100644
index 0000000000..e724acf43f
--- /dev/null
+++ b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/CpuProfileServletTest.java
@@ -0,0 +1,44 @@
+package io.dropwizard.metrics.servlets;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CpuProfileServletTest extends AbstractServletTest {
+
+ @Override
+ protected void setUp(ServletTester tester) {
+ tester.addServlet(CpuProfileServlet.class, "/pprof");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ request.setMethod("GET");
+ request.setURI("/pprof?duration=1");
+ request.setVersion("HTTP/1.0");
+
+ processRequest();
+ }
+
+ @Test
+ public void returns200OK() {
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ }
+
+ @Test
+ public void returnsPprofRaw() {
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("pprof/raw");
+ }
+
+ @Test
+ public void returnsUncacheable() {
+ assertThat(response.get(HttpHeader.CACHE_CONTROL))
+ .isEqualTo("must-revalidate,no-cache,no-store");
+
+ }
+}
diff --git a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/HealthCheckServletTest.java b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/HealthCheckServletTest.java
new file mode 100644
index 0000000000..c34f7da438
--- /dev/null
+++ b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/HealthCheckServletTest.java
@@ -0,0 +1,269 @@
+package io.dropwizard.metrics.servlets;
+
+import com.codahale.metrics.Clock;
+import com.codahale.metrics.health.HealthCheck;
+import com.codahale.metrics.health.HealthCheckFilter;
+import com.codahale.metrics.health.HealthCheckRegistry;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class HealthCheckServletTest extends AbstractServletTest {
+
+ private static final ZonedDateTime FIXED_TIME = ZonedDateTime.now();
+
+ private static final DateTimeFormatter DATE_TIME_FORMATTER =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
+
+ private static final String EXPECTED_TIMESTAMP = DATE_TIME_FORMATTER.format(FIXED_TIME);
+
+ private static final Clock FIXED_CLOCK = new Clock() {
+ @Override
+ public long getTick() {
+ return 0L;
+ }
+
+ @Override
+ public long getTime() {
+ return FIXED_TIME.toInstant().toEpochMilli();
+ }
+ };
+
+ private final HealthCheckRegistry registry = new HealthCheckRegistry();
+ private final ExecutorService threadPool = Executors.newCachedThreadPool();
+
+ @Override
+ protected void setUp(ServletTester tester) {
+ tester.addServlet(io.dropwizard.metrics.servlets.HealthCheckServlet.class, "/healthchecks");
+ tester.setAttribute("io.dropwizard.metrics.servlets.HealthCheckServlet.registry", registry);
+ tester.setAttribute("io.dropwizard.metrics.servlets.HealthCheckServlet.executor", threadPool);
+ tester.setAttribute("io.dropwizard.metrics.servlets.HealthCheckServlet.healthCheckFilter",
+ (HealthCheckFilter) (name, healthCheck) -> !"filtered".equals(name));
+ }
+
+ @Before
+ public void setUp() {
+ request.setMethod("GET");
+ request.setURI("/healthchecks");
+ request.setVersion("HTTP/1.0");
+ }
+
+ @After
+ public void tearDown() {
+ threadPool.shutdown();
+ }
+
+ @Test
+ public void returns501IfNoHealthChecksAreRegistered() throws Exception {
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(501);
+ assertThat(response.getContent())
+ .isEqualTo("{}");
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+
+ @Test
+ public void returnsA200IfAllHealthChecksAreHealthy() throws Exception {
+ registry.register("fun", new HealthCheck() {
+ @Override
+ protected Result check() {
+ return healthyResultUsingFixedClockWithMessage("whee");
+ }
+
+ @Override
+ protected Clock clock() {
+ return FIXED_CLOCK;
+ }
+ });
+
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ assertThat(response.getContent())
+ .isEqualTo("{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" +
+ EXPECTED_TIMESTAMP +
+ "\"}}");
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+
+ @Test
+ public void returnsASubsetOfHealthChecksIfFiltered() throws Exception {
+ registry.register("fun", new HealthCheck() {
+ @Override
+ protected Result check() {
+ return healthyResultUsingFixedClockWithMessage("whee");
+ }
+
+ @Override
+ protected Clock clock() {
+ return FIXED_CLOCK;
+ }
+ });
+
+ registry.register("filtered", new HealthCheck() {
+ @Override
+ protected Result check() {
+ return Result.unhealthy("whee");
+ }
+
+ @Override
+ protected Clock clock() {
+ return FIXED_CLOCK;
+ }
+ });
+
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ assertThat(response.getContent())
+ .isEqualTo("{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" +
+ EXPECTED_TIMESTAMP +
+ "\"}}");
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+
+ @Test
+ public void returnsA500IfAnyHealthChecksAreUnhealthy() throws Exception {
+ registry.register("fun", new HealthCheck() {
+ @Override
+ protected Result check() {
+ return healthyResultUsingFixedClockWithMessage("whee");
+ }
+
+ @Override
+ protected Clock clock() {
+ return FIXED_CLOCK;
+ }
+ });
+
+ registry.register("notFun", new HealthCheck() {
+ @Override
+ protected Result check() {
+ return Result.builder().usingClock(FIXED_CLOCK).unhealthy().withMessage("whee").build();
+ }
+
+ @Override
+ protected Clock clock() {
+ return FIXED_CLOCK;
+ }
+ });
+
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(500);
+ assertThat(response.getContent())
+ .contains(
+ "{\"fun\":{\"healthy\":true,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}",
+ ",\"notFun\":{\"healthy\":false,\"message\":\"whee\",\"duration\":0,\"timestamp\":\"" + EXPECTED_TIMESTAMP + "\"}}");
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+
+ @Test
+ public void optionallyPrettyPrintsTheJson() throws Exception {
+ registry.register("fun", new HealthCheck() {
+ @Override
+ protected Result check() {
+ return healthyResultUsingFixedClockWithMessage("foo bar 123");
+ }
+
+ @Override
+ protected Clock clock() {
+ return FIXED_CLOCK;
+ }
+ });
+
+ request.setURI("/healthchecks?pretty=true");
+
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ assertThat(response.getContent())
+ .isEqualTo(String.format("{%n" +
+ " \"fun\" : {%n" +
+ " \"healthy\" : true,%n" +
+ " \"message\" : \"foo bar 123\",%n" +
+ " \"duration\" : 0,%n" +
+ " \"timestamp\" : \"" + EXPECTED_TIMESTAMP + "\"" +
+ "%n }%n}"));
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+
+ private static HealthCheck.Result healthyResultUsingFixedClockWithMessage(String message) {
+ return HealthCheck.Result.builder()
+ .healthy()
+ .withMessage(message)
+ .usingClock(FIXED_CLOCK)
+ .build();
+ }
+
+ @Test
+ public void constructorWithRegistryAsArgumentIsUsedInPreferenceOverServletConfig() throws Exception {
+ final HealthCheckRegistry healthCheckRegistry = mock(HealthCheckRegistry.class);
+ final ServletContext servletContext = mock(ServletContext.class);
+ final ServletConfig servletConfig = mock(ServletConfig.class);
+ when(servletConfig.getServletContext()).thenReturn(servletContext);
+
+ final io.dropwizard.metrics.servlets.HealthCheckServlet healthCheckServlet = new io.dropwizard.metrics.servlets.HealthCheckServlet(healthCheckRegistry);
+ healthCheckServlet.init(servletConfig);
+
+ verify(servletConfig, times(1)).getServletContext();
+ verify(servletContext, never()).getAttribute(eq(io.dropwizard.metrics.servlets.HealthCheckServlet.HEALTH_CHECK_REGISTRY));
+ }
+
+ @Test
+ public void constructorWithRegistryAsArgumentUsesServletConfigWhenNull() throws Exception {
+ final HealthCheckRegistry healthCheckRegistry = mock(HealthCheckRegistry.class);
+ final ServletContext servletContext = mock(ServletContext.class);
+ final ServletConfig servletConfig = mock(ServletConfig.class);
+ when(servletConfig.getServletContext()).thenReturn(servletContext);
+ when(servletContext.getAttribute(eq(io.dropwizard.metrics.servlets.HealthCheckServlet.HEALTH_CHECK_REGISTRY)))
+ .thenReturn(healthCheckRegistry);
+
+ final io.dropwizard.metrics.servlets.HealthCheckServlet healthCheckServlet = new io.dropwizard.metrics.servlets.HealthCheckServlet(null);
+ healthCheckServlet.init(servletConfig);
+
+ verify(servletConfig, times(1)).getServletContext();
+ verify(servletContext, times(1)).getAttribute(eq(io.dropwizard.metrics.servlets.HealthCheckServlet.HEALTH_CHECK_REGISTRY));
+ }
+
+ @Test(expected = ServletException.class)
+ public void constructorWithRegistryAsArgumentUsesServletConfigWhenNullButWrongTypeInContext() throws Exception {
+ final ServletContext servletContext = mock(ServletContext.class);
+ final ServletConfig servletConfig = mock(ServletConfig.class);
+ when(servletConfig.getServletContext()).thenReturn(servletContext);
+ when(servletContext.getAttribute(eq(io.dropwizard.metrics.servlets.HealthCheckServlet.HEALTH_CHECK_REGISTRY)))
+ .thenReturn("IRELLEVANT_STRING");
+
+ final io.dropwizard.metrics.servlets.HealthCheckServlet healthCheckServlet = new HealthCheckServlet(null);
+ healthCheckServlet.init(servletConfig);
+ }
+}
diff --git a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/MetricsServletContextListenerTest.java b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/MetricsServletContextListenerTest.java
new file mode 100644
index 0000000000..49ffb1cada
--- /dev/null
+++ b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/MetricsServletContextListenerTest.java
@@ -0,0 +1,171 @@
+package io.dropwizard.metrics.servlets;
+
+import com.codahale.metrics.Clock;
+import com.codahale.metrics.ExponentiallyDecayingReservoir;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class MetricsServletContextListenerTest extends AbstractServletTest {
+ private final Clock clock = mock(Clock.class);
+ private final MetricRegistry registry = new MetricRegistry();
+ private final String allowedOrigin = "some.other.origin";
+
+ @Override
+ protected void setUp(ServletTester tester) {
+ tester.setAttribute("io.dropwizard.metrics.servlets.MetricsServlet.registry", registry);
+ tester.addServlet(io.dropwizard.metrics.servlets.MetricsServlet.class, "/metrics");
+ tester.getContext().addEventListener(new MetricsServlet.ContextListener() {
+ @Override
+ protected MetricRegistry getMetricRegistry() {
+ return registry;
+ }
+
+ @Override
+ protected TimeUnit getDurationUnit() {
+ return TimeUnit.MILLISECONDS;
+ }
+
+ @Override
+ protected TimeUnit getRateUnit() {
+ return TimeUnit.MINUTES;
+ }
+
+ @Override
+ protected String getAllowedOrigin() {
+ return allowedOrigin;
+ }
+ });
+ }
+
+ @Before
+ public void setUp() {
+ // provide ticks for the setup (calls getTick 6 times). The serialization in the tests themselves
+ // will call getTick again several times and always get the same value (the last specified here)
+ when(clock.getTick()).thenReturn(100L, 100L, 200L, 300L, 300L, 400L);
+
+ registry.register("g1", (Gauge) () -> 100L);
+ registry.counter("c").inc();
+ registry.histogram("h").update(1);
+ registry.register("m", new Meter(clock)).mark();
+ registry.register("t", new Timer(new ExponentiallyDecayingReservoir(), clock))
+ .update(1, TimeUnit.SECONDS);
+
+ request.setMethod("GET");
+ request.setURI("/metrics");
+ request.setVersion("HTTP/1.0");
+ }
+
+ @Test
+ public void returnsA200() throws Exception {
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ assertThat(response.get("Access-Control-Allow-Origin"))
+ .isEqualTo(allowedOrigin);
+ assertThat(response.getContent())
+ .isEqualTo("{" +
+ "\"version\":\"4.0.0\"," +
+ "\"gauges\":{" +
+ "\"g1\":{\"value\":100}" +
+ "}," +
+ "\"counters\":{" +
+ "\"c\":{\"count\":1}" +
+ "}," +
+ "\"histograms\":{" +
+ "\"h\":{\"count\":1,\"max\":1,\"mean\":1.0,\"min\":1,\"p50\":1.0,\"p75\":1.0,\"p95\":1.0,\"p98\":1.0,\"p99\":1.0,\"p999\":1.0,\"stddev\":0.0}" +
+ "}," +
+ "\"meters\":{" +
+ "\"m\":{\"count\":1,\"m15_rate\":0.0,\"m1_rate\":0.0,\"m5_rate\":0.0,\"mean_rate\":2.0E8,\"units\":\"events/minute\"}},\"timers\":{\"t\":{\"count\":1,\"max\":1000.0,\"mean\":1000.0,\"min\":1000.0,\"p50\":1000.0,\"p75\":1000.0,\"p95\":1000.0,\"p98\":1000.0,\"p99\":1000.0,\"p999\":1000.0,\"stddev\":0.0,\"m15_rate\":0.0,\"m1_rate\":0.0,\"m5_rate\":0.0,\"mean_rate\":6.0E8,\"duration_units\":\"milliseconds\",\"rate_units\":\"calls/minute\"}" +
+ "}" +
+ "}");
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+
+ @Test
+ public void optionallyPrettyPrintsTheJson() throws Exception {
+ request.setURI("/metrics?pretty=true");
+
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ assertThat(response.get("Access-Control-Allow-Origin"))
+ .isEqualTo(allowedOrigin);
+ assertThat(response.getContent())
+ .isEqualTo(String.format("{%n" +
+ " \"version\" : \"4.0.0\",%n" +
+ " \"gauges\" : {%n" +
+ " \"g1\" : {%n" +
+ " \"value\" : 100%n" +
+ " }%n" +
+ " },%n" +
+ " \"counters\" : {%n" +
+ " \"c\" : {%n" +
+ " \"count\" : 1%n" +
+ " }%n" +
+ " },%n" +
+ " \"histograms\" : {%n" +
+ " \"h\" : {%n" +
+ " \"count\" : 1,%n" +
+ " \"max\" : 1,%n" +
+ " \"mean\" : 1.0,%n" +
+ " \"min\" : 1,%n" +
+ " \"p50\" : 1.0,%n" +
+ " \"p75\" : 1.0,%n" +
+ " \"p95\" : 1.0,%n" +
+ " \"p98\" : 1.0,%n" +
+ " \"p99\" : 1.0,%n" +
+ " \"p999\" : 1.0,%n" +
+ " \"stddev\" : 0.0%n" +
+ " }%n" +
+ " },%n" +
+ " \"meters\" : {%n" +
+ " \"m\" : {%n" +
+ " \"count\" : 1,%n" +
+ " \"m15_rate\" : 0.0,%n" +
+ " \"m1_rate\" : 0.0,%n" +
+ " \"m5_rate\" : 0.0,%n" +
+ " \"mean_rate\" : 2.0E8,%n" +
+ " \"units\" : \"events/minute\"%n" +
+ " }%n" +
+ " },%n" +
+ " \"timers\" : {%n" +
+ " \"t\" : {%n" +
+ " \"count\" : 1,%n" +
+ " \"max\" : 1000.0,%n" +
+ " \"mean\" : 1000.0,%n" +
+ " \"min\" : 1000.0,%n" +
+ " \"p50\" : 1000.0,%n" +
+ " \"p75\" : 1000.0,%n" +
+ " \"p95\" : 1000.0,%n" +
+ " \"p98\" : 1000.0,%n" +
+ " \"p99\" : 1000.0,%n" +
+ " \"p999\" : 1000.0,%n" +
+ " \"stddev\" : 0.0,%n" +
+ " \"m15_rate\" : 0.0,%n" +
+ " \"m1_rate\" : 0.0,%n" +
+ " \"m5_rate\" : 0.0,%n" +
+ " \"mean_rate\" : 6.0E8,%n" +
+ " \"duration_units\" : \"milliseconds\",%n" +
+ " \"rate_units\" : \"calls/minute\"%n" +
+ " }%n" +
+ " }%n" +
+ "}"));
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+}
diff --git a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/MetricsServletTest.java b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/MetricsServletTest.java
new file mode 100644
index 0000000000..c70a16f4f9
--- /dev/null
+++ b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/MetricsServletTest.java
@@ -0,0 +1,263 @@
+package io.dropwizard.metrics.servlets;
+
+import com.codahale.metrics.Clock;
+import com.codahale.metrics.ExponentiallyDecayingReservoir;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class MetricsServletTest extends AbstractServletTest {
+ private final Clock clock = mock(Clock.class);
+ private final MetricRegistry registry = new MetricRegistry();
+ private ServletTester tester;
+
+ @Override
+ protected void setUp(ServletTester tester) {
+ this.tester = tester;
+ tester.setAttribute("io.dropwizard.metrics.servlets.MetricsServlet.registry", registry);
+ tester.addServlet(io.dropwizard.metrics.servlets.MetricsServlet.class, "/metrics");
+ tester.getContext().setInitParameter("io.dropwizard.metrics.servlets.MetricsServlet.allowedOrigin", "*");
+ }
+
+ @Before
+ public void setUp() {
+ // provide ticks for the setup (calls getTick 6 times). The serialization in the tests themselves
+ // will call getTick again several times and always get the same value (the last specified here)
+ when(clock.getTick()).thenReturn(100L, 100L, 200L, 300L, 300L, 400L);
+
+ registry.register("g1", (Gauge) () -> 100L);
+ registry.counter("c").inc();
+ registry.histogram("h").update(1);
+ registry.register("m", new Meter(clock)).mark();
+ registry.register("t", new Timer(new ExponentiallyDecayingReservoir(), clock))
+ .update(1, TimeUnit.SECONDS);
+
+ request.setMethod("GET");
+ request.setURI("/metrics");
+ request.setVersion("HTTP/1.0");
+ }
+
+ @Test
+ public void returnsA200() throws Exception {
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ assertThat(response.get("Access-Control-Allow-Origin"))
+ .isEqualTo("*");
+ assertThat(response.getContent())
+ .isEqualTo("{" +
+ "\"version\":\"4.0.0\"," +
+ "\"gauges\":{" +
+ "\"g1\":{\"value\":100}" +
+ "}," +
+ "\"counters\":{" +
+ "\"c\":{\"count\":1}" +
+ "}," +
+ "\"histograms\":{" +
+ "\"h\":{\"count\":1,\"max\":1,\"mean\":1.0,\"min\":1,\"p50\":1.0,\"p75\":1.0,\"p95\":1.0,\"p98\":1.0,\"p99\":1.0,\"p999\":1.0,\"stddev\":0.0}" +
+ "}," +
+ "\"meters\":{" +
+ "\"m\":{\"count\":1,\"m15_rate\":0.0,\"m1_rate\":0.0,\"m5_rate\":0.0,\"mean_rate\":3333333.3333333335,\"units\":\"events/second\"}},\"timers\":{\"t\":{\"count\":1,\"max\":1.0,\"mean\":1.0,\"min\":1.0,\"p50\":1.0,\"p75\":1.0,\"p95\":1.0,\"p98\":1.0,\"p99\":1.0,\"p999\":1.0,\"stddev\":0.0,\"m15_rate\":0.0,\"m1_rate\":0.0,\"m5_rate\":0.0,\"mean_rate\":1.0E7,\"duration_units\":\"seconds\",\"rate_units\":\"calls/second\"}" +
+ "}" +
+ "}");
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+
+ @Test
+ public void returnsJsonWhenJsonpInitParamNotSet() throws Exception {
+ String callbackParamName = "callbackParam";
+ String callbackParamVal = "callbackParamVal";
+ request.setURI("/metrics?" + callbackParamName + "=" + callbackParamVal);
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ assertThat(response.get("Access-Control-Allow-Origin"))
+ .isEqualTo("*");
+ assertThat(response.getContent())
+ .isEqualTo("{" +
+ "\"version\":\"4.0.0\"," +
+ "\"gauges\":{" +
+ "\"g1\":{\"value\":100}" +
+ "}," +
+ "\"counters\":{" +
+ "\"c\":{\"count\":1}" +
+ "}," +
+ "\"histograms\":{" +
+ "\"h\":{\"count\":1,\"max\":1,\"mean\":1.0,\"min\":1,\"p50\":1.0,\"p75\":1.0,\"p95\":1.0,\"p98\":1.0,\"p99\":1.0,\"p999\":1.0,\"stddev\":0.0}" +
+ "}," +
+ "\"meters\":{" +
+ "\"m\":{\"count\":1,\"m15_rate\":0.0,\"m1_rate\":0.0,\"m5_rate\":0.0,\"mean_rate\":3333333.3333333335,\"units\":\"events/second\"}},\"timers\":{\"t\":{\"count\":1,\"max\":1.0,\"mean\":1.0,\"min\":1.0,\"p50\":1.0,\"p75\":1.0,\"p95\":1.0,\"p98\":1.0,\"p99\":1.0,\"p999\":1.0,\"stddev\":0.0,\"m15_rate\":0.0,\"m1_rate\":0.0,\"m5_rate\":0.0,\"mean_rate\":1.0E7,\"duration_units\":\"seconds\",\"rate_units\":\"calls/second\"}" +
+ "}" +
+ "}");
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+
+ @Test
+ public void returnsJsonpWhenInitParamSet() throws Exception {
+ String callbackParamName = "callbackParam";
+ String callbackParamVal = "callbackParamVal";
+ request.setURI("/metrics?" + callbackParamName + "=" + callbackParamVal);
+ tester.getContext().setInitParameter("io.dropwizard.metrics.servlets.MetricsServlet.jsonpCallback", callbackParamName);
+ processRequest();
+
+ assertThat(response.getStatus()).isEqualTo(200);
+ assertThat(response.get("Access-Control-Allow-Origin"))
+ .isEqualTo("*");
+ assertThat(response.getContent())
+ .isEqualTo(callbackParamVal + "({" +
+ "\"version\":\"4.0.0\"," +
+ "\"gauges\":{" +
+ "\"g1\":{\"value\":100}" +
+ "}," +
+ "\"counters\":{" +
+ "\"c\":{\"count\":1}" +
+ "}," +
+ "\"histograms\":{" +
+ "\"h\":{\"count\":1,\"max\":1,\"mean\":1.0,\"min\":1,\"p50\":1.0,\"p75\":1.0,\"p95\":1.0,\"p98\":1.0,\"p99\":1.0,\"p999\":1.0,\"stddev\":0.0}" +
+ "}," +
+ "\"meters\":{" +
+ "\"m\":{\"count\":1,\"m15_rate\":0.0,\"m1_rate\":0.0,\"m5_rate\":0.0,\"mean_rate\":3333333.3333333335,\"units\":\"events/second\"}},\"timers\":{\"t\":{\"count\":1,\"max\":1.0,\"mean\":1.0,\"min\":1.0,\"p50\":1.0,\"p75\":1.0,\"p95\":1.0,\"p98\":1.0,\"p99\":1.0,\"p999\":1.0,\"stddev\":0.0,\"m15_rate\":0.0,\"m1_rate\":0.0,\"m5_rate\":0.0,\"mean_rate\":1.0E7,\"duration_units\":\"seconds\",\"rate_units\":\"calls/second\"}" +
+ "}" +
+ "})");
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+
+ @Test
+ public void optionallyPrettyPrintsTheJson() throws Exception {
+ request.setURI("/metrics?pretty=true");
+
+ processRequest();
+
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ assertThat(response.get("Access-Control-Allow-Origin"))
+ .isEqualTo("*");
+ assertThat(response.getContent())
+ .isEqualTo(String.format("{%n" +
+ " \"version\" : \"4.0.0\",%n" +
+ " \"gauges\" : {%n" +
+ " \"g1\" : {%n" +
+ " \"value\" : 100%n" +
+ " }%n" +
+ " },%n" +
+ " \"counters\" : {%n" +
+ " \"c\" : {%n" +
+ " \"count\" : 1%n" +
+ " }%n" +
+ " },%n" +
+ " \"histograms\" : {%n" +
+ " \"h\" : {%n" +
+ " \"count\" : 1,%n" +
+ " \"max\" : 1,%n" +
+ " \"mean\" : 1.0,%n" +
+ " \"min\" : 1,%n" +
+ " \"p50\" : 1.0,%n" +
+ " \"p75\" : 1.0,%n" +
+ " \"p95\" : 1.0,%n" +
+ " \"p98\" : 1.0,%n" +
+ " \"p99\" : 1.0,%n" +
+ " \"p999\" : 1.0,%n" +
+ " \"stddev\" : 0.0%n" +
+ " }%n" +
+ " },%n" +
+ " \"meters\" : {%n" +
+ " \"m\" : {%n" +
+ " \"count\" : 1,%n" +
+ " \"m15_rate\" : 0.0,%n" +
+ " \"m1_rate\" : 0.0,%n" +
+ " \"m5_rate\" : 0.0,%n" +
+ " \"mean_rate\" : 3333333.3333333335,%n" +
+ " \"units\" : \"events/second\"%n" +
+ " }%n" +
+ " },%n" +
+ " \"timers\" : {%n" +
+ " \"t\" : {%n" +
+ " \"count\" : 1,%n" +
+ " \"max\" : 1.0,%n" +
+ " \"mean\" : 1.0,%n" +
+ " \"min\" : 1.0,%n" +
+ " \"p50\" : 1.0,%n" +
+ " \"p75\" : 1.0,%n" +
+ " \"p95\" : 1.0,%n" +
+ " \"p98\" : 1.0,%n" +
+ " \"p99\" : 1.0,%n" +
+ " \"p999\" : 1.0,%n" +
+ " \"stddev\" : 0.0,%n" +
+ " \"m15_rate\" : 0.0,%n" +
+ " \"m1_rate\" : 0.0,%n" +
+ " \"m5_rate\" : 0.0,%n" +
+ " \"mean_rate\" : 1.0E7,%n" +
+ " \"duration_units\" : \"seconds\",%n" +
+ " \"rate_units\" : \"calls/second\"%n" +
+ " }%n" +
+ " }%n" +
+ "}"));
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("application/json");
+ }
+
+ @Test
+ public void constructorWithRegistryAsArgumentIsUsedInPreferenceOverServletConfig() throws Exception {
+ final MetricRegistry metricRegistry = mock(MetricRegistry.class);
+ final ServletContext servletContext = mock(ServletContext.class);
+ final ServletConfig servletConfig = mock(ServletConfig.class);
+ when(servletConfig.getServletContext()).thenReturn(servletContext);
+
+ final io.dropwizard.metrics.servlets.MetricsServlet metricsServlet = new io.dropwizard.metrics.servlets.MetricsServlet(metricRegistry);
+ metricsServlet.init(servletConfig);
+
+ verify(servletConfig, times(1)).getServletContext();
+ verify(servletContext, never()).getAttribute(eq(io.dropwizard.metrics.servlets.MetricsServlet.METRICS_REGISTRY));
+ }
+
+ @Test
+ public void constructorWithRegistryAsArgumentUsesServletConfigWhenNull() throws Exception {
+ final MetricRegistry metricRegistry = mock(MetricRegistry.class);
+ final ServletContext servletContext = mock(ServletContext.class);
+ final ServletConfig servletConfig = mock(ServletConfig.class);
+ when(servletConfig.getServletContext()).thenReturn(servletContext);
+ when(servletContext.getAttribute(eq(io.dropwizard.metrics.servlets.MetricsServlet.METRICS_REGISTRY)))
+ .thenReturn(metricRegistry);
+
+ final io.dropwizard.metrics.servlets.MetricsServlet metricsServlet = new io.dropwizard.metrics.servlets.MetricsServlet(null);
+ metricsServlet.init(servletConfig);
+
+ verify(servletConfig, times(1)).getServletContext();
+ verify(servletContext, times(1)).getAttribute(eq(io.dropwizard.metrics.servlets.MetricsServlet.METRICS_REGISTRY));
+ }
+
+ @Test(expected = ServletException.class)
+ public void constructorWithRegistryAsArgumentUsesServletConfigWhenNullButWrongTypeInContext() throws Exception {
+ final ServletContext servletContext = mock(ServletContext.class);
+ final ServletConfig servletConfig = mock(ServletConfig.class);
+ when(servletConfig.getServletContext()).thenReturn(servletContext);
+ when(servletContext.getAttribute(eq(io.dropwizard.metrics.servlets.MetricsServlet.METRICS_REGISTRY)))
+ .thenReturn("IRELLEVANT_STRING");
+
+ final io.dropwizard.metrics.servlets.MetricsServlet metricsServlet = new MetricsServlet(null);
+ metricsServlet.init(servletConfig);
+ }
+}
diff --git a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/PingServletTest.java b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/PingServletTest.java
new file mode 100644
index 0000000000..a0685601df
--- /dev/null
+++ b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/PingServletTest.java
@@ -0,0 +1,49 @@
+package io.dropwizard.metrics.servlets;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PingServletTest extends AbstractServletTest {
+ @Override
+ protected void setUp(ServletTester tester) {
+ tester.addServlet(PingServlet.class, "/ping");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ request.setMethod("GET");
+ request.setURI("/ping");
+ request.setVersion("HTTP/1.0");
+
+ processRequest();
+ }
+
+ @Test
+ public void returns200OK() {
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ }
+
+ @Test
+ public void returnsPong() {
+ assertThat(response.getContent())
+ .isEqualTo(String.format("pong%n"));
+ }
+
+ @Test
+ public void returnsTextPlain() {
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("text/plain;charset=ISO-8859-1");
+ }
+
+ @Test
+ public void returnsUncacheable() {
+ assertThat(response.get(HttpHeader.CACHE_CONTROL))
+ .isEqualTo("must-revalidate,no-cache,no-store");
+
+ }
+}
diff --git a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/ThreadDumpServletTest.java b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/ThreadDumpServletTest.java
new file mode 100644
index 0000000000..af4db51f21
--- /dev/null
+++ b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/ThreadDumpServletTest.java
@@ -0,0 +1,49 @@
+package io.dropwizard.metrics.servlets;
+
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ThreadDumpServletTest extends AbstractServletTest {
+ @Override
+ protected void setUp(ServletTester tester) {
+ tester.addServlet(ThreadDumpServlet.class, "/threads");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ request.setMethod("GET");
+ request.setURI("/threads");
+ request.setVersion("HTTP/1.0");
+
+ processRequest();
+ }
+
+ @Test
+ public void returns200OK() {
+ assertThat(response.getStatus())
+ .isEqualTo(200);
+ }
+
+ @Test
+ public void returnsAThreadDump() {
+ assertThat(response.getContent())
+ .contains("Finalizer");
+ }
+
+ @Test
+ public void returnsTextPlain() {
+ assertThat(response.get(HttpHeader.CONTENT_TYPE))
+ .isEqualTo("text/plain");
+ }
+
+ @Test
+ public void returnsUncacheable() {
+ assertThat(response.get(HttpHeader.CACHE_CONTROL))
+ .isEqualTo("must-revalidate,no-cache,no-store");
+
+ }
+}
diff --git a/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/experiments/ExampleServer.java b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/experiments/ExampleServer.java
new file mode 100644
index 0000000000..5fb5db441b
--- /dev/null
+++ b/metrics-jakarta-servlets/src/test/java/io/dropwizard/metrics/servlets/experiments/ExampleServer.java
@@ -0,0 +1,60 @@
+package io.dropwizard.metrics.servlets.experiments;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.health.HealthCheckRegistry;
+import io.dropwizard.metrics.jetty11.InstrumentedConnectionFactory;
+import io.dropwizard.metrics.jetty11.InstrumentedHandler;
+import io.dropwizard.metrics.jetty11.InstrumentedQueuedThreadPool;
+import io.dropwizard.metrics.servlets.AdminServlet;
+import io.dropwizard.metrics.servlets.HealthCheckServlet;
+import io.dropwizard.metrics.servlets.MetricsServlet;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.thread.ThreadPool;
+
+import static com.codahale.metrics.MetricRegistry.name;
+
+public class ExampleServer {
+ private static final MetricRegistry REGISTRY = new MetricRegistry();
+ private static final Counter COUNTER_1 = REGISTRY.counter(name(ExampleServer.class, "wah", "doody"));
+ private static final Counter COUNTER_2 = REGISTRY.counter(name(ExampleServer.class, "woo"));
+
+ static {
+ REGISTRY.register(name(ExampleServer.class, "boo"), (Gauge) () -> {
+ throw new RuntimeException("asplode!");
+ });
+ }
+
+ public static void main(String[] args) throws Exception {
+ COUNTER_1.inc();
+ COUNTER_2.inc();
+
+ final ThreadPool threadPool = new InstrumentedQueuedThreadPool(REGISTRY);
+ final Server server = new Server(threadPool);
+
+ final Connector connector = new ServerConnector(server, new InstrumentedConnectionFactory(
+ new HttpConnectionFactory(), REGISTRY.timer("http.connection")));
+ server.addConnector(connector);
+
+ final ServletContextHandler context = new ServletContextHandler();
+ context.setContextPath("/initial");
+ context.setAttribute(MetricsServlet.METRICS_REGISTRY, REGISTRY);
+ context.setAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY, new HealthCheckRegistry());
+
+ final ServletHolder holder = new ServletHolder(new AdminServlet());
+ context.addServlet(holder, "/dingo/*");
+
+ final InstrumentedHandler handler = new InstrumentedHandler(REGISTRY);
+ handler.setHandler(context);
+ server.setHandler(handler);
+
+ server.start();
+ server.join();
+ }
+}
diff --git a/pom.xml b/pom.xml
index 9eb5141502..41f97c7378 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,6 +26,8 @@
metrics-httpclient
metrics-httpclient5
metrics-httpasyncclient
+ metrics-jakarta-servlet
+ metrics-jakarta-servlets
metrics-jcache
metrics-jcstress
metrics-jdbi