Spring WebFlux endpoint performance logger - spring

Do you know any good practices to log Spring WebFlux controller endpoint performance to keep reactive nature? Seems the following principle wouldn't work because it will block the IO as the ProceedingJoinPoint doesn't return Publisher<> , it returns just an object
#Aspect
#Component
#Slf4j
public class LoggingAspect
{
//AOP expression for which methods shall be intercepted
#Around("execution(* com.company.service..*(..)))")
public Object profileAllMethods(ProceedingJoinPoint proceedingJoinPoint) throws Throwable
{
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
//Get intercepted method details
String className = methodSignature.getDeclaringType().getSimpleName();
String methodName = methodSignature.getName();
final StopWatch stopWatch = new StopWatch();
//Measure method execution time
stopWatch.start();
Object result = proceedingJoinPoint.proceed();
stopWatch.stop();
//Log method execution time
log.info("Execution time of " + className + "." + methodName + " :: " + stopWatch.getTotalTimeMillis() + " ms");
return result;
}
}

Actually as mentioned #Toerktumlare in the comments proceedingJoinPoint.proceed() returns the type of object whatever is you controller endpoint return type, so it is possible to keep reactive nature. Note .subscribeOn(Schedulers.parallel()) is optional here, that is for my back code to support parallelism. Posting the solution for this:
#Aspect
#Component
#Slf4j
public class LoggingAspect
{
//AOP expression for which methods shall be intercepted
#Around("execution(* com.company.service..*(..)))")
public Object logEndpointPerformance(ProceedingJoinPoint proceedingJoinPoint) throws Throwable
{
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
//Get intercepted method details
String className = methodSignature.getDeclaringType().getSimpleName();
String methodName = methodSignature.getName();
final StopWatch stopWatch = new StopWatch();
//Measure method execution time
stopWatch.start();
Object result = proceedingJoinPoint.proceed();
if(result instanceof Mono){
return ((Mono)result).subscribeOn(Schedulers.parallel()).flatMap(r -> {
logExecutionTime(className, methodName, stopWatch);
return Mono.just(r);
});
}
else if(result instanceof Flux){
return ((Flux<Object>)result).subscribeOn(Schedulers.parallel()).collectList().flatMapMany(r -> {
logExecutionTime(className, methodName, stopWatch);
return Flux.fromIterable(r);
});
}
else{
logExecutionTime(className, methodName, stopWatch);
return result;
}
}
private void logExecutionTime(final String className, final String methodName, final StopWatch stopWatch){
stopWatch.stop();
//Log method execution time
log.debug("[ " + stopWatch.getTotalTimeMillis() + " mls ] lasted execution of" + className + "." + methodName );
}
}

Related

Spring Boot custom annotation design not working

