Class LoggedAspect
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 Summary
ConstructorsConstructorDescriptionLoggedAspect(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.LoggedAspect(InvocationEventEmitter eventEmitter, MetricsRecorder metricsRecorder, IClientInfoPort clientInfoPort, boolean mdcEnabled) Creates an aspect with explicit control over MDC propagation. -
Method Summary
Modifier and TypeMethodDescriptionlogInvocation(org.aspectj.lang.ProceedingJoinPoint pjp, Logged logged) Intercepts aLogged-annotated method call, measuring its duration and outcome, and delegating the emission decision and call-chain tracking to their respective collaborators.
-
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 invocationsmetricsRecorder- collaborator that records every invocation as a metricclientInfoPort- 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 invocationsmetricsRecorder- collaborator that records every invocation as a metricclientInfoPort- collaborator that resolves the caller's identitymdcEnabled- whetherLoggedMdcKeysshould be written to the current thread's MDC while a@Loggedmethod runs
-
-
Method Details
-
logInvocation
public Object logInvocation(org.aspectj.lang.ProceedingJoinPoint pjp, Logged logged) throws Throwable Intercepts aLogged-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 thanJoinPoint.getSignature(), since the latter reports the type on which the intercepted method is declared. When@Loggedis placed on a method declared by an interface (for exampleUserService) and implemented by a concrete class (for exampleUserServiceImpl), 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.
-