Logging Package
The org.apache.juneau.commons.logging package now centers on RichLogger, a JUL-compatible logger wrapper with configurable message rendering, hierarchical capture propagation, and deterministic test-capture ergonomics.
RichLogger
RichLogger
RichLogger wraps an underlying java.util.logging.Logger and keeps a stable canonical logger identity per name while allowing independently configured builder-produced views.
RichLogger logger = RichLogger.getLogger(MyClass.class);
logger.info("Started");
logger.warning("Low disk space");
You can customize message rendering per view using MessageGenerator while preserving canonical listener/capture identity.
RichLogger mfLogger = RichLogger
.getLogger(MyClass.class)
.builder()
.generator(MessageGenerator.MESSAGE_FORMAT)
.build();
mfLogger.info("User {0} logged in", userId);
The
RichLoggerjavadocs link above is expected to 404 until the next docs/javadocs publish picks up the renamed class page.
Request-scoped context (LogContext)
LogContext
LogContext is a thread-confined, MDC-style key/value context. While a scope is open, every
java.util.logging.LogRecord emitted by any RichLogger on the same thread automatically carries the context's
entries — attachment happens once, centrally, in RichLogger.log(LogRecord), not per logger instance. The API home is
RichLogger.context().
try (LogContext.Scope c = RichLogger.context().with("requestId", id)) {
// Every record logged on this thread in this scope automatically carries requestId.
logger.info("processing");
}
Semantics:
- Classic MDC save/restore nesting.
with(key, value)records the key's previous value (or "absent") before overwriting, andScope.close()restores exactly that prior state — never a blanket clear. Nested scopes on the same key are safe.with(Map)sets a batch atomically and restores all of it in oneclose(). - Null policy. A
nullkey is rejected; anullvalue means "remove this key for the scope's duration" (anullis never stored). snapshot()returns an immutable copy of the current thread's entries (the shared emptyMap.of()singleton when empty, so no allocation on the common empty path).- Scope discipline. A
Scopemust be closed on the same thread that opened it — use try-with-resources. A forgottenclose()leaves the entry live on that thread; the leak is the caller's, not the framework's.
LogContext is thread-confined only in v1. It is not inherited by child threads, not propagated across
executor or CompletableFuture boundaries, and it is not SLF4J MDC and is not restored by MdcAsyncListener.
Apps that rely on SLF4J MDC on an async-completion thread keep it; apps that rely on LogContext do not — with one
deliberate exception: the REST debug pipeline's completion record carries the request-thread snapshot across the async
hop explicitly (see REST Logging / Debugging). General async propagation
is intentionally out of scope for v1.
LogRecordContext
LogRecordContext is the identity-keyed side table that holds the LogContext snapshot attached to a given
LogRecord. It is how a captured record exposes its correlation fields after the fact:
Object requestId = LogRecordContext.of(record).get("requestId");
RichLogger.log(LogRecord) calls attachIfAbsent(record) as its first action, so a record that already carries a
pre-seeded context (for example, one carried across an async boundary) is never overwritten by the live thread context.
Message Rendering
MessageGenerator
MessageGenerator is the message-format strategy seam used by RichLogger and LogRecord.
MessageGenerator.PRINTFusesStringUtils.format(...)(printf-style placeholders).MessageGenerator.MESSAGE_FORMATusesStringUtils.mformat(...)(MessageFormat-style placeholders).- Custom generators can be supplied through
RichLogger.Builder.generator(...).
LogRecord
LogRecord remains a java.util.logging.LogRecord subtype and now carries generator-aware lazy formatting so rendered text is produced only when needed.
Test Support
LogRecordCapture
LogRecordCapture captures emitted JUL records in memory for assertions and includes helper accessors (messages(), last(), byLevel(...), matching(...), and assert helpers).
RichLogger logger = RichLogger.getLogger(MyClass.class);
try (LogRecordCapture capture = logger.captureEvents()) {
doSomethingThatLogs();
capture.assertMessage("Started");
}
Capture listeners are anchored to canonical logger identity and can observe descendant loggers through parent-chain propagation.
LogRecordListener
A listener interface receiving java.util.logging.LogRecord notifications, used by capture and custom test probes.