I am following a tutorial by PacktPublishing where some annotations are used in an example,
but the code is from 2018 and there have probably been some changes.
Spring does not recognize the Annotation when creating a bean.
Specifically, here is an annotation design that just does not work for me locally:
link
Some important code snippets are:
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Component
public #interface ChannelHandler {
/**
* Channel patter, alias of value()
*/
String pattern() default "";
/**
* The channel pattern that the handler will be mapped to by {#link WebSocketRequestDispatcher}
* using Spring's {#link org.springframework.util.AntPathMatcher}
*/
String value() default "";
}
#ChannelHandler("/board/*")
public class BoardChannelHandler {
private static final Logger log = LoggerFactory.getLogger(BoardChannelHandler.class);
#Action("subscribe")
public void subscribe(RealTimeSession session, #ChannelValue String channel) {
log.debug("RealTimeSession[{}] Subscribe to channel `{}`", session.id(), channel);
SubscriptionHub.subscribe(session, channel);
}
#Action("unsubscribe")
public void unsubscribe(RealTimeSession session, #ChannelValue String channel) {
log.debug("RealTimeSession[{}] Unsubscribe from channel `{}`", session.id(), channel);
SubscriptionHub.unsubscribe(session, channel);
}
}
#Target({ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Action {
/**
* The action pattern. It needs to be an exact match.
* <p>For example, "subscribe"
*/
String value() default "";
}
Can you see what the issue is here? Is there some other annotation missing for newer versions
of Spring?
UPDATE - adding other necessary code.
public class ChannelHandlerInvoker {
private static final Logger log = LoggerFactory.getLogger(ChannelHandlerInvoker.class);
private static final AntPathMatcher antPathMatcher = new AntPathMatcher();
private String channelPattern;
private Object handler;
// Key is the action, value is the method to handle that action
private final Map<String, Method> actionMethods = new HashMap<>();
public ChannelHandlerInvoker(Object handler) {
Assert.notNull(handler, "Parameter `handler` must not be null");
Class<?> handlerClass = handler.getClass();
ChannelHandler handlerAnnotation = handlerClass.getAnnotation(ChannelHandler.class);
Assert.notNull(handlerAnnotation, "Parameter `handler` must have annotation #ChannelHandler");
Method[] methods = handlerClass.getMethods();
for (Method method : methods) {
Action actionAnnotation = method.getAnnotation(Action.class);
if (actionAnnotation == null) {
continue;
}
String action = actionAnnotation.value();
actionMethods.put(action, method);
log.debug("Mapped action `{}` in channel handler `{}#{}`", action, handlerClass.getName(), method);
}
this.channelPattern = ChannelHandlers.getPattern(handlerAnnotation);
this.handler = handler;
}
public boolean supports(String action) {
return actionMethods.containsKey(action);
}
public void handle(IncomingMessage incomingMessage, RealTimeSession session) {
Assert.isTrue(antPathMatcher.match(channelPattern, incomingMessage.getChannel()), "Channel of the handler must match");
Method actionMethod = actionMethods.get(incomingMessage.getAction());
Assert.notNull(actionMethod, "Action method for `" + incomingMessage.getAction() + "` must exist");
// Find all required parameters
Class<?>[] parameterTypes = actionMethod.getParameterTypes();
// All the annotations for each parameter
Annotation[][] allParameterAnnotations = actionMethod.getParameterAnnotations();
// The arguments that will be passed to the action method
Object[] args = new Object[parameterTypes.length];
try {
// Populate arguments
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
Annotation[] parameterAnnotations = allParameterAnnotations[i];
// No annotation applied on this parameter
if (parameterAnnotations.length == 0) {
if (parameterType.isInstance(session)) {
args[i] = session;
} else {
args[i] = null;
}
continue;
}
// Only use the first annotation applied on the parameter
Annotation parameterAnnotation = parameterAnnotations[0];
if (parameterAnnotation instanceof Payload) {
Object arg = JsonUtils.toObject(incomingMessage.getPayload(), parameterType);
if (arg == null) {
throw new IllegalArgumentException("Unable to instantiate parameter of type `" +
parameterType.getName() + "`.");
}
args[i] = arg;
} else if (parameterAnnotation instanceof ChannelValue) {
args[i] = incomingMessage.getChannel();
}
}
actionMethod.invoke(handler, args);
} catch (Exception e) {
String error = "Failed to invoker action method `" + incomingMessage.getAction() +
"` at channel `" + incomingMessage.getChannel() + "` ";
log.error(error, e);
session.error(error);
}
}
}
#Component
public class ChannelHandlerResolver {
private static final Logger log = LoggerFactory.getLogger(ChannelHandlerResolver.class);
private static final AntPathMatcher antPathMatcher = new AntPathMatcher();
// The key is the channel ant-like path pattern, value is the corresponding invoker
private final Map<String, ChannelHandlerInvoker> invokers = new HashMap<>();
private ApplicationContext applicationContext;
public ChannelHandlerResolver(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
this.bootstrap();
}
public ChannelHandlerInvoker findInvoker(IncomingMessage incomingMessage) {
ChannelHandlerInvoker invoker = null;
Set<String> pathPatterns = invokers.keySet();
for (String pathPattern : pathPatterns) {
if (antPathMatcher.match(pathPattern, incomingMessage.getChannel())) {
invoker = invokers.get(pathPattern);
}
}
if (invoker == null) {
return null;
}
return invoker.supports(incomingMessage.getAction()) ? invoker : null;
}
private void bootstrap() {
log.info("Bootstrapping channel handler resolver");
Map<String, Object> handlers = applicationContext.getBeansWithAnnotation(ChannelHandler.class);
for (String handlerName : handlers.keySet()) {
Object handler = handlers.get(handlerName);
Class<?> handlerClass = handler.getClass();
ChannelHandler handlerAnnotation = handlerClass.getAnnotation(ChannelHandler.class);
String channelPattern = ChannelHandlers.getPattern(handlerAnnotation);
if (invokers.containsKey(channelPattern)) {
throw new IllegalStateException("Duplicated handlers found for chanel pattern `" + channelPattern + "`.");
}
invokers.put(channelPattern, new ChannelHandlerInvoker(handler));
log.debug("Mapped channel `{}` to channel handler `{}`", channelPattern, handlerClass.getName());
}
}
}
<!-- begin snippet: js hide: false console: true babel: false -->
UPDATE 2
I have managed to make ChannelHandler and Action annotations work by adding #Inherited annotation and using AnnotationUtils.findAnnotation() which traverses its super methods if the annotation is not directly present on the given method itself.
However, I haven't managed to access custom annotation value of type parameter (ChannelValue)
Here, Annotation[][] allParameterAnnotations = actionMethod.getParameterAnnotations();
returns null value.
UPDATE 3 -> SOLVED
Just add #Aspect annotation to your ChannelHandler implementation (e.g.
"BoardChannelHandler").
Looks like bootstrap() method, that goes through all the #ChannelHandler annotated beans is executed too early - try to debug it to check if it detects any beans at this stage.
If not try calling bootstrap() after Spring context is ready (for example listen for ContextRefreshedEvent.

JUnit4 with Mockito for unit testing

public class DgiQtyAction extends DispatchAction {
private final Logger mLog = Logger.getLogger(this.getClass());
public ActionForward fnDgiQty(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
mLog.debug(request.getParameter(EcoConstants.ecopidid));
ActionErrors errorMessage = new ActionErrors();
if(request.getSession().getAttribute(EcoConstants.userBean)==null)
{
request.setAttribute(EcoConstants.ERROR_MESSAGE,EcoConstants.SESSION_TIMEOUT);
errorMessage.add(Globals.MESSAGE_KEY, new ActionMessage(EcoConstants.error_message,EcoConstants.SESSION_TIMEOUT));
saveMessages(request, errorMessage);
request.setAttribute(EcoConstants.errorMessageType,EcoConstants.errorMessageType);
return mapping.findForward(EcoConstants.SESSION_FORWARD);
}
String ecoPidID = (String) request.getParameter(EcoConstants.ecopidid);
String pidId = ESAPI.encoder().encodeForHTML((String) request.getParameter(EcoConstants.pidid));
mLog.debug("pidid --------" + pidId);
mLog.debug("ecopidpid --------" + ecoPidID);
ArrayList dgiQty = new ArrayList();
NeedDgiForm niForm = new NeedDgiForm();
try {
NeedDgiBD niBD = new NeedDgiBD();
if (ecoPidID != null) {
dgiQty = niBD.getDgiQty(ecoPidID);
if (dgiQty.size() != 0) {
mLog.debug(dgiQty.get(0).toString());
if (dgiQty.get(0).toString().equals(EcoConstants.hundred)) {
niForm.setGlqtyList(new ArrayList());
request.setAttribute(EcoConstants.pidId, pidId);
return mapping.findForward(EcoConstants.SuccessInfo);
} else {
mLog.debug("fnBug 1----------------> " + dgiQty.get(0));
mLog.info("dgiQty sizeeeee: :" + dgiQty.size());
niForm.setGlqtyList(dgiQty);
}
}
}
} catch (Exception e) {
//log.error("General Exception in DgiQtyAction.fnDgiQty: "
// + e.getMessage(), e);
request.setAttribute(EcoConstants.ERROR_MESSAGE, e.getMessage());
return mapping.findForward(EcoConstants.ERROR_PAGE);
}
mLog.debug("pidid after wards--------" + pidId);
request.setAttribute(EcoConstants.pidId, pidId);
request.setAttribute(mapping.getAttribute(), niForm);
return mapping.findForward(EcoConstants.SuccessInfo);
}
}
public class DgiQtyActionTest {
ActionMapping am;
ActionForm af;
DgiQtyAction dat;
private MockHttpSession mocksession;
private MockHttpServletRequest mockrequest;
private MockHttpServletResponse mockresponse;
#Test
public void fnDgiQty() throws Exception
{
mocksession = new MockHttpSession();
mockrequest = new MockHttpServletRequest();
mockresponse = new MockHttpServletResponse();
((MockHttpServletRequest) mockrequest).setSession(mocksession);
mocksession.setAttribute("userBean","userBean");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(mockrequest));
mockrequest.addParameter("ecopid","something");
mockrequest.addParameter("pid","<script>");
Encoder instance = ESAPI.encoder();
assertEquals("something",mockrequest.getParameter("ecopid"));
assertEquals("<script>",instance.encodeForHTML(mockrequest.getParameter("pid")));
dat=mock(DgiQtyAction.class);
am=mock(ActionMapping.class);
af=mock(ActionForm.class);
dat.fnDgiQty(am,af,mockrequest, mockresponse);
}
}
I wrote the unit test case for above class. i ran this code through jenkins and used sonarqube as code coverage.I need to cover the ESAPi encoder for the parameter, it got build success but the coverage percentage doesn't increase. i couldn't found the mistake in it. pls help me guys. Thanks in Advance

