How to log incoming requests to all REST services in Spring Boot?

(As an alternate to the below solution you can check out this post , which is easier and more straightforward)

Let’s say you want to log all the requests to your REST APIs developed using Spring Boot in a centralized way.

You don’t want to add logs to each and every API.

How to do that in Spring Boot?

Using Spring Interceptors.

But it is in straight forward to use it.

You can implement it by following the below algorithm:

STEP1 : Create a spring handler interceptor and log all incoming requests.

STEP2: Register the interceptor so that Spring Boot is aware of it.

STEP3: Create a HTTPServletRequest wrapper class so that you can wrap HttpServletRequest objects (HttpServletRequest object can be read only once and so you wrap it using a wrapper class which can be read any number of times)

STEP4: Create a Servlet filter to filter all httpservlet requests and wrap them with the wrapper class you just created.

First create a sample project through https://start.spring.io/ and add Spring Web as dependency.

STEP 1: Create a Handler Interceptor

You create a handler interceptor by implementing HandlerInterceptor interface. It has three methods preHandle(), postHandle() and afterCompletion() methods. You can choose preHandle() method which as the name suggests intercepts incoming requests before they are processed.

Here is a sample LogInterceptor class:

package com.example.demo;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import com.fasterxml.jackson.databind.ObjectMapper;

@Component
public class LogInterceptor implements HandlerInterceptor {

	Logger logger = LoggerFactory.getLogger(LogInterceptor.class);

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		Map<String, Object> inputMap = new ObjectMapper().readValue(request.getInputStream(), Map.class);

		logger.info("Incoming request is " + inputMap);
		logger.info("Incoming url is: "+ request.getRequestURL());

		return true;
	}



	
}

Notice that I am using Jackson API (which comes pre built with Spring Starter Web dependency) to convert input stream to a map object which represents input JSON. I am logging both the input and the request URL.

STEP 2: Register the Interceptor:

Register the interceptor by creating a configuration class .

You can create a configuration class by implementing WebMvcConfigurer class.

Once created add the interceptor you created by overriding addInterceptors() method:

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Component
public class Configuration implements WebMvcConfigurer {

	@Autowired
	private LogInterceptor logInterceptor;

	@Override
	public void addInterceptors(InterceptorRegistry registry) {

		registry.addInterceptor(logInterceptor);
	}

}

Notice that I have used only the annotation @Component for the configuration class instead of @Configuration annotation and it still works.

Now before moving on to the next step , lets test if the above two steps are enough to log the request.

Let’s create two APIs : numberOneAPI and numberTwoAPI . Both just return whatever input is passed to them.

We want to log those requests along with the URL.

Here is the class:

package com.example.demo;

import java.util.Map;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@org.springframework.web.bind.annotation.RestController
public class RestController {

	@PostMapping(value = "/numberOneAPI")
	public Map<String, Object> numberOneAPI(@RequestBody Map<String, Object> request) throws Exception {

		return request;
	}

	@PostMapping(value = "/numberTwoAPI")
	public Map<String, Object> numberTwoAPI(@RequestBody Map<String, Object> request) throws Exception {

		return request;
	}
}

Let me try to hit the first API through postman:

It throws me 400 bad request:

Here is the error trace:

2020-09-25 23:30:46.128 ERROR 16852 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exception
java.io.IOException: Stream closed
at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:359) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:132) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
at com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.ensureLoaded(ByteSourceJsonBootstrapper.java:524) ~[jackson-core-2.11.0.jar:2.11.0]
at com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.detectEncoding(ByteSourceJsonBootstrapper.java:129) ~[jackson-core-2.11.0.jar:2.11.0]

Stream is closed!

We tried to read the input stream from HttpServletRequest object but it is already closed. You can read the input stream only once and Spring is already reading it somewhere.

So how to resolve this issue?

By wrapping the request object.

Proceed to the next step.

STEP 3: Create a HttpServletRequestWrapper

