-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for Jakarta Servlet 6.x (#2652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
- Loading branch information
1 parent
59c3544
commit 95e28e3
Showing
10 changed files
with
385 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<parent> | ||
<groupId>io.dropwizard.metrics</groupId> | ||
<artifactId>metrics-parent</artifactId> | ||
<version>4.2.20-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>metrics-jakarta-servlet6</artifactId> | ||
<name>Metrics Integration for Jakarta Servlets 6.x</name> | ||
<packaging>bundle</packaging> | ||
<description> | ||
An instrumented filter for servlet 6.x environments. | ||
</description> | ||
|
||
<properties> | ||
<javaModuleName>io.dropwizard.metrics.servlet</javaModuleName> | ||
<servlet6.version>6.0.0</servlet6.version> | ||
</properties> | ||
|
||
<dependencyManagement> | ||
<dependencies> | ||
<dependency> | ||
<groupId>io.dropwizard.metrics</groupId> | ||
<artifactId>metrics-bom</artifactId> | ||
<version>${project.version}</version> | ||
<type>pom</type> | ||
<scope>import</scope> | ||
</dependency> | ||
</dependencies> | ||
</dependencyManagement> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>io.dropwizard.metrics</groupId> | ||
<artifactId>metrics-core</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>jakarta.servlet</groupId> | ||
<artifactId>jakarta.servlet-api</artifactId> | ||
<version>${servlet6.version}</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>junit</groupId> | ||
<artifactId>junit</artifactId> | ||
<version>${junit.version}</version> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.mockito</groupId> | ||
<artifactId>mockito-core</artifactId> | ||
<version>${mockito.version}</version> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
</project> |
211 changes: 211 additions & 0 deletions
211
...rta-servlet6/src/main/java/io/dropwizard/metrics/servlet6/AbstractInstrumentedFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Integer, String> meterNamesByStatusCode; | ||
private final String registryAttribute; | ||
|
||
// initialized after call of init method | ||
private ConcurrentMap<Integer, Meter> 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<Integer, String> 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<Integer, String> 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 { | ||
|
||
} | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
...ics-jakarta-servlet6/src/main/java/io/dropwizard/metrics/servlet6/InstrumentedFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. <p>Use it in your servlet.xml like this:<p> | ||
* <pre>{@code | ||
* <filter> | ||
* <filter-name>instrumentedFilter</filter-name> | ||
* <filter-class>io.dropwizard.metrics.servlet.InstrumentedFilter</filter-class> | ||
* </filter> | ||
* <filter-mapping> | ||
* <filter-name>instrumentedFilter</filter-name> | ||
* <url-pattern>/*</url-pattern> | ||
* </filter-mapping> | ||
* }</pre> | ||
*/ | ||
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<Integer, String> createMeterNamesByStatusCode() { | ||
final Map<Integer, String> 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; | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
...vlet6/src/main/java/io/dropwizard/metrics/servlet6/InstrumentedFilterContextListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) { | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
...6/src/test/java/io/dropwizard/metrics/servlet6/InstrumentedFilterContextListenerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
Oops, something went wrong.