Not getting actual parameter names in Spring Boot Aspect

I am trying to add log statements before executing every method dynamically using Aspectj.
Code:
#Component
#Aspect
public class MethodLogger {
DiagnosticLogger logger = DiagnosticLogger.getLogger(getClass());
#Before("execution(* com.xyz..*.*(..))")
public void beforeMethod(JoinPoint joinPoint) throws Throwable {
System.out.println("Class******" + joinPoint.getTarget().getClass().getName());
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
System.out.println("Method******" + signature.getName());
// append args
Object[] args = joinPoint.getArgs();
String[] parameterNames = signature.getParameterNames();
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
System.out.println("parameterNames******" + parameterNames[i] + ":" + args[i]);
}
}
}
}
Output:
Class******com.xyz.security.web.UserController
Method******forgotPassword
parameterNames******userEmail:naresh#xyz.com
Class******com.xyz.security.service.impl.UserServiceImpl
Method******forgotPassword
parameterNames******userEmail:naresh#xyz.com
Class******com.sun.proxy.$Proxy436
Method******findByUserEmail
I am able to get at controller and service level. But when comes to Spring Data JPA Repository method its not able to print.
How to get the parameter names at Repository level ?
Here an example of what I did.
By adding + sign also the classes that implement my Repository or any other of my interfaces in com.example.** are intercepted.
#Slf4j
#Component
#Aspect
public class MethodLogger {
#Before("execution(* com.example.*..*+.*(..))")
public void beforeMethod(JoinPoint joinPoint) throws Throwable {
log.info("Class******" + joinPoint.getTarget().getClass().getName());
for (Class<?> theinterface: joinPoint.getTarget().getClass().getInterfaces()) {
log.info("Interfaces******" + theinterface.getName());
}
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
log.info("Method******" + signature.getName());
Object[] args = joinPoint.getArgs();
String[] parameterNames = signature.getParameterNames();
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
log.info("parameterNames******" + parameterNames[i] + ":" + args[i]);
}
}
}
}
Parameter names are also logged:
Class******com.sun.proxy.$Proxy87
Interfaces******com.example.demoaspectmethodlogging.control.EmployeeRepository
Interfaces******org.springframework.data.repository.Repository
Interfaces******org.springframework.transaction.interceptor.TransactionalProxy
Interfaces******org.springframework.aop.framework.Advised
Interfaces******org.springframework.core.DecoratingProxy
Method******findByName
parameterNames******name:Simon