You can create a HttpServletRequestWrapper object by extending HttpServletRequestWrapper class:

package com.example.demo;

import java.io.IOException;
import java.io.StringReader;
import java.util.Scanner;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class RequestWrapper extends HttpServletRequestWrapper {

	private String requestData = null;

	public RequestWrapper(HttpServletRequest request) {
		
		super(request);

		try (Scanner s = new Scanner(request.getInputStream()).useDelimiter("\\A")) {

			requestData = s.hasNext() ? s.next() : "";

		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	@Override
	public ServletInputStream getInputStream() throws IOException {

		StringReader reader = new StringReader(requestData);

		return new ServletInputStream() {

			private ReadListener readListener = null;

			@Override
			public int read() throws IOException {

				return reader.read();
			}

			@Override
			public void setReadListener(ReadListener listener) {
				this.readListener = listener;

				try {
					if (!isFinished()) {

						readListener.onDataAvailable();
					} else {

						readListener.onAllDataRead();
					}
				} catch (IOException io) {

					io.printStackTrace();
				}

			}

			@Override
			public boolean isReady() {

				return isFinished();
			}

			@Override
			public boolean isFinished() {

				try {
					return reader.read() < 0;
				} catch (IOException e) {
					e.printStackTrace();
				}

				return false;

			}
		};
	}

}

You have to do two things here.

Create a constructor which accepts the original HttpServletRequest object and extracts the data out of it as a string. I have used Scanner class to implement this . It can also be done using traditional BufferedReader or using Apache IOUtils.toString() method or in other ways.

The second thing to do inside this class is to override getInputStream() . Inside this method you need to create a new instance of ServletInputStream and return it. You need to override four methods of this class :

read()

isFinished()

isReady()

setReadListener()

You can use the same code used above. In short , you are overriding the default implementation.

Notice that this class is not annotated as a Spring Component using @Component annotation. If you do that it won’t work. It will throw the below exception:

Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.

STEP 4: Create a Servlet Filter

Now that you have created a wrapper object , filter all incoming requests and wrap them using this wrapper object.

package com.example.demo;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Component;

@Component
public class RequestFilter implements Filter {

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest wrappedRequest = new RequestWrapper((HttpServletRequest) request);

		chain.doFilter(wrappedRequest, response);

	}

}

Notice that the original request is wrapped using the wrapper object and this wrapper object is then passed on to the filter chain. So when you read HttpServletRequest object from here on you will be reading the wrapper object even though you don’t mention it specifically ( go back and have a look at the LogInterceptor class again)

That’s it!

Now you can test again if the requests are logged by hitting any of your APIs:

We get the response.

Now let’s check the console for the logs .

2020-09-25 23:44:02.191 INFO 22044 --- [nio-8080-exec-2] com.example.demo.LogInterceptor : Incoming request is {name=James Bond, age=34, address={street= Bond Street, city=The Bond City}}
2020-09-25 23:44:02.192 INFO 22044 --- [nio-8080-exec-2] com.example.demo.LogInterceptor : Incoming url is: http://localhost:8080/numberOneAPI

The request is logged.

Similarly for the second API:

2020-09-25 23:45:59.038 INFO 22044 --- [nio-8080-exec-5] com.example.demo.LogInterceptor : Incoming request is {name=Sachin Tendulkar, age=44, address={street= Celebrity Street, city=Mumbai}}
2020-09-25 23:45:59.038 INFO 22044 --- [nio-8080-exec-5] com.example.demo.LogInterceptor : Incoming url is: http://localhost:8080/numberTwoAPI

It works!

We could log requests in a centralized way by using Spring Interceptor , Spring WebMvcConfigurer , HttpServletRequestWrapper and Servlet Filter.

Here is the entire code:

https://github.com/vijaysrj/requestlogging

Comments

Leave a Reply

Discover more from The Full Stack Developer

Subscribe now to keep reading and get access to the full archive.

Continue reading