Skip to main content

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 RichLogger javadocs 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, and Scope.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 one close().
  • Null policy. A null key is rejected; a null value means "remove this key for the scope's duration" (a null is never stored).
  • snapshot() returns an immutable copy of the current thread's entries (the shared empty Map.of() singleton when empty, so no allocation on the common empty path).
  • Scope discipline. A Scope must be closed on the same thread that opened it — use try-with-resources. A forgotten close() leaves the entry live on that thread; the leak is the caller's, not the framework's.
Not SLF4J MDC

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.PRINTF uses StringUtils.format(...) (printf-style placeholders).
  • MessageGenerator.MESSAGE_FORMAT uses StringUtils.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.