27

I am writing a web Rest web service using Spring Boot.

I want to log time taken by my webservice to process request. Also I want to log headers,method and URI called.

I have done similar in my jersey web service few months back using ContainerRequestFilter and ContainerResponseFilter filter() method.

Also, AOP is Better or Filter?

VedantK
  • 9,728
  • 7
  • 66
  • 71
  • You should probably take a good look at this: https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-metrics.html – Software Engineer Mar 17 '17 at 13:12
  • 3
    Add `spring-boot-actuator` and it will already be logged for you. – M. Deinum Mar 17 '17 at 13:12
  • As Martin says, use the actuator support. You'll get all you want for free and can also expose the stats over REST,JMX etc, as well as having the ability to export this data elsewhere. See https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-metrics.html – PaulNUK Mar 17 '17 at 14:28

5 Answers5

32

Have you tried with a basic filter like this?

import java.io.IOException;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@WebFilter("/*")
public class StatsFilter implements Filter {

    private static final Logger LOGGER = LoggerFactory.getLogger(StatsFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // empty
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        long time = System.currentTimeMillis();
        try {
            chain.doFilter(req, resp);
        } finally {
            time = System.currentTimeMillis() - time;
            LOGGER.trace("{}: {} ms ", ((HttpServletRequest) req).getRequestURI(),  time);
        }
    }

    @Override
    public void destroy() {
        // empty
    }
}

EDITED: (Thanks @suren-aznauryan) Now, It uses Instant and Duration and avoids System.currentTimeMillis().

import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
@WebFilter("/*")
public class StatsFilter implements Filter {

    private static final Logger LOGGER = LoggerFactory.getLogger(StatsFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // empty
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        Instant start = Instant.now();
        try {
            chain.doFilter(req, resp);
        } finally {
            Instant finish = Instant.now();
            long time = Duration.between(start, finish).toMillis();
            LOGGER.trace("{}: {} ms ", ((HttpServletRequest) req).getRequestURI(),  time);
        }
    }

    @Override
    public void destroy() {
        // empty
    }
}
David Pérez Cabrera
  • 4,960
  • 2
  • 23
  • 37
  • have you logged start time here? – VedantK Mar 17 '17 at 12:49
  • @SeeKing It only log the total time in ms, but if you want to know the start time and end time is easy. You must to save the initial moment and trace it. – David Pérez Cabrera Mar 17 '17 at 12:51
  • Btw can you let me know where do I register this filter? We dont have web.xml in springboot – VedantK Mar 17 '17 at 12:56
  • 1
    @SeeKing It's a component, Spring allows to do this easily, but my preferred way is to scan its package. – David Pérez Cabrera Mar 17 '17 at 12:59
  • This might not be the correct way to do this. If there are other filters, there is no guarantee that the above filter is executed first. It might be the second, third, or the last one, which means a lot of elapsed time would not be accounted for. Isn't there a more bullet-proof solution? – Snackoverflow Feb 15 '19 at 13:04
  • 6
    You need to add this to the class: `@Order(Ordered.HIGHEST_PRECEDENCE)` https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/Ordered.html – David Pérez Cabrera Feb 15 '19 at 13:34
  • 1
    How about interservice calls? I have a filter which will be invoked in all microservices. will this approach work ? – Sumanth Varada Jul 31 '19 at 15:41
  • Which one is best way Interceptor OR Filter? Why? Reason for asking: I see that Interceptor is calculating the response time after sending the response. But, Filter is calculating while response is about to be sent. Though it is very minute milli seconds, I think the Interceptor way is correct. However, the filter way seems elegant code-wise. Am I right? – manikanta nvsr Jan 29 '21 at 14:34
  • 4
    Using `System.currentTimeMillis()` is not a safe way to measure the elapsed time(see [this post](https://www.baeldung.com/java-measure-elapsed-time) ) – Suren Aznauryan Apr 30 '21 at 10:24
  • HttpServletResponse res = (HttpServletResponse) resp; res.setHeader("Server-Timing", "service;dur=" + time); you can add these 2 line, to see the server time in response header directly instead of checking in logs. also you can see timing for load, wait all in timing section in API Network, there it will show as a section 'service' based on the name provided – Satish Patro Jan 28 '22 at 06:18
  • Instant.now() under the hood uses System.currentTimeMillis(), so execpt nicer API it doesnt give any benefits. And both ways are *wrong*. To measure elapsed time in java the only correct way is to use System.nano() time or any hight level API that uses it. – antony Sep 28 '22 at 08:52
  • Filter chain returns when we make internal api calls which gives wrong elapsed time – Avinash Jeeva Oct 14 '22 at 09:55
23

Spring boot :logging interceptor

public class ApiLogger extends HandlerInterceptorAdapter {
  private static final Logger logger = LoggerFactory
    .getLogger(ApiLogger.class);

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String requestId = UUID.randomUUID().toString();
    log(request,response, requestId);
    long startTime = System.currentTimeMillis();
    request.setAttribute("startTime", startTime);
    request.setAttribute("requestId", requestId);
      return true;
  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    super.afterCompletion(request, response, handler, ex);
    long startTime = (Long)request.getAttribute("startTime");    
    long endTime = System.currentTimeMillis();
    long executeTime = endTime - startTime;
    logger.info("requestId {}, Handle :{} , request take time: {}",request.getAttribute("requestId"), handler, executeTime);
  }

  private void log(HttpServletRequest request, HttpServletResponse response, String requestId) {
    logger.info("requestId {}, host {}  HttpMethod: {}, URI : {}",requestId, request.getHeader("host"),
      request.getMethod(), request.getRequestURI() );
  }
}

register interceptor :

@Configuration
@EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter {
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new ApiLogger()).addPathPatterns("/api/v1/*");
  }
}
Syakur Rahman
  • 2,056
  • 32
  • 40
