A Daily - Yash-777/MyWorld GitHub Wiki

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

/**
 * AOP aspect that intercepts all public methods on Spring MVC controller classes
 * (annotated with {@code @RestController} or {@code @Controller}) and logs:
 * <ul>
 *   <li>Fully-qualified or simple class name of the controller</li>
 *   <li>Fully-qualified or simple method signature, including each parameter's
 *       type (with or without package prefix) and parameter name</li>
 *   <li>Actual argument values passed at runtime</li>
 *   <li>Execution time in milliseconds</li>
 * </ul>
 *
 * <p><strong>Configuration properties</strong> (in {@code application.properties} / profile files):</p>
 * <pre>
 * # Master switch โ€“ set to false to disable the aspect entirely (default: true)
 * myapp.aop.controller.logging.enabled=true
 *
 * # Include the full package name in the class identifier logged (default: true)
 * # true  โ†’ com.net.bosch.crmmaster.controller.AppointmentController
 * # false โ†’ AppointmentController
 * myapp.aop.controller.logging.include-class-package=true
 *
 * # Include the full package name for each parameter type logged (default: true)
 * # true  โ†’ (com.net.bosch.crmmaster.dto.AppointmentRequest req, java.lang.String location)
 * # false โ†’ (AppointmentRequest req, String location)
 * myapp.aop.controller.logging.include-param-package=true
 *
 * # Include the full package name in the method's declaring class (default: true)
 * myapp.aop.controller.logging.include-method-package=true
 * </pre>
 *
 * <p>The aspect is registered as a Spring {@link Component} and will be picked up
 * by component scanning automatically. It sits in the {@code com.net.bosch.aspect}
 * package alongside {@link CommonPointcuts}, {@link HmacValidationAspect}, and
 * {@link LocationValidationAspect}.</p>
 *
 * @author RE Prime Platform โ€“ Bosch
 * @since 2.3.3
 * @see CommonPointcuts
 */