#Around advice returning correct response but at client side response is null or undefined

I am trying to apply Around advice to my "Login.jsp" with angular js. And the problem is my controller method is check and I am applying around advice to check method but when I run my application I will get undefined as response at Login.jsp. And but the result which I had printed in my advice contains expected result.But I am not getting it on client side.
AroundAdvice.java
#Aspect #Component
public class AroundAdvice {
static Logger log = Logger.getLogger(AfterLoginAspect.class.getName());
#Around("execution(* com.admin.controller.LoginController.check(..))")
public void logWrittter(ProceedingJoinPoint jp) throws Throwable {
SimpleDateFormat date=new SimpleDateFormat();
log.info("Date Time :: " + date.format(new Date().getTime()));
Object result = jp.proceed();
System.out.println("result around");
log.info("result :: " + result);
// returns {"get Status":"home"}
}
}
LoginController.jsp
// authentication check
#RequestMapping(value = "/PostFormData", method = RequestMethod.POST)
public #ResponseBody JSONObject check(#RequestBody LoginBo login) {
System.out.println("checkCredentials::" + login.getUserName());
String username = login.getUserName();
// log.info("uswername ::"+username);
JSONObject result = new JSONObject();
String encrptedpassword = encryptdPwd.encrypt(login.getPassWord());
boolean login_status = loginService.checkCredentials(username, encrptedpassword);
// log.info("login_status ::"+login_status);
// System.out.println("staus ::"+login_status);
if (login_status == true && login.isIs_system_generated_pwd() == true) {
System.out.println("sys gen chnge pwd:: " + login.isIs_system_generated_pwd());
result.put("getStatus", "change");
// System.out.println(resultPage);
// login.setIs_system_generated_pwd(false);
} else if (login_status == true && login.isIs_system_generated_pwd() == false) {
result.put("getStatus", "home");
// System.out.println("Home paege ");
} else {
result.put("getStatus", "error");
}
System.out.println("result ::" + result);
// log.info("result ::"+resultPage);
return result;
}
Your pointcut does not match because the advice has a void return type, but your method returns a JSONObject. So maybe you want to change your advice declaration to:
#Aspect #Component
public class AroundAdvice {
static Logger log = Logger.getLogger(AfterLoginAspect.class.getName());
#Around("execution(* com.admin.controller.LoginController.check(..))")
public JSONObject logWriter(ProceedingJoinPoint jp) throws Throwable {
SimpleDateFormat date=new SimpleDateFormat();
log.info("Date Time :: " + date.format(new Date().getTime()));
JSONObject result = (JSONObject) jp.proceed();
System.out.println("result around");
log.info("result :: " + result);
return result;
}
}
Please note
public JSONObject logWriter instead of public void logWrittter,
JSONObject result = (JSONObject) jp.proceed(); instead of Object result = jp.proceed(); and
return result; instead of no return value.