Bhushan Uniyal
  • 5,575
  • 2
  • 22
  • 45
  • Should it be `"/api/v1/**"` – aadidasu Apr 29 '17 at 17:33
  • @aadidasu yes, actually it's depend on requirement, it's not restrict any pattern. – Bhushan Uniyal Apr 29 '17 at 17:36
  • Is this approach valid for Async calls too? The request thread will return immediately hence the actual time taken by the whole Async thread will not be recorded. – aadidasu Apr 29 '17 at 17:50
  • @aadidasu yes, this approach is not valid for async call, you are right in case of async call a new thead will create and actual thread will complete it's work, and not included async thread work in it time request. – Bhushan Uniyal Apr 29 '17 at 18:29
  • This works for us ! I still wonder if there are any performance considerations..... – MetalGeorge Sep 07 '18 at 21:31
  • hi @SAtahAhmedKhan please take a look https://stackoverflow.com/questions/45240005/how-to-log-request-and-response-bodies-in-spring-webflux – Bhushan Uniyal Sep 17 '19 at 04:29
  • How can I implement a reusable thing to apply statistics aggregation on an API and reuse it for database access statistics for example? – user666 Oct 28 '19 at 10:34
  • @user666 I don't think it's a good idea to persist this kind of statistics in DB as it will perform lots of writes (O(n) writes) but if you still need, then you can improve it by collecting data in thread-safe map/list and plush this batch of data in DB after a specific time (can use scheduler). – Bhushan Uniyal Oct 30 '19 at 05:04
  • @BhushanUniyal I did not mean to store in db, I meant to calculate statistics on time spent during a db call for example – user666 Oct 30 '19 at 07:03
  • @user666 then you need to play with hibernate also http://knes1.github.io/blog/2015/2015-07-08-counting-queries-per-request-with-hibernate-and-spring.html – Bhushan Uniyal Oct 30 '19 at 08:03
5

If your controller is Async use Aspect to get the correct and complete execution time.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.UUID;

@Aspect
@Component
public class LoggingAspect {

  static Logger log = LoggerFactory.getLogger(LoggingAspect.class);

