I have a RESTful web service running under Glassfish 3.1.2 using Jersey and Jackson:
@Stateless
@LocalBean
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Path("users")
public class UserRestService {
private static final Logger log = ...;
@GET
@Path("{userId:[0-9]+}")
public User getUser(@PathParam("userId") Long userId) {
User user;
user = loadUserByIdAndThrowApplicableWebApplicationExceptionIfNotFound(userId);
return user;
}
}
For expected exceptions, I throw the appropriate WebApplicationException
, and I'm happy with the HTTP 500 status that is returned if an unexpected exception occurs.
I would now like to add logging for these unexpected exceptions, but despite searching, cannot find out how I should be going about this.
Fruitless Attempt
I have tried using a Thread.UncaughtExceptionHandler
and can confirm that it is applied inside the method body, but its uncaughtException
method is never called, as something else is handling the uncaught exceptions before they reach my handler.
Other Ideas: #1
Another option I've seen some people use is an ExceptionMapper
, which catches all exceptions and then filters out WebApplicationExceptions:
@Provider
public class ExampleExceptionMapper implements ExceptionMapper<Throwable> {
private static final Logger log = ...;
public Response toResponse(Throwable t) {
if (t instanceof WebApplicationException) {
return ((WebApplicationException)t).getResponse();
} else {
log.error("Uncaught exception thrown by REST service", t);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
// Add an entity, etc.
.build();
}
}
}
While this approach may work, it feels to me like misuse of what ExceptionMappers are supposed to be used for, that is, mapping certain exceptions to certain responses.
Other Ideas: #2
Most sample JAX-RS code returns the Response
object directly. Following this approach, I could change my code to something like:
public Response getUser(@PathParam("userId") Long userId) {
try {
User user;
user = loadUserByIdAndThrowApplicableWebApplicationExceptionIfNotFound(userId);
return Response.ok().entity(user).build();
} catch (Throwable t) {
return processException(t);
}
}
private Response processException(Throwable t) {
if (t instanceof WebApplicationException) {
return ((WebApplicationException)t).getResponse();
} else {
log.error("Uncaught exception thrown by REST service", t);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
// Add an entity, etc.
.build();
}
}
However, I'm leery of going this route, as my actual project is not as simple as this example, and I would have to implement this same pattern over and over again, not to mention having to manually build up the Responses.
What should I do?
Are there better methods for adding logging for uncaught exceptions?
Is there a "right" way of implementing this?
For lack of a better way to implement logging for uncaught JAX-RS exceptions, using a catch-all ExceptionMapper
as in Other Ideas: #1 seems to be the cleanest, simplest way to add this functionality.
Here's my implementation:
@Provider
public class ThrowableExceptionMapper implements ExceptionMapper<Throwable> {
private static final Logger log = Logger.getLogger(ThrowableExceptionMapper.class);
@Context
HttpServletRequest request;
@Override
public Response toResponse(Throwable t) {
if (t instanceof WebApplicationException) {
return ((WebApplicationException) t).getResponse();
} else {
String errorMessage = buildErrorMessage(request);
log.error(errorMessage, t);
return Response.serverError().entity("").build();
}
}
private String buildErrorMessage(HttpServletRequest req) {
StringBuilder message = new StringBuilder();
String entity = "(empty)";
try {
// How to cache getInputStream: http://stackoverflow.com/a/17129256/356408
InputStream is = req.getInputStream();
// Read an InputStream elegantly: http://stackoverflow.com/a/5445161/356408
Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A");
entity = s.hasNext() ? s.next() : entity;
} catch (Exception ex) {
// Ignore exceptions around getting the entity
}
message.append("Uncaught REST API exception:\n");
message.append("URL: ").append(getOriginalURL(req)).append("\n");
message.append("Method: ").append(req.getMethod()).append("\n");
message.append("Entity: ").append(entity).append("\n");
return message.toString();
}
private String getOriginalURL(HttpServletRequest req) {
// Rebuild the original request URL: http://stackoverflow.com/a/5212336/356408
String scheme = req.getScheme(); // http
String serverName = req.getServerName(); // hostname.com
int serverPort = req.getServerPort(); // 80
String contextPath = req.getContextPath(); // /mywebapp
String servletPath = req.getServletPath(); // /servlet/MyServlet
String pathInfo = req.getPathInfo(); // /a/b;c=123
String queryString = req.getQueryString(); // d=789
// Reconstruct original requesting URL
StringBuilder url = new StringBuilder();
url.append(scheme).append("://").append(serverName);
if (serverPort != 80 && serverPort != 443) {
url.append(":").append(serverPort);
}
url.append(contextPath).append(servletPath);
if (pathInfo != null) {
url.append(pathInfo);
}
if (queryString != null) {
url.append("?").append(queryString);
}
return url.toString();
}
}
Jersey (and JAX-RS 2.0) provides ContainerResponseFilter (and ContainerResponseFilter in JAX-RS 2.0).
Using Jersey version 1.x response filter would look like
public class ExceptionsLoggingContainerResponseFilter implements ContainerResponseFilter {
private final static Logger LOGGER = LoggerFactory.getLogger(ExceptionsLoggingContainerResponseFilter.class);
@Override
public ContainerResponse filter(ContainerRequest request, ContainerResponse response) {
Throwable throwable = response.getMappedThrowable();
if (throwable != null) {
LOGGER.info(buildErrorMessage(request), throwable);
}
return response;
}
private String buildErrorMessage(ContainerRequest request) {
StringBuilder message = new StringBuilder();
message.append("Uncaught REST API exception:\n");
message.append("URL: ").append(request.getRequestUri()).append("\n");
message.append("Method: ").append(request.getMethod()).append("\n");
message.append("Entity: ").append(extractDisplayableEntity(request)).append("\n");
return message.toString();
}
private String extractDisplayableEntity(ContainerRequest request) {
String entity = request.getEntity(String.class);
return entity.equals("") ? "(blank)" : entity;
}
}
Filter should be registered to Jersey. In web.xml the following parameter should be set to Jersey servlet:
<init-param>
<param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name>
<param-value>my.package.ExceptionsLoggingContainerResponseFilter</param-value>
</init-param>
Furhtermore, entity should be buffered. It can be done in various ways: using servlet level buffering (as Ashley Ross pointed out https://stackoverflow.com/a/17129256/356408) or using ContainerRequestFilter.
Approach #1 is perfect except for one problem: you end up catching WebApplicationException
. It's important to let the WebApplicationException
pass through unhindered because it will either invoke default logic (e.g. NotFoundException
) or it can carry a specific Response
that the resource crafted for a particular error condition.
Luckily, if you're using Jersey, you can use a modified Approach #1 and implement the ExtendedExceptionMapper. It's extends from the standard ExceptionMapper
to add the ability to conditionally ignore certain types of exceptions. You can thereby filter out WebApplicationException
like so:
@Provider
public class UncaughtThrowableExceptionMapper implements ExtendedExceptionMapper<Throwable> {
@Override
public boolean isMappable(Throwable throwable) {
// ignore these guys and let jersey handle them
return !(throwable instanceof WebApplicationException);
}
@Override
public Response toResponse(Throwable throwable) {
// your uncaught exception handling logic here...
}
}
The accepted answer does not work (or even compile) in Jersey 2 because ContainerResponseFilter was totally changed.
I think the best answer I've found is @Adrian's answer in Jersey... how to log all exceptions, but still invoke ExceptionMappers where he used a RequestEventListener and focused on the RequestEvent.Type.ON_EXCEPTION.
However, I have provided another alternative below that is a spin on @stevevls answer here.
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status.Family;
import javax.ws.rs.ext.Provider;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.glassfish.jersey.spi.ExtendedExceptionMapper;
/**
* The purpose of this exception mapper is to log any exception that occurs.
* Contrary to the purpose of the interface it implements, it does not change or determine
* the response that is returned to the client.
* It does this by logging all exceptions passed to the isMappable and then always returning false.
*
*/
@Provider
public class LogAllExceptions implements ExtendedExceptionMapper<Throwable> {
private static final Logger logger = Logger.getLogger(LogAllExceptions.class);
@Override
public boolean isMappable(Throwable thro) {
/* Primarily, we don't want to log client errors (i.e. 400's) as an error. */
Level level = isServerError(thro) ? Level.ERROR : Level.INFO;
/* TODO add information about the request (using @Context). */
logger.log(level, "ThrowableLogger_ExceptionMapper logging error.", thro);
return false;
}
private boolean isServerError(Throwable thro) {
/* Note: We consider anything that is not an instance of WebApplicationException a server error. */
return thro instanceof WebApplicationException
&& isServerError((WebApplicationException)thro);
}
private boolean isServerError(WebApplicationException exc) {
return exc.getResponse().getStatusInfo().getFamily().equals(Family.SERVER_ERROR);
}
@Override
public Response toResponse(Throwable throwable) {
//assert false;
logger.fatal("ThrowableLogger_ExceptionMapper.toResponse: This should not have been called.");
throw new RuntimeException("This should not have been called");
}
}
They are probably already logged, all you need to find and enable proper logger. For example under Spring Boot + Jersey, all you need is to add a line to application.properties
:
logging.level.org.glassfish.jersey.server.ServerRuntime$Responder=TRACE