Class LoggedAspect

java.lang.Object
com.fayupable.logged.spring.aspect.LoggedAspect

public class LoggedAspect extends Object
Spring AOP interceptor that implements the behavior described by Logged.

This is a proxy-based Spring AOP aspect, not a full AspectJ weaving aspect. As a consequence, it only intercepts calls made from outside the proxied bean: a method calling another @Logged method on this bypasses the proxy and is not intercepted. This is a known limitation of Spring AOP and must be documented for library consumers.

For every intercepted call, this aspect always records an invocation metric through MetricsRecorder, regardless of sampling or threshold settings, so that dashboards and alerts remain accurate. Whether a MethodInvocationEvent is additionally emitted through InvocationEventEmitter is decided by EmissionPolicy: a call is emitted when it failed, exceeded its configured slow threshold, or was selected by random sampling. This ensures that failures and slow calls are never silently dropped by a low sample rate, while still reducing log volume for fast, high-traffic, successful calls.

This aspect maintains a call-chain position for nested @Logged calls on the same thread through FlowContextHolder, so that log output can be reconstructed as a call chain and a failure can be traced back to exactly where in that chain it occurred.

A failure inside MetricsRecorder.record(java.lang.String, java.lang.String, long, boolean, java.lang.String) or InvocationEventEmitter.emit(com.fayupable.logged.core.model.MethodInvocationEvent), whether from a bundled implementation or a custom one supplied by the consuming application, is caught and logged rather than allowed to propagate. Without this, a misbehaving observability collaborator could replace the intercepted method's own exception in a finally block, hiding the real failure behind an unrelated one from this library's own bookkeeping.

The class and method name reported for each invocation are resolved once per distinct (target class, method) pair and cached afterward in methodNameCache, instead of being recomputed on every single call. Class.getSimpleName() and Method.getName() are cheap individually, but on a method called thousands of times per second the repeated string production adds up; caching removes it entirely after the first call. This cache is bounded by the number of distinct @Logged methods actually invoked in the running application, a quantity fixed by the codebase itself, not by request volume or any user-supplied value, so it cannot grow without bound the way a cache keyed by request data could.

On failure, this aspect reports two exception types rather than one: MethodInvocationEvent.exceptionType(), the type actually thrown across the @Logged boundary, and MethodInvocationEvent.rootCauseType(), the type at the end of that exception's Throwable.getCause() chain. A generic wrapper exception at the boundary (for example a framework's checked-exception translation) would otherwise hide which lower-level failure actually occurred; walking to the root cause recovers that detail while still reporting only class names, never exception messages.

A @Logged method that returns a CompletableFuture is detected and handled differently from an ordinary synchronous method. ProceedingJoinPoint.proceed() returning is not the same as the method's work being done: it only means a CompletableFuture was constructed and handed back, typically before the work it represents has even started on another thread. Recording the invocation at that point would report a near-zero duration and an unconditional success, regardless of what the asynchronous work actually does afterward. Instead, this aspect defers recording until the returned future itself completes, via CompletableFuture.whenComplete(java.util.function.BiConsumer<? super T, ? super java.lang.Throwable>), so the reported duration and outcome reflect the real asynchronous work rather than just the time it took to submit it. The caller identity and call-chain position are still captured synchronously, on the original calling thread, since resolving them later on whatever thread completes the future could observe completely different, thread-bound state (for example an HTTP request that is only available on the original servlet thread).

While a @Logged method runs, this aspect also writes LoggedMdcKeys into the current thread's MDC, restoring whatever was there before once the call ends. This is separate from, and in addition to, the summary line eventEmitter produces: it lets a consuming application's own log statements made from inside a @Logged method automatically carry the same trace id, depth, class, and method name, without threading that information through by hand. This can be disabled entirely via mdcEnabled, for applications that manage MDC themselves or want to avoid the extra MDC.put/MDC.remove calls on a very hot path.

  • Constructor Details

    • LoggedAspect

      public LoggedAspect(InvocationEventEmitter eventEmitter, MetricsRecorder metricsRecorder, IClientInfoPort clientInfoPort)
      Creates an aspect with MDC propagation enabled, the default for any consumer that does not need to disable it explicitly.
      Parameters:
      eventEmitter - collaborator that emits sampled/failed/slow invocations
      metricsRecorder - collaborator that records every invocation as a metric
      clientInfoPort - collaborator that resolves the caller's identity
    • LoggedAspect

      public LoggedAspect(InvocationEventEmitter eventEmitter, MetricsRecorder metricsRecorder, IClientInfoPort clientInfoPort, boolean mdcEnabled)
      Creates an aspect with explicit control over MDC propagation.
      Parameters:
      eventEmitter - collaborator that emits sampled/failed/slow invocations
      metricsRecorder - collaborator that records every invocation as a metric
      clientInfoPort - collaborator that resolves the caller's identity
      mdcEnabled - whether LoggedMdcKeys should be written to the current thread's MDC while a @Logged method runs
  • Method Details

    • logInvocation

      public Object logInvocation(org.aspectj.lang.ProceedingJoinPoint pjp, Logged logged) throws Throwable
      Intercepts a Logged-annotated method call, measuring its duration and outcome, and delegating the emission decision and call-chain tracking to their respective collaborators.

      The captured class name is resolved from JoinPoint.getTarget() rather than JoinPoint.getSignature(), since the latter reports the type on which the intercepted method is declared. When @Logged is placed on a method declared by an interface (for example UserService) and implemented by a concrete class (for example UserServiceImpl), resolving from the signature would report the interface name instead of the class actually handling the call. getTarget() returns the real object being advised, so the reported class name always matches what is actually running, regardless of whether the annotated method is declared on an interface or directly on the class.

      Parameters:
      pjp - the join point representing the intercepted call
      logged - the Logged annotation present on the intercepted method
      Returns:
      the value returned by the intercepted method
      Throws:
      Throwable - whatever the intercepted method throws; this aspect never swallows an exception