  @Around("execution(* com.aakchoo.api.controller..*(..))")
  public Object profileExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {

    long start = System.currentTimeMillis();
    String className = joinPoint.getSignature().getDeclaringTypeName();
    String methodName = joinPoint.getSignature().getName();
    String apiName = className + "."+ methodName;
    HttpServletRequest request =
        ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
    String requestId = UUID.randomUUID().toString();
    log.info("----->>>>>\nREQUESTED_ID: {}\nHOST: {} HttpMethod: {}\nURI: {}\nAPI: {}\nArguments: {}\n",
        requestId,
        request.getHeader("host"),
        request.getMethod(),
        request.getRequestURI(),
        apiName,
        Arrays.toString(joinPoint.getArgs()));

    Object result = joinPoint.proceed();
    long elapsedTime = System.currentTimeMillis() - start;
    log.info("<<<<<-----\nExecution Time: {} ms [REQUESTED_ID: {}] [API: {}]", elapsedTime,requestId,apiName);

    return result;
  }
}

Add @EnableAspectJAutoProxy to your Applciation Class

@EnableAsync
@SpringBootApplication
@EnableAspectJAutoProxy
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

And your build.gradle will need the following

compile 'org.aspectj:aspectjweaver:1.8.10'
compile 'org.springframework.boot:spring-boot-starter-aop'
aadidasu
  • 1,037
  • 4
  • 15
  • 26
4

Answer by David is correct - filter is a good way to implement such functionality in Spring Boot.

Spring Boot has a built-in endpoint which returns the information about last 100 requests like shown below:

[{
    "timestamp": 1394343677415,
    "info": {
        "method": "GET",
        "path": "/trace",
        "headers": {
            "request": {
                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                "Connection": "keep-alive",
                "Accept-Encoding": "gzip, deflate",
                "User-Agent": "Mozilla/5.0 Gecko/Firefox",
                "Accept-Language": "en-US,en;q=0.5",
                "Cookie": "_ga=GA1.1.827067509.1390890128; ..."
                "Authorization": "Basic ...",
                "Host": "localhost:8080"
            },
            "response": {
                "Strict-Transport-Security": "max-age=31536000 ; includeSubDomains",
                "X-Application-Context": "application:8080",
                "Content-Type": "application/json;charset=UTF-8",
                "status": "200"
            }
        }
    }
},{
    "timestamp": 1394343684465,
    ...
}]

If your application is calling another services or querying the database consider using Sniffy for profiling - it will show you not only time spent on your server but also time spent in calling downstream systems. See a live demo here (Check the black widget in top bottom corner).

Disclaimer: I'm the author of Sniffy

Sniffy demo screenshot

bedrin
  • 4,458
  • 32
  • 53
  • 2
    This isn't a bad answer, but it doesn't answer the question asked and it is a little self gratifying since Sniffy is your product. Might at least consider mentioning that if you're going to plug your stuff. "I created Sniffy which helps....". – Gregg Mar 17 '17 at 13:26
  • 2
    @Gregg fair enough - I've added a disclaimer – bedrin Mar 17 '17 at 13:32
2

we can make use of Springboot InMemoryHttpTraceRepository by overriding add() method

public class MyCustomTraceRepository extends InMemoryHttpTraceRepository {


    @Override
    public void add(HttpTrace trace) {
        log(trace);
        super.add(trace);
    }

    //Log only you interested in
    public void log(HttpTrace trace) {
        Map<String, Object> traceMap = new LinkedHashMap<>();
        traceMap.put("status", trace.getResponse().getStatus());
        traceMap.put("method", trace.getRequest().getMethod());
        traceMap.put("uri", trace.getRequest().getUri().getPath());
        traceMap.put("timeTaken", trace.getTimeTaken());

        logger.info(traceMap)
    }
}    

//will Result into
{status=<>, method=<>, uri=/<>, timeTaken=<>}

@See more request and response trace options: https://docs.spring.io/spring-boot/docs/current/actuator-api/html/#http-trace

Suresh Naik
  • 275
  • 4
  • 11
  • May I know Why we are calling super.add(trace) after overridding the add method? – manikanta nvsr Jan 28 '21 at 14:59
  • @manikantanvsr That's because the default implementation of trace actuator endpoint which needs this info is InMemoryHttpTraceRepository with add(), now since we want to add more functionality we are writing our implementation, then calling on super will add the API & trace all in actuator endpoint. Now, if you do not write, trace actuator endpoint will not have new API information – Satish Patro Jan 25 '22 at 15:07