1

It could be a simple solution but I am unable to get it done. I need to log the overall execution time for my request in SpringBoot Rest API. Request always enters to MainController always and can exit from two places-

  1. Main Restcontroller same method or
  2. ExceptionHandlerController handler method

I have created one custom annotation and injecting it to both main Controller and ExceptionController methods and getting elapsed time for individual methods. So here is the problem. I need to add these individual times to calculate the total time which I don't want.

Is there any other way to log this information easily.

Aspect class:

@Aspect
@Component
public class PointsAspect {

    private final static Logger logger = LoggerFactory.getLogger(PointsAspect.class);

    @Around("@annotation(annotation)")
    public Object logMethod(final ProceedingJoinPoint proceedingJoinPoint, final LogAround annotation)
            throws Throwable {
        final long start = System.currentTimeMillis();
        Object obj;
        try {
            logger.debug("Starting...! Method Name - " +proceedingJoinPoint.getSignature().getName());
            obj = proceedingJoinPoint.proceed();
        } finally {
            logger.debug("Exiting...! Method Name - " +proceedingJoinPoint.getSignature().getName() +"Execution Time in Milliseconds:> "+ String.valueOf(System.currentTimeMillis()-start));
        }
        return obj;
    }
}

Marker Interface:

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

}

And this is how I am injecting it:

**ExceptionHandlerController.java**

@LogAround
@ExceptionHandler(HttpMessageNotReadableException.class)
public GenericFailureResponse missingRequestBodyException(HttpServletResponse response,
            HttpServletRequest request, Exception ex) throws IOException {
        GenericFailureResponse failureResponse =  new GenericFailureResponse();
        //TODO: exception logic
        return failureResponse;
}



**MainController.java**

@LogAround
 public String getTotalPoints(@RequestParam(required=true) long memberNo,
            HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        //TODO : some logic
        return "something";
 }
Avhi
  • 806
  • 2
  • 15
  • 29

2 Answers2

4

You can use a simple filter.

@Component
public class LogTimeFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        long startTime = System.currentTimeMillis();
        chain.doFilter(request, response);
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("Request take " + duration + " ms");
    }
}
zlaval
  • 1,941
  • 1
  • 10
  • 11
  • After adding above code in new class, It started logging message infinitely. After application start it's logging the "Request take ** ms" message continously even without hitting any endpoint of API. 2019-07-01 09:32:29.664 INFO [Points,,,] 21896 --- [ restartedMain] c.h.g.a.p.MainApplication : Started MainApplication in 26.113 seconds (JVM running for 28.843) Running..!!!!!! o.s.web.servlet.DispatcherServlet : Completed initialization in 126 ms Request take 64 ms Request take 13 ms Request take 8 ms Request take 5 ms Request take 5 ms ---------- – Avhi Jul 01 '19 at 13:53
  • can you please give me some implementation detail. – zlaval Jul 01 '19 at 19:09
  • This is a simple SprongBoot rest API. nothing doing much as of now. doing a POC and got stuck with this use case. – Avhi Jul 01 '19 at 20:43
  • I asked because we use the example above in many places and it works fine. it couldnt run if nothing hit any endpoints. maybe try to debug and watch the call stack. – zlaval Jul 01 '19 at 20:55
  • I forgot to mention one thing that I have swagger dependencies as well in my project. few of request are for swager-configuration initially and then requests are keep coming for some random framework classes.. – Avhi Jul 01 '19 at 21:25
  • Sample attribute: {org.springframework.web.context.request.async.WebAsyncManager.WEB_ASYNC_MANAGER=org.springframework.web.context.request.async.WebAsyncManager@5188d799, brave.SpanCustomizer=INSTANCE, hiddenHttpMethodFilter.FILTERED=true, formContentFilter.FILTERED=true, requestContextFilter.FILTERED=true, characterEncodingFilter.FILTERED=true, webMvcMetricsFilter.FILTERED=true, org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter$TimingContext=org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter$TimingContext@212b99eb, brave.propagation. – Avhi Jul 01 '19 at 21:30
  • you can register the filter to not run on swagger endpoints, if you use the recommended resti endpoint convention: /api/v1/**, you can register the filter to run only endpoints start with /api. Remove the Component annotation and register a new Filter Bean: @Bean public FilterRegistrationBean logTimeFilter(){ FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new LogTimeFilter()); registrationBean.addUrlPatterns("/api/**"); return registrationBean; } – zlaval Jul 01 '19 at 21:33
  • This helped filtering the requests. Thanks a lot.. (y) – Avhi Jul 02 '19 at 13:23
  • Please be aware this solution has a flaw for asynch/reactive program. – Brendan Kim May 23 '23 at 16:14
  • @BrendanKim Yes, this is a thread-per-request solution. The filter class is different - WebFilter - for Webflux. Also adding this around the webfilterchain wont work. It can be solved in multiple ways, but i encourage everybody to use micrometer instead of custom solutions. – zlaval May 23 '23 at 17:43
  • @zlaval Not only Reactive, but if the process uses multi-threads in terms of async, such as (Completable)Future for example, the elapsed time will be printed first before all tasks are really completed. – Brendan Kim May 24 '23 at 18:18
  • yes, it just measure the time from the request start till it returns. it wont work with async processes, streaming responses and so on. That would be the case with AOP as well. – zlaval May 24 '23 at 20:47
0

I have solved it using annotation and advice.

First need to add these dependencies

org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop

then i create a annotation as follows

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Creator : Tanvir Chowdhury
 * Date    : 2023-02-23
 */
@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface TimeTraker {
}

then i write the time measuring logic in the advice class

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.util.StopWatch;

/**
 * Creator : Tanvir Chowdhury
 * Date    : 2023-02-23
 */


@Aspect
@Component
public class RequestResponseTimeTracker  {


    private static Logger logger = LoggerFactory.getLogger(RequestResponseTimeTracker.class);

    @Around("@annotation(com.reliantvalue.lake.security.TimeTraker)")
    public Object measureTime(ProceedingJoinPoint point) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object object = point.proceed();
        stopWatch.stop();
        logger.debug("Execution Time taken by : " + point.getSignature().getName() + "() method is - "
                + stopWatch.getTotalTimeMillis() + " ms");
        return object;
    }
}

thats all. Now use the annotation TimeTracker on rest controller like this

@SneakyThrows
    @PostMapping
    @JsonView(View.Full.class)
    @TimeTraker
    public Loan createLoan(@CurrentUser User user,
                           @RequestParam(value = "type", required = false) String type,
                           @RequestParam(value = "wf", required = false) String wfId,
                           @RequestBody LoanCreateRequestDTO loanCreateRequest) {
}

the output will be something like this

2023-02-23 17:32:36,958 INFO  [http-nio-5000-exec-6] c.r.l.s.RequestResponseTimeTracker: Execution Time taken by : deleteLoan() method is - 3776 ms
TanvirChowdhury
  • 2,498
  • 23
  • 28