diff --git a/metrics-bom/pom.xml b/metrics-bom/pom.xml index 932f43d09f..9aa689d9bd 100644 --- a/metrics-bom/pom.xml +++ b/metrics-bom/pom.xml @@ -75,6 +75,11 @@ metrics-jakarta-servlet ${project.version} + + io.dropwizard.metrics + metrics-jakarta-servlet6 + ${project.version} + io.dropwizard.metrics metrics-jakarta-servlets diff --git a/metrics-jakarta-servlet/pom.xml b/metrics-jakarta-servlet/pom.xml index b851646f40..c4039ccb4d 100644 --- a/metrics-jakarta-servlet/pom.xml +++ b/metrics-jakarta-servlet/pom.xml @@ -17,7 +17,7 @@ io.dropwizard.metrics.servlet - 5.0.0 + 6.0.0 diff --git a/metrics-jakarta-servlet6/pom.xml b/metrics-jakarta-servlet6/pom.xml new file mode 100644 index 0000000000..aed6d93ee4 --- /dev/null +++ b/metrics-jakarta-servlet6/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + io.dropwizard.metrics + metrics-parent + 4.2.20-SNAPSHOT + + + metrics-jakarta-servlet6 + Metrics Integration for Jakarta Servlets 6.x + bundle + + An instrumented filter for servlet 6.x environments. + + + + io.dropwizard.metrics.servlet + 6.0.0 + + + + + + io.dropwizard.metrics + metrics-bom + ${project.version} + pom + import + + + + + + + io.dropwizard.metrics + metrics-core + + + jakarta.servlet + jakarta.servlet-api + ${servlet6.version} + provided + + + junit + junit + ${junit.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + diff --git a/metrics-jakarta-servlet6/src/main/java/io/dropwizard/metrics/servlet6/AbstractInstrumentedFilter.java b/metrics-jakarta-servlet6/src/main/java/io/dropwizard/metrics/servlet6/AbstractInstrumentedFilter.java new file mode 100644 index 0000000000..9134247583 --- /dev/null +++ b/metrics-jakarta-servlet6/src/main/java/io/dropwizard/metrics/servlet6/AbstractInstrumentedFilter.java @@ -0,0 +1,211 @@ +package io.dropwizard.metrics.servlet6; + +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 + public int getStatus() { + return httpStatus; + } + } + + private class AsyncResultListener implements AsyncListener { + private final 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-servlet6/src/main/java/io/dropwizard/metrics/servlet6/InstrumentedFilter.java b/metrics-jakarta-servlet6/src/main/java/io/dropwizard/metrics/servlet6/InstrumentedFilter.java new file mode 100644 index 0000000000..e4b37fdc79 --- /dev/null +++ b/metrics-jakarta-servlet6/src/main/java/io/dropwizard/metrics/servlet6/InstrumentedFilter.java @@ -0,0 +1,48 @@ +package io.dropwizard.metrics.servlet6; + +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-servlet6/src/main/java/io/dropwizard/metrics/servlet6/InstrumentedFilterContextListener.java b/metrics-jakarta-servlet6/src/main/java/io/dropwizard/metrics/servlet6/InstrumentedFilterContextListener.java new file mode 100644 index 0000000000..b9315847fe --- /dev/null +++ b/metrics-jakarta-servlet6/src/main/java/io/dropwizard/metrics/servlet6/InstrumentedFilterContextListener.java @@ -0,0 +1,26 @@ +package io.dropwizard.metrics.servlet6; + +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-servlet6/src/test/java/io/dropwizard/metrics/servlet6/InstrumentedFilterContextListenerTest.java b/metrics-jakarta-servlet6/src/test/java/io/dropwizard/metrics/servlet6/InstrumentedFilterContextListenerTest.java new file mode 100644 index 0000000000..74062ef7cc --- /dev/null +++ b/metrics-jakarta-servlet6/src/test/java/io/dropwizard/metrics/servlet6/InstrumentedFilterContextListenerTest.java @@ -0,0 +1,32 @@ +package io.dropwizard.metrics.servlet6; + +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.servlet6.InstrumentedFilter.registry", registry); + } +} diff --git a/metrics-jakarta-servlets/pom.xml b/metrics-jakarta-servlets/pom.xml index fd6f8ab211..678a34053d 100644 --- a/metrics-jakarta-servlets/pom.xml +++ b/metrics-jakarta-servlets/pom.xml @@ -19,7 +19,7 @@ io.dropwizard.metrics.servlets 1.1.1 - 5.0.0 + 6.0.0 2.12.7.1 2.0.7 diff --git a/metrics-servlet/pom.xml b/metrics-servlet/pom.xml index 644f5208d8..66155d237b 100644 --- a/metrics-servlet/pom.xml +++ b/metrics-servlet/pom.xml @@ -17,7 +17,7 @@ com.codahale.metrics.servlet - 3.1.0 + 4.0.1 diff --git a/metrics-servlets/pom.xml b/metrics-servlets/pom.xml index 46449a6a64..adbabefca6 100644 --- a/metrics-servlets/pom.xml +++ b/metrics-servlets/pom.xml @@ -19,7 +19,7 @@ com.codahale.metrics.servlets 1.1.1 - 3.1.0 + 4.0.1 2.12.7.1 diff --git a/pom.xml b/pom.xml index 10a70540e9..4628525be6 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ metrics-httpclient5 metrics-httpasyncclient metrics-jakarta-servlet + metrics-jakarta-servlet6 metrics-jakarta-servlets metrics-jcache metrics-jcstress