Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Logging Spring REST APIs

DZone's Guide to

Logging Spring REST APIs

In this article, we take a look at how Java developers can approach the logging of Spring-based REST APIs. Put on your lumberjack hat!

· Integration Zone ·
Free Resource

SnapLogic is the leading self-service enterprise-grade integration platform. Download the 2018 GartnerMagic Quadrant for Enterprise iPaaS or play around on the platform, risk free, for 30 days.

In this article, I will try to explain an approach for logging Spring-based Rest APIs. Although Spring provides tracing of request and responses using its Actuator, logging the request and response body is not supported out of the box. This sometimes makes it difficult to find what is coming to and going from the Rest APIs.

There are several approaches for writing a logger for Spring applications. One of them is writing a listener. But when you write a listener, it starts to work for all the APIs. In this demonstration, we will focus on logging only the APIs that we annotate with our own logging annotation. 

Before the logging part, we have to change Spring’s default HttpServletRequestWrapper to our own, because default one doesn’t support caching. With the default wrapper, when Spring initializes the API parameters, it opens the input stream and reads the content. When it finishes, it closes the stream, and reading from the stream from that time is not possible and any attempt results in an exception. We will replace this with ContentCachingRequestWrapper, which allows for the caching of a request. For registering this wrapper, we will extend DispatcherServlet and override the doDispatch method.

You can find the implementation details below.

public class LoggableDispatcherServlet extends DispatcherServlet {
@Override
protected void doDispatch(HttpServletRequest request, 
                              HttpServletResponse response) throws Exception {
if (!(request instanceof ContentCachingRequestWrapper)) {
request = new ContentCachingRequestWrapper(request);
}
super.doDispatch(request, response);
}
}

As you can see, this dispatcher replaces Spring’s request wrapper with ContentCachingRequestWrapper, so we will be able to read request parameters as much as we want without any error.

After this, we have to register our own dispatcher servlet to a Spring context like below, this can be achieved in any of the Spring configuration files.

@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet() {
return new LoggableDispatcherServlet();
}

Now Spring will start using our new dispatcher servlet. After this, we will create a new annotation type for logging.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestLogger {
}

This will allow us to annotate our Rest APIs that we want to log. This way we will be able to log only the APIs we want.

And, now, we come to the implementation of logging. The implementation details for the annotation are below.

@Aspect
@Component
public class RequestLoggerAspect {

private Logger logger = LoggerFactory.getLogger(RequestLoggerAspect.class);

@Autowired
private ObjectMapper objectMapper;

@Around("execution(@RequestLogger * *(..)) && @annotation(requestLogger)")
public Object logRequest(ProceedingJoinPoint joinPoint, RequestLogger requestLogger) throws Throwable {

ContentCachingRequestWrapper request = getWrapper(joinPoint);

StringBuilder apiLog = new StringBuilder();

apiLog.append("Rest API: ").append(request.getRequestURL().toString()).append("\n");

apiLog.append("Body:").append(getRequestBody(request)).append("\n");

for (String header : Collections.list(request.getHeaderNames())) {
apiLog.append(header).append(":").append(request.getHeader(header))
.append("\n");
}

logger.debug(apiLog.toString());

Object retVal = joinPoint.proceed();

logger.debug("Response:" + objectMapper.writeValueAsString(retVal));

return retVal;

}

private String getRequestBody(final ContentCachingRequestWrapper wrapper) {
String payload = null;
if (wrapper != null) {

byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
try {
int maxLength = buf.length > 500 ? 500 : buf.length;

payload = new String(buf, 0, maxLength,
wrapper.getCharacterEncoding());
} catch (UnsupportedEncodingException e) {
logger.error("UnsupportedEncoding.", e);
}
}
}
return payload;
}

private ContentCachingRequestWrapper getWrapper(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();

ContentCachingRequestWrapper request = null;

for (Object arg : args) {
if (arg instanceof ContentCachingRequestWrapper) {
request = (ContentCachingRequestWrapper) arg;
break;
}
}

return request;
}

If you look at line 13, we start with getting the ContentCachingRequestWrapper object from the jointPoint object. To achieve this, HttpServletRequest should be included in the specified Rest API method's parameters. If you look at the other lines, we are able to get an API URL, request body, and headers from the ContentCachingRequestWrapper object.

For getting the body part, please have a look at the getRequestBody method on line 36. In this method, if the length of the body is greater than 500, it is cut to 500. This magic number (500) can be parameterized in a more serious project and fine-tuned according to your performance and storage needs.

If you look at line 28, we call joinPoint.proceed(). This causes the API method to be executed. When the method returns, we have the chance to get the return value and log it, too. At this point, we have finished the logging of a Spring Rest API using annotation. If you want to further investigate, you can find a working sample project on GitHub. I hope you found the article helpful. 

With SnapLogic’s integration platform you can save millions of dollars, increase integrator productivity by 5X, and reduce integration time to value by 90%. Sign up for our risk-free 30-day trial!

Topics:
spring ,logging ,rest api ,integration

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}