@Aspect
@Component
public class ControllerEndpointLoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(ControllerEndpointLoggingAspect.class);

    // -------------------------------------------------------------------------
    // Configuration properties injected from application-*.properties
    // -------------------------------------------------------------------------

    /**
     * Master switch. When {@code false} the aspect is a no-op and simply
     * delegates to the join point without any logging overhead.
     *
     * <p>Property: {@code myapp.aop.controller.logging.enabled} (default: {@code true})</p>
     */
    @Value("${myapp.aop.controller.logging.enabled:true}")
    private boolean enabled;

    /**
     * When {@code true}, the fully-qualified class name (with package) is
     * included in the log output; when {@code false}, only the simple class
     * name is used.
     *
     * <p>Property: {@code myapp.aop.controller.logging.include-class-package} (default: {@code true})</p>
     */
    @Value("${myapp.aop.controller.logging.include-class-package:true}")
    private boolean includeClassPackage;

    /**
     * When {@code true}, each parameter's type is logged with its fully-qualified
     * package name; when {@code false}, only the simple type name is used.
     *
     * <p>Property: {@code myapp.aop.controller.logging.include-param-package} (default: {@code true})</p>
     */
    @Value("${myapp.aop.controller.logging.include-param-package:true}")
    private boolean includeParamPackage;

    /**
     * When {@code true}, the method's declaring class is logged with its
     * fully-qualified package name; when {@code false}, only the simple name
     * is used.
     *
     * <p>Property: {@code myapp.aop.controller.logging.include-method-package} (default: {@code true})</p>
     */
    @Value("${myapp.aop.controller.logging.include-method-package:true}")
    private boolean includeMethodPackage;

    // -------------------------------------------------------------------------
    // Pointcut definitions
    // -------------------------------------------------------------------------

    /**
     * Matches any type (and therefore any method execution within that type)
     * that is annotated with Spring's {@code @RestController}.
     */
    @Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
    public void restControllerClass() { }

    /**
     * Matches any type annotated with Spring's {@code @Controller}.
     */
    @Pointcut("@within(org.springframework.stereotype.Controller)")
    public void controllerClass() { }

    /**
     * Composite pointcut: any public method execution inside a class that is
     * annotated with either {@code @RestController} or {@code @Controller}.
     */
    @Pointcut("(restControllerClass() || controllerClass()) && execution(public * *(..))")
    public void allControllerEndpoints() { }

    // -------------------------------------------------------------------------
    // Advice
    // -------------------------------------------------------------------------

    /**
     * Around advice applied to {@link #allControllerEndpoints()}.
     *
     * <p>Behaviour:</p>
     * <ol>
     *   <li>Short-circuits immediately when {@link #enabled} is {@code false}.</li>
     *   <li>Resolves the controller class identifier (simple or fully-qualified)
     *       driven by {@link #includeClassPackage}.</li>
     *   <li>Builds a human-readable method signature that lists each parameter
     *       with its type (optionally package-qualified) and parameter name,
     *       driven by {@link #includeParamPackage} and {@link #includeMethodPackage}.</li>
     *   <li>Logs the argument values actually supplied at runtime.</li>
     *   <li>Measures end-to-end execution time with {@link org.springframework.util.StopWatch}.</li>
     *   <li>Re-throws any exception unchanged so that Spring MVC's
     *       {@code @ExceptionHandler} / global error handling is never bypassed.</li>
     * </ol>
     *
     * @param pjp the proceeding join point supplied by the AspectJ runtime
     * @return the value returned by the controller method, forwarded unchanged
     * @throws Throwable any exception thrown by the controller method, re-thrown unchanged
     */
    @Around("allControllerEndpoints()")
    public Object logControllerEndpoint(ProceedingJoinPoint pjp) throws Throwable {

        // Master switch โ€“ zero overhead fast path
        if (!enabled) {
            return pjp.proceed();
        }

        org.springframework.util.StopWatch stopWatch = new org.springframework.util.StopWatch();
        stopWatch.start();

        try {
            Object result = pjp.proceed();
            stopWatch.stop();

            logEntry(pjp, stopWatch.getTotalTimeMillis(), null);

            return result;

        } catch (Throwable ex) {
            stopWatch.stop();
            logEntry(pjp, stopWatch.getTotalTimeMillis(), ex);
            throw ex; // always re-throw so Spring MVC error handling is unaffected
        }
    }

    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------

    /**
     * Builds and emits the structured log entry.
     *
     * @param pjp          the proceeding join point
     * @param elapsedMs    elapsed execution time in milliseconds
     * @param thrownEx     the exception thrown by the controller method, or
     *                     {@code null} if the method returned normally
     */
    private void logEntry(ProceedingJoinPoint pjp, long elapsedMs, Throwable thrownEx) {

        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method          method          = methodSignatumyapp.getMethod();

        // --- Controller class identifier -----------------------------------
        Class<?> targetClass   = pjp.getTarget().getClass();
        String   classIdentifier = includeClassPackage
                ? targetClass.getName()          // com.net.bosch.crmmaster.controller.AppointmentController
                : targetClass.getSimpleName();   // AppointmentController

        // --- Method declaring class identifier -----------------------------
        String methodClassIdentifier = includeMethodPackage
                ? method.getDeclaringClass().getName()
                : method.getDeclaringClass().getSimpleName();

        // --- Parameter signature -------------------------------------------
        Parameter[] parameters  = method.getParameters();
        Object[]    args        = pjp.getArgs();
        String      paramSignature = buildParameterSignature(parameters, args);

        // --- Method name (with or without declaring-class package) ---------
        String methodIdentifier = methodClassIdentifier + "#" + method.getName();

        // --- Return type ---------------------------------------------------
        String returnType = resolveTypeName(method.getReturnType());

        // --- Log ----------------------------------------------------------
        if (thrownEx == null) {
            log.info(
                "[CONTROLLER-AOP] class=[{}] method=[{}] params=[{}] returnType=[{}] timeTaken=[{} ms]",
                classIdentifier,
                methodIdentifier,
                paramSignature,
                returnType,
                elapsedMs
            );
        } else {
            log.error(
                "[CONTROLLER-AOP] class=[{}] method=[{}] params=[{}] returnType=[{}] timeTaken=[{} ms] exception=[{}]",
                classIdentifier,
                methodIdentifier,
                paramSignature,
                returnType,
                elapsedMs,
                thrownEx.getClass().getName() + ": " + thrownEx.getMessage()
            );
        }
    }

    /**
     * Constructs a readable parameter list, e.g.:
     * <pre>
     * (com.net.bosch.crmmaster.dto.AppointmentRequest appointmentRequest,
     *  java.lang.String location, int page)
     * </pre>
     * or, with package names disabled:
     * <pre>
     * (AppointmentRequest appointmentRequest, String location, int page)
     * </pre>
     * Actual runtime argument values are appended after a {@code =} sign so
     * that both type metadata and live data are visible in one log line:
     * <pre>
     * (AppointmentRequest appointmentRequest=AppointmentRequest{...}, String location=IN, int page=0)
     * </pre>
     *
     * @param parameters the formal parameters from {@link Method#getParameters()}
     * @param args       the actual argument values from {@link ProceedingJoinPoint#getArgs()}
     * @return a single formatted string representing the full parameter list
     */
    private String buildParameterSignature(Parameter[] parameters, Object[] args) {
        if (parameters == null || parameters.length == 0) {
            return "()";
        }

        StringBuilder sb = new StringBuilder("(");
        for (int i = 0; i < parameters.length; i++) {
            Parameter param     = parameters[i];
            String    typeName  = resolveTypeName(param.getType());
            String    paramName = param.isNamePresent() ? param.getName() : "arg" + i;
            Object    argValue  = (args != null && i < args.length) ? args[i] : "<unavailable>";

            sb.append(typeName)
              .append(" ")
              .append(paramName)
              .append("=")
              .append(safeToString(argValue));

            if (i < parameters.length - 1) {
                sb.append(", ");
            }
        }
        sb.append(")");
        return sb.toString();
    }

    /**
     * Resolves a {@link Class} to either its fully-qualified name or its
     * simple name depending on the {@link #includeParamPackage} flag.
     *
     * <p>Primitive types (e.g. {@code int}, {@code boolean}) are always
     * returned by their keyword name regardless of the flag, since they have
     * no package.</p>
     *
     * @param type the class whose name should be resolved
     * @return the resolved type name string
     */
    private String resolveTypeName(Class<?> type) {
        if (type.isPrimitive() || type.isArray()) {
            // Primitives and arrays: use the canonical name (e.g. "int", "int[]")
            return type.getCanonicalName() != null ? type.getCanonicalName() : type.getName();
        }
        return includeParamPackage ? type.getName() : type.getSimpleName();
    }

    /**
     * Safely converts an argument value to a {@link String} for logging.
     *
     * <p>Catches any exception thrown by the argument's {@code toString()} method
     * (e.g. lazy-loaded JPA proxies, streams) and returns a safe fallback so
     * that the logging aspect itself never causes a failumyapp.</p>
     *
     * @param value the runtime argument value, may be {@code null}
     * @return a safe string representation of {@code value}
     */
    private String safeToString(Object value) {
        if (value == null) {
            return "null";
        }
        try {
            return value.toString();
        } catch (Exception ex) {
            return "<toString-failed: " + value.getClass().getName() + ">";
        }
    }
}
โš ๏ธ **GitHub.com Fallback** โš ๏ธ