Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Jakarta Servlet 6.x (major) #2652

Merged
merged 1 commit into from Aug 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}
}