diff --git a/metrics-bom/pom.xml b/metrics-bom/pom.xml index 28953ed1e3..24f90e942b 100644 --- a/metrics-bom/pom.xml +++ b/metrics-bom/pom.xml @@ -60,6 +60,16 @@ metrics-httpasyncclient ${project.version} + + io.dropwizard.metrics + metrics-jakarta-servlet + ${project.version} + + + io.dropwizard.metrics + metrics-jakarta-servlets + ${project.version} + io.dropwizard.metrics metrics-jcache diff --git a/metrics-jakarta-servlet/pom.xml b/metrics-jakarta-servlet/pom.xml new file mode 100644 index 0000000000..49d81475bf --- /dev/null +++ b/metrics-jakarta-servlet/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + + io.dropwizard.metrics + metrics-parent + 4.2.0-SNAPSHOT + + + metrics-jakarta-servlet + Metrics Integration for Jakarta Servlets + bundle + + An instrumented filter for servlet environments. + + + + io.dropwizard.metrics.servlet + 5.0.0 + + + + + + io.dropwizard.metrics + metrics-bom + ${project.version} + pom + import + + + + + + + io.dropwizard.metrics + metrics-core + + + jakarta.servlet + jakarta.servlet-api + ${servlet.version} + provided + + + diff --git a/metrics-jakarta-servlet/src/main/java/io/dropwizard/metrics/servlet/AbstractInstrumentedFilter.java b/metrics-jakarta-servlet/src/main/java/io/dropwizard/metrics/servlet/AbstractInstrumentedFilter.java new file mode 100644 index 0000000000..c895565e1d --- /dev/null +++ b/metrics-jakarta-servlet/src/main/java/io/dropwizard/metrics/servlet/AbstractInstrumentedFilter.java @@ -0,0 +1,218 @@ +package io.dropwizard.metrics.servlet; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; + +import java.io.IOException; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.codahale.metrics.MetricRegistry.name; + +/** + * {@link Filter} implementation which captures request information and a breakdown of the response + * codes being returned. + */ +public abstract class AbstractInstrumentedFilter implements Filter { + static final String METRIC_PREFIX = "name-prefix"; + + private final String otherMetricName; + private final Map meterNamesByStatusCode; + private final String registryAttribute; + + // initialized after call of init method + private ConcurrentMap metersByStatusCode; + private Meter otherMeter; + private Meter timeoutsMeter; + private Meter errorsMeter; + private Counter activeRequests; + private Timer requestTimer; + + + /** + * Creates a new instance of the filter. + * + * @param registryAttribute the attribute used to look up the metrics registry in the + * servlet context + * @param meterNamesByStatusCode A map, keyed by status code, of meter names that we are + * interested in. + * @param otherMetricName The name used for the catch-all meter. + */ + protected AbstractInstrumentedFilter(String registryAttribute, + Map meterNamesByStatusCode, + String otherMetricName) { + this.registryAttribute = registryAttribute; + this.otherMetricName = otherMetricName; + this.meterNamesByStatusCode = meterNamesByStatusCode; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + final MetricRegistry metricsRegistry = getMetricsFactory(filterConfig); + + String metricName = filterConfig.getInitParameter(METRIC_PREFIX); + if (metricName == null || metricName.isEmpty()) { + metricName = getClass().getName(); + } + + this.metersByStatusCode = new ConcurrentHashMap<>(meterNamesByStatusCode.size()); + for (Entry 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