Skip to content

Commit

Permalink
Add support for Jakarta Servlet 6.x (#2652)
Browse files Browse the repository at this point in the history
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
  • Loading branch information
renovate[bot] committed Aug 6, 2023
1 parent 59c3544 commit 95e28e3
Show file tree
Hide file tree
Showing 10 changed files with 385 additions and 3 deletions.
5 changes: 5 additions & 0 deletions metrics-bom/pom.xml
Expand Up @@ -75,6 +75,11 @@
<artifactId>metrics-jakarta-servlet</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-jakarta-servlet6</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-jakarta-servlets</artifactId>
Expand Down
59 changes: 59 additions & 0 deletions metrics-jakarta-servlet6/pom.xml
@@ -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>
@@ -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 {

}
}
}
@@ -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;
}
}
@@ -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) {
}
}
@@ -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);
}
}

0 comments on commit 95e28e3

Please sign in to comment.