Skip to content

Instantly share code, notes, and snippets.

@noslowerdna
Last active March 14, 2016 14:07
Show Gist options
  • Save noslowerdna/6572c1113a1c770c95a7 to your computer and use it in GitHub Desktop.
Save noslowerdna/6572c1113a1c770c95a7 to your computer and use it in GitHub Desktop.
import com.yammer.metrics.Metrics;
import com.yammer.metrics.core.Counter;
import com.yammer.metrics.core.Meter;
import com.yammer.metrics.core.Timer;
import com.yammer.metrics.core.TimerContext;
import com.yammer.metrics.web.WebappMetricsFilter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* {@link Filter} implementation which captures request information and a breakdown of the response
* codes being returned.
*
* <p>
* Copied from {@link WebappMetricsFilter} and {@link com.yammer.metrics.web.DefaultWebappMetricsFilter} in metrics-web, which do
* not accurately track response statuses as of version 2.2.0, because {@link HttpServletResponseWrapper#setStatus(int, String)} is
* not intercepted, and that is the method that ultimately is getting called to set the status for the response.
* </p>
*/
public class UpdatedMetricsFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(MetricsFilter.class);
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;
// Additional status codes used by our services.
private static final int UNAUTHORIZED = 401;
private static final int FORBIDDEN = 403;
private static final int TIMEOUT = 408;
private static final int CONFLICT = 409;
private static final int PRECONDITION_FAILED = 412;
private static final int SERVICE_UNAVAILABLE = 503;
private final Map<Integer, Meter> metersByStatusCode;
private final Map<StatusType, Meter> metersByStatusType;
private final Meter otherMeter;
private final Counter activeRequests;
private final Timer requestTimer;
/**
* Enumerated response status groupings.
*/
private enum StatusType {
/**
* A status of 2xx indicates that the client's request was successfully received, understood, and accepted.
*/
SUCCESSFUL,
/**
* A status of 4xx is used for cases in which the client seems to have erred.
*/
CLIENT_ERROR,
/**
* A status of 5xx means that the server is aware that it has erred or is incapable of performing the request.
*/
SERVER_ERROR,
/**
* Fail-safe type for any response statuses that do not map to one of the other types.
*/
OTHER
}
/**
* Creates a new instance of the filter.
*/
public UpdatedMetricsFilter() {
Map<Integer, String> meterNamesByStatusCode = createMeterNamesByStatusCode();
this.metersByStatusCode = new ConcurrentHashMap<Integer, Meter>(meterNamesByStatusCode.size());
for (Entry<Integer, String> entry : meterNamesByStatusCode.entrySet()) {
metersByStatusCode.put(entry.getKey(), getMeter(entry.getValue()));
}
Map<StatusType, String> meterNamesByStatusType = createMeterNamesByStatusType();
this.metersByStatusType = new ConcurrentHashMap<StatusType, Meter>(meterNamesByStatusType.size());
for (Entry<StatusType, String> entry : meterNamesByStatusType.entrySet()) {
metersByStatusType.put(entry.getKey(), getMeter(entry.getValue()));
}
this.otherMeter = getMeter(NAME_PREFIX + "other");
this.activeRequests = Metrics.newCounter(WebappMetricsFilter.class, "activeRequests");
this.requestTimer = Metrics.newTimer(WebappMetricsFilter.class,
"requests",
TimeUnit.MILLISECONDS,
TimeUnit.SECONDS);
}
private static Meter getMeter(String name) {
return Metrics.newMeter(WebappMetricsFilter.class, name, "responses", TimeUnit.SECONDS);
}
private static Map<Integer, String> createMeterNamesByStatusCode() {
final Map<Integer, String> meterNamesByStatusCode = new HashMap<Integer, String>();
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");
meterNamesByStatusCode.put(UNAUTHORIZED, NAME_PREFIX + "unauthorized");
meterNamesByStatusCode.put(FORBIDDEN, NAME_PREFIX + "forbidden");
meterNamesByStatusCode.put(TIMEOUT, NAME_PREFIX + "timeout");
meterNamesByStatusCode.put(CONFLICT, NAME_PREFIX + "conflict");
meterNamesByStatusCode.put(PRECONDITION_FAILED, NAME_PREFIX + "preconditionFailed");
meterNamesByStatusCode.put(SERVICE_UNAVAILABLE, NAME_PREFIX + "serviceUnavailable");
return meterNamesByStatusCode;
}
private static Map<StatusType, String> createMeterNamesByStatusType() {
final Map<StatusType, String> meterNamesByStatusType = new HashMap<StatusType, String>();
meterNamesByStatusType.put(StatusType.SUCCESSFUL, NAME_PREFIX + "2xx");
meterNamesByStatusType.put(StatusType.CLIENT_ERROR, NAME_PREFIX + "4xx");
meterNamesByStatusType.put(StatusType.SERVER_ERROR, NAME_PREFIX + "5xx");
return meterNamesByStatusType;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@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 TimerContext context = requestTimer.time();
try {
chain.doFilter(request, wrappedResponse);
} finally {
context.stop();
activeRequests.dec();
markMeterForStatusCode(wrappedResponse.getStatus());
}
}
private void markMeterForStatusCode(int status) {
final Meter statusCodeMeter = metersByStatusCode.get(status);
if (statusCodeMeter != null) {
statusCodeMeter.mark();
} else {
otherMeter.mark();
}
final Meter statusTypeMeter = metersByStatusType.get(getStatusType(status));
if (statusTypeMeter != null) {
statusTypeMeter.mark();
}
}
private static StatusType getStatusType(int status) {
switch (status / 100) {
case 2:
// 2xx
return StatusType.SUCCESSFUL;
case 4:
// 4xx
return StatusType.CLIENT_ERROR;
case 5:
// 5xx
return StatusType.SERVER_ERROR;
default:
// Anything else is very unlikely.
LOG.debug("Unexpected status outside of the standard 2xx/4xx/5xx groups: {}", status);
return StatusType.OTHER;
}
}
private static class StatusExposingServletResponse extends HttpServletResponseWrapper {
private int httpStatus;
/**
* Creates a new status-exposing response wrapper instance.
*
* @param response the wrapped response
*/
public StatusExposingServletResponse(HttpServletResponse response) {
super(response);
// Defaults to 200 if not set, in compliance with the servlet specification.
this.httpStatus = 200;
}
@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 void setStatus(int sc, String sm) {
httpStatus = sc;
super.setStatus(sc, sm);
}
/**
* Returns the intercepted response status code.
*
* @return the status code of the response.
*/
public int getStatus() {
return httpStatus;
}
}
}
@noslowerdna
Copy link
Author

<filter>
    <filter-name>webappMetricsFilter</filter-name>
    <filter-class>UpdatedMetricsFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>webappMetricsFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment