javaaopmdc

Java MDC Logger - methods with too many MDC.put()


Small question regarding MDC with Java please.

At first, I had very straightforward methods, some methods with many parameters (I shorten the list of parameters to keep things short just for this question, but please imagine a lot of parameters)

 public String invokeMethodForPerson(int age, String name, boolean isCool, long distanceRun, double weight) {
        LOGGER.info("begin to invoke method invokeMethodForPerson");
        return methodForPerson(age, name, isCool, distanceRun, weight);
    }

    public String invokeMethodForCar(String model, boolean isElectric, long price, int numberOfDoors) {
        LOGGER.info("begin to invoke method invokeMethodForCar");
        return methodForCar(model, isElectric, price, numberOfDoors);
    }

    public String invokeMethodForFlower(String name, String color) {
        LOGGER.info("begin to invoke method invokeMethodForFlower");
        return methodForFlower(name, color);
    }

(This question is not about how to refactor the long list of parameters)

Then, I wanted to leverage MDC. MDC is very helpful, it allow better search in log aggregator tools etc. Having them as first level, not inside the logger itself is very helpful.

Therefore, in order to leverage MDC, the code went from simple, to something now similar to this, here is what I tried.

public String invokeMethodForPerson(int age, String name, boolean isCool, long distanceRun, double weight) {
        MDC.put("age", age);
        MDC.put("name", name);
        MDC.put("isCool", isCool);
        MDC.put("distanceRun", distanceRun);
        MDC.put("weight", weight);
        LOGGER.info("begin to invoke method invokeMethodForPerson");
        return methodForPerson(age, name, isCool, distanceRun, weight);
    }

    public String invokeMethodForCar(String model, boolean isElectric, long price, int numberOfDoors) {
        MDC.put("model", model);
        MDC.put("isElectric", isElectric);
        MDC.put("price", price);
        MDC.put("numberOfDoors", numberOfDoors);
        LOGGER.info("begin to invoke method invokeMethodForCar");
        return methodForCar(model, isElectric, price, numberOfDoors);
    }

    public String invokeMethodForFlower(String name, String color) {
        MDC.put("name", name);
        MDC.put("color", color);
        LOGGER.info("begin to invoke method invokeMethodForFlower");
        return methodForFlower(name, color);
    }

Problem: now looking at the code, there is actually more lines of MDC.put() than actual business logic code

Question: Is there a cleaner way to leveraging MDC, for many parameters, other than just adding tons of lines of MDC.put(), if possible, using AOP/aspects/advice/pointcut please?

Thank you


Solution

  • You can create your own utils to work with MDC.

    public class MDCUtils {
        private MDCUtils() {
    
        }
    
        public static void putAll(Object... params) {
            if ((params.length & 1) != 0) {
                throw new IllegalArgumentException("Length is odd");
            }
    
            for (int i = 0; i < params.length; i += 2) {
                String key = Objects.requireNonNull((String) params[i]); //The key parameter cannot be null
                Object value = params[i + 1]; //The val parameter can be null only if the underlying implementation supports it.
                MDC.put(key, value);
            }
        }
    
        public static void putAll(Map<String, Object> map) {
            map.forEach(MDC::put);
        }
    
        public static <K extends String, V> void put(K k1, V v1, K k2, V v2) {
            putAll(k1, v1, k2, v2);
        }
    
        public static <K extends String, V> void put(K k1, V v1, K k2, V v2, K k3, V v3) {
            putAll(k1, v1, k2, v2, k3, v3);
        }
    }
    

    Example of usage:

        public String invokeMethodForPerson(int age, String name, boolean isCool, long distanceRun, double weight) {
            MDCUtils.putAll("age",age, "name", name,"isCool", isCool,"distanceRun", distanceRun,"weight", weight);
    
            LOGGER.info("begin to invoke method invokeMethodForPerson");
            return methodForPerson(age, name, isCool, distanceRun, weight);
        }
    

    More strict example:

        public String invokeMethodForFlower(String name, String color) {
            MDCUtils.put("name", name, "color", color);
            
            LOGGER.info("begin to invoke method invokeMethodForFlower");
            return methodForFlower(name, color);
        }
    

    Example using Map.of, but it does not support nullable values.

        public String invokeMethodForPerson(int age, String name, boolean isCool, long distanceRun, double weight) {
            MDCUtils.putAll(Map.of( "age",age,"name", name,"isCool", isCool,"distanceRun", distanceRun,"weight", weight));
    
            LOGGER.info("begin to invoke method invokeMethodForPerson");
            return methodForPerson(age, name, isCool, distanceRun, weight);
        }
    

    UPDATE:
    AOP solution via AspectJ
    Add annotation @LogMDC for method and parameter target. The idea is to add the ability to put all method arguments or specific ones to MDC

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target({ElementType.METHOD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface LogMDC {
    }
    

    Add aspect which will catch methods marked with @LogMDCannotation and perform storing arguments to the MDC.

    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.MethodSignature;
    
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Method;
    
    @Aspect
    public class MDCAspect {
        
        //match the method marked @LogMDC annotation
        @Before("@annotation(LogMDC) && execution(* *(..))")
        public void beforeMethodAnnotation(JoinPoint joinPoint) {
            String[] argNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
            Object[] values = joinPoint.getArgs();
            if (argNames.length != 0) {
                for (int i = 0; i < argNames.length; i++) {
                    MDC.put(argNames[i], values[i]);
                }
            }
        }
    
        //match the method which has any parameter with @LogMDC annotation
        @Before("execution(* *(.., @LogMDC (*), ..))")
        public void beforeParamsAnnotation(JoinPoint joinPoint) {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            String[] argNames = methodSignature.getParameterNames();
            Object[] values = joinPoint.getArgs();
            Method method = methodSignature.getMethod();
            Annotation[][] parameterAnnotations = method.getParameterAnnotations();
    
            for (int i = 0; i < parameterAnnotations.length; i++) {
                Annotation[] annotations = parameterAnnotations[i];
                for (Annotation annotation : annotations) {
                    if (annotation.annotationType() == LogMDC.class) {
                        MDC.put(argNames[i], values[i]);
                    }
                }
            }
        }
    }
    

    Example of using. Put all parameters of method to MDC

        @LogMDC
        public String invokeMethodForPerson(int age, String name, boolean isCool, long distanceRun, double weight) {
            LOGGER.info("begin to invoke method invokeMethodForPerson");
            return methodForPerson(age, name, isCool, distanceRun, weight);
        }
    

    Example of using. Put only specific parameters of the method to MDC, that marked with annotation.

        public String invokeMethodForPerson(@LogMDC int age, @LogMDC String name, boolean isCool, long distanceRun, @LogMDC double weight) {
            LOGGER.info("begin to invoke method invokeMethodForPerson");
            return methodForPerson(age, name, isCool, distanceRun, weight);
        }
    

    Please note, AspecJ have a little performace impact
    Performance penalty for using AspectJ
    Performance impact of using aop
    Performance: using AspectJ to log run time of all methods

    Depends on your use cases you should decide what to use simple util method call or aop solution.