Logging request and response for Spring mvc Service

hi All , i Have to log all the request , response , Exception ,
Errors for my Spring services. i had searched about the
interceptors , filters , logging filters , Spring interceptors :
HandlerInterceptorAdapter logger filters :
AbstractRequestLoggingFilter.java and its sub classes
(http://www.javawebdevelop.com/1704067/),
CommonsRequestLoggingFilter.java filters : LoggerFilter.
can any body its difference, and the best approach to do so i am
confused or i need to find out the third party library to do this
?..
I used AOP for something similar in one project. I had to write a class like this:
#Component
#Aspect
public class RequestMonitor {
private static final Logger logger = LoggerFactory.getLogger(RequestMonitor.class);
#Around("#annotation(org.example.ToBeLogged)")
public Object wrap(ProceedingJoinPoint pjp) throws Throwable {
logger.info("Before controller method " + pjp.getSignature().getName() + ". Thread " + Thread.currentThread().getName());
Object retVal = pjp.proceed();
logger.info("Controller method " + pjp.getSignature().getName() + " execution successful");
return retVal;
}
}
Log4j is a java based logging utility with easy integration with Spring MVC. Log4j comes with different logging levels to allow for appropriate logging in correspondence to the development environment.
Log4j 2 is the successor to log4j and has better performance as compared to its predecessor.
Kindly refer the following link for an integration of spring MVC + Log4j
http://www.codejava.net/frameworks/spring/how-to-use-log4j-in-spring-mvc
Edit: As mentioned in comment , PFB the code for logging both request and response.
#Aspect
#Component
public class ResponseLoggerAspect {
private static final Logger logger = Logger.getLogger("requestResponseLogger");
ExclusionStrategy excludeJsonAnnotation = new JsonIgnoreAnnotationExclusionStrategy();
Gson gson = new GsonBuilder().setExclusionStrategies(excludeJsonAnnotation).create();
#Pointcut("within(#org.springframework.stereotype.Controller *)")
public void controller() {}
#Pointcut("execution(* *(..))")
public void method() {}
#Pointcut("execution(#com.company.annotation.AddLog * *(..))")
public void Loggable() {}
//This will be caught for only those controller method where #AddLog annotation is written
#Before("Loggable()")
public void printRequestLog(JoinPoint joinPoint) {
try {
Object[] argsList = joinPoint.getArgs();
String str = "[";
for(Object arg : argsList) {
if(arg instanceof Object[]) {
str += Arrays.toString((Object[])arg) + ", ";
} else {
str += String.valueOf(arg) + ", ";
}
}
str += "]";
logger.info("Request args for " + joinPoint.getSignature().getName() + " are : " + str);
} catch(Exception ex) {
logger.info("Unable to log request args", ex);
}
}
//This will be called for all controller methods after returning
#AfterReturning(pointcut = "controller() && method()", returning="result")
public void afterReturning(JoinPoint joinPoint , Object result) {
long start = System.nanoTime();
try {
logger.info("Response sent by " + joinPoint.getSignature().getName() + " are : " + gson.toJson(result));
} catch(Exception ex) {
logger.error("Returned result cant be converted in JSON " , ex);
}
long end = System.nanoTime();
logger.info("elapsed time : " + (end - start));
}
}

Resources