This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/docs by this push:
new 559c517857 Document REST correlation IDs, RichLogger logging context,
and admin console chrome
559c517857 is described below
commit 559c517857ecd33bb0c116a5d3f1045b3cd640fb
Author: James Bognar <[email protected]>
AuthorDate: Mon Aug 17 07:15:43 2026 -0400
Document REST correlation IDs, RichLogger logging context, and admin
console chrome
- New topic: 10.64.AdminConsoleChrome documenting the themeable admin
console chrome (Tag/Theme model and Freemarker mixins).
- Update 02.15.JuneauCommonsLogging for the RichLogger MDC-lite logging
context (LogContext/LogRecordContext).
- Update 10.32.RestServerLoggingAndDebugging and
13.10.RestClientLoggingAndDebugging for always-on request correlation
IDs (X-Request-Id) and debug-pipeline hardening.
- Update sidebars.ts and release-notes/10.0.0 accordingly.
---
pages/release-notes/10.0.0.md | 92 ++++++++++
pages/topics/02.15.JuneauCommonsLogging.md | 47 +++++
.../topics/10.32.RestServerLoggingAndDebugging.md | 88 ++++++++++
pages/topics/10.64.AdminConsoleChrome.md | 191 +++++++++++++++++++++
.../topics/13.10.RestClientLoggingAndDebugging.md | 127 +++++++++++++-
sidebars.ts | 5 +
6 files changed, 548 insertions(+), 2 deletions(-)
diff --git a/pages/release-notes/10.0.0.md b/pages/release-notes/10.0.0.md
index 3e914e056d..cf4fbfe8aa 100644
--- a/pages/release-notes/10.0.0.md
+++ b/pages/release-notes/10.0.0.md
@@ -468,6 +468,17 @@ Juneau 10.0 decouples the query *wire protocol* (request
parameter names + respo
- **`DataTablesResults<T>`** — the
`{draw,recordsTotal,recordsFiltered,data}` (+ optional `error`) envelope bean,
rendered by the negotiated serializer (no hardcoded JSON).
- **Browser-side helpers** — `DataTablesColumns` (generate `columns:[…]`
from bean-property order + `@Schema` titles), `DataTablesTable` (build a
DataTables-ready HTML5 `Table` DOM bean, with `MarshallingContext` overloads),
and `DataTablesMixin` (serves a first-party `juneau-datatables.js` auto-init
glue script). DataTables' own JS/CSS and jQuery remain **caller-provided** (not
vendored — their license is not ASF category-A).
+### Themeable admin-console chrome + FreeMarker macro composition (new
modules: `juneau-rest-server-console-ui`, `-console-ui-freemarker`,
`-console-ui-freemarker-datatables`)
+
+Juneau 10.0 adds three opt-in modules that give an admin-console-style
resource a themeable CSS "chrome", an enum status/domain pill component, and
(for FreeMarker consumers) macro-based composition — without pulling a template
engine or the DataTables add-on into `juneau-rest-server` core. The three
modules form an acyclic dependency chain (`console-ui` →
`console-ui-freemarker` → `console-ui-freemarker-datatables`); a chrome-only
consumer never drags in FreeMarker or DataTables transitively.
+
+- **`juneau-rest-server-console-ui`** — `ConsoleChromeMixin` serves a
dynamically themed `chrome.css` (the module's shipped structural stylesheet
plus a `:root` block built from validated `--jc-*` theme tokens);
`Theme`/`ThemeSettings` validate every token value through a
**normalize-then-accept-known-safe** pipeline (reject C0/C1/DEL control chars →
strip CSS comments → case-insensitively reject any `url(` production → match an
allowlist grammar for colors/fonts/lengths/gradients) rathe [...]
+- **`juneau-rest-server-console-ui-freemarker`** — `ConsoleFreemarkerMixin
extends FreemarkerMixin` (register it typed as `FreemarkerMixin`, mirroring the
load-bearing-return-type rule `FreemarkerMixin` beans already follow) augments
the resolved `Configuration`'s `TemplateLoader` with a second,
classpath-root-rooted `ClassTemplateLoader`, without shadowing the consumer's
own `basePath`-relative templates. This resolves the reserved
`org/apache/juneau/console/base.ftlh` template (pulled [...]
+- **`juneau-rest-server-console-ui-freemarker-datatables`** —
`ConsoleDataTablesFreemarkerMixin extends ConsoleFreemarkerMixin` additionally
resolves the reserved `<@datatable id=... rows=... rowType=.../>` macro, which
renders a `DataTablesTable.of(...)`-built `<table class="jc-table"
data-juneau-datatable ...>` through the same trusted-HTML adapter.
+- **`DataTablesTable` additive `@Html(render=...)` honoring** (in the existing
`juneau-rest-server-datatables` module) —
`DataTablesTable.of(MarshallingContext, String, Collection<?>, Class<?>)` now
resolves and applies each row property's `@Html(render=...)` the same way the
ordinary `HtmlSerializer`/`HtmlDocSerializer` path already does, so a
`@TagDomain` enum column renders as a pill instead of a raw enum name. **This
is not a purely-additive change**: a bean already using `@Html(rend [...]
+
+See the new [Admin-Console Chrome](/docs/topics/AdminConsoleChrome) topic page.
+
### JUL-level-driven REST debug/logging (`RestDebugFormatter`)
Juneau 10.0 replaces the multi-signal REST debug/logging system
(`@Rest(debug)`/`@Debug`/`DebugEnablement`/
@@ -565,8 +576,89 @@ changes land in `juneau-rest-server`.
See the [Logging /
Debugging](/docs/topics/RestServerLoggingAndDebugging#secure-by-default) topic
page for the full
model and the operator opt-in danger callout.
+### Always-on request correlation (`X-Request-Id` / `requestId`)
+
+Juneau 10.0 adds always-on, end-to-end request correlation on the REST server.
A per-request `requestId` is resolved
+during session build — before any `@RestStartCall` hook — so it covers
**every** code path that produces a debug
+record, including 404s and early-error paths, and gives the debug formatter's
`statusLine` a synchronous
+`RestRequest.getRequestId()` to read.
+
+- **Mint-or-honor.** An inbound `X-Request-Id` request header is honored when
present; otherwise a fresh time-ordered
+ `Uuid7` is minted. The resolved id is echoed on the `X-Request-Id`
**response** header. Exactly one `requestId`
+ `LogContext` scope is opened per request and closed after the debug record
is emitted (emit-before-close), so every
+ record on the request thread — the cumulative debug record and any
handler-emitted records — carries the structured
+ `requestId` field, and the rendered message is prefixed `[requestId=<id>] `.
The id also rides the async-completion
+ snapshot, so true-async completion records correlate identically.
+- **`RequestIdSettings` bean** (public write-surface, resolved by
`RestContext` via a `getBean` → `@Bean`-factory →
+ default ladder): `validator(Predicate<String>)` defaults to
**sanitize-and-accept** (strip log-forging sequences,
+ accept the bounded remainder — not reject-and-remint), plus
`attributeKey(String)` and `idSupplier(Supplier<String>)`.
+- **`RequestIdFilter` reconciled to a thin façade.** Its `apply()` is now
idempotent with respect to the
+ already-resolved session id (honor, never double-mint); its per-instance
builder knobs are documented **no-ops** —
+ request-id behavior is configured on `RequestIdSettings`.
+- **Auth non-influence.** `X-Request-Id` has zero influence on
authentication/authorization outcomes; configuring an
+ auth guard to read it is an anti-pattern (regression-tested).
+
+See the [Logging /
Debugging](/docs/topics/RestServerLoggingAndDebugging#always-on-request-correlation-requestid)
+topic page.
+
### juneau-rest-client / juneau-rest-common
+### Secure-by-default NG REST *client* debug hardening
(`RestClientDebugFormatter`)
+
+The next-generation `RestClient`'s debug logging pipeline
(`RestClientDebugPipeline` /
+`BasicRestClientDebugFormatter`) now gets the same stable-emission and
secret-hardening treatment the server side's
+`RestDebugFormatter` got above — ported onto the client's parallel classes,
reusing the same
+`juneau-rest-common` primitives (`DebugTextSanitizer`,
`RestDebugBodyScrubber`). This is an unreleased-10.0 **hard
+break** in the client debug formatter's output — there is no compatibility
shim.
+
+- **Stable-`INFO` emission.** Every debug `LogRecord` — on both the normal
response-close path and the
+ transport-exception synthetic-response path — is now always stamped
`Level.INFO`; the resolved tier (`FINE`/
+ `FINEST`) continues to control message *detail* only, not the emitted record
level.
+- **No body dumps by default.** Request/response bodies are **never** dumped
unless an operator explicitly sets the
+ **same** `JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES` environment variable the
server uses (byte-identical name and
+ trim-then-parse truthy semantics — one env var now gates both sides). With
the gate unset, `FINEST` emits a
+ suppression placeholder + byte count instead of body content, replacing the
previous unconditional UTF-8 + hex
+ dump. Even opted in, bodies render only for renderable text content (via a
duplicated `isBodyRenderable(String)`
+ predicate on `RestClientDebugFormatter`, resolved independently per
request/response side) and only for an
+ identity `Content-Encoding`; non-renderable/binary content and non-identity
encodings yield a distinct "not
+ rendered" placeholder.
+- **`RestDebugBodyScrubber` opt-in**, registered via the new
`bodyScrubber(...)` builder method on
+ `BasicRestClientDebugFormatter` — gate-subordinate and fail-closed, same
contract as the server side: a throw or
+ `null` result falls closed to a placeholder, never the raw body.
+- **Widened, formatter-local header redaction**, matching the server's added
set (`X-Auth-Token`,
+ `X-Authorization`, `WWW-Authenticate`, `Referer`, `Location`, atop the
shared `RedactedHeaders.DEFAULT`), matched
+ with separator-normalized folding
(`X-Auth-Token`/`X_Auth_Token`/`XAuthToken` all match).
+- **Log-forging sanitization.** The request method/URI, header names/values,
and any dumped body text are escaped
+ (CR/LF and other control characters → `\r`/`\n`/`\uXXXX`), so a server
cannot inject a newline into a response
+ header/body to forge a second physical log line.
+
+Out of scope for this port (client-specific rationale): the server's
`maxHeaders`/`maxHeaderScan`/`maxFieldLength`/
+`maxUriLength` bounding knobs have no client-side analog, since the client's
header source is already a finite,
+bounded list rather than an adversarial unbounded servlet input.
+
+See the [Logging and
Debugging](/docs/topics/RestClientLoggingAndDebugging#secure-by-default) topic
page for the
+full model.
+
+### Client-side request correlation (`X-Request-Id`)
+
+`RestClient` now auto-participates in end-to-end request correlation, pairing
with the server's always-on
+`requestId` resolver above.
+
+- **Auto-send / mint.** Every outgoing request carries an `X-Request-Id`
header; if the caller hasn't set one, the
+ client mints a time-ordered `Uuid7`. A caller-supplied header
(builder-global or per-request) is honored, never
+ overwritten. `RestClient.Builder.sendRequestId(false)` opts out of
auto-sending.
+- **Pre-call setter + getters.** `RestRequest.requestId(String)` sets the id
for one call; `RestRequest.getRequestId()`
+ reads the sent id; `RestResponse.getRequestId()` exposes the id the server
echoed back.
+- **Effective-id logging.** `RestClientDebugPipeline` stamps each emitted
`LogRecord` with a structured `requestId`
+ field (echoed id when present, else the sent id) via
`LogRecordContext.attachIfAbsent(...)`, and
+ `BasicRestClientDebugFormatter` prepends a `[requestId=<id>] ` marker to the
rendered message — matching the
+ server's `statusLine` prefix so a request reads identically on both sides.
+- **Shared constants.** The `X-Request-Id` header name and max length live in
a shared `RequestIdConstants` in
+ `juneau-rest-common`, consumed by both client and server.
+
+See the [Logging and
Debugging](/docs/topics/RestClientLoggingAndDebugging#request-correlation-x-request-id)
topic
+page.
+
### Next-generation remote-proxy declarative features
Juneau 10.0 significantly expands what the [REST
proxy](/docs/topics/RestProxies) annotation family (`@Remote`, `@RemoteOp`, and
the verb annotations `@RemoteGet` / `@RemotePost` / `@RemotePut` /
`@RemoteDelete` / `@RemotePatch`) can express declaratively. Most additions
below are honored by the **next-generation** proxy engine only
(`RestClient.remote(...)` → `RemoteClient`); the classic `getRemote(...)`
engine ignores those members. The one exception is dynamic URL & base override
(`@U [...]
diff --git a/pages/topics/02.15.JuneauCommonsLogging.md
b/pages/topics/02.15.JuneauCommonsLogging.md
index 47fe979788..d4034625fd 100644
--- a/pages/topics/02.15.JuneauCommonsLogging.md
+++ b/pages/topics/02.15.JuneauCommonsLogging.md
@@ -30,6 +30,53 @@ 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`)
+
+### <java-class><a
href="/site/apidocs/org/apache/juneau/commons/logging/LogContext.html"
target="_blank">LogContext</a></java-class>
+`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()`.
+
+```java
+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.
+
+:::note 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](/docs/topics/RestServerLoggingAndDebugging)). General async
propagation
+is intentionally out of scope for v1.
+:::
+
+### <java-class><a
href="/site/apidocs/org/apache/juneau/commons/logging/LogRecordContext.html"
target="_blank">LogRecordContext</a></java-class>
+`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:
+
+```java
+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
### <java-interface><a
href="/site/apidocs/org/apache/juneau/commons/logging/MessageGenerator.html"
target="_blank">MessageGenerator</a></java-interface>
diff --git a/pages/topics/10.32.RestServerLoggingAndDebugging.md
b/pages/topics/10.32.RestServerLoggingAndDebugging.md
index 0219f0bf09..6cc1696921 100644
--- a/pages/topics/10.32.RestServerLoggingAndDebugging.md
+++ b/pages/topics/10.32.RestServerLoggingAndDebugging.md
@@ -313,6 +313,94 @@ See the
[migration guide's footgun
callout](/docs/topics/V10MigrationGuide#rest-debuglogging-redesign-jul-level-driven-restdebugformatter)
for operational guidance on raising logger levels in production.
+## Application logging from a handler
+
+Separately from the one cumulative debug record the pipeline emits per
request, handler code can emit its own ad hoc log
+statements during request processing via convenience methods on
+<a href="/site/apidocs/org/apache/juneau/rest/server/RestRequest.html"
target="_blank">RestRequest</a>. These delegate to
+the request's resolved per-operation `RichLogger` (`req.getLogger()`), so they
ride the same JUL level resolution and
+the same parent-chain capture propagation as everything else on that logger:
+
+```java
+@RestGet(path="/widgets/{id}")
+public Widget getWidget(RestRequest req, @Path("id") String id) {
+ req.fine("Loading widget %s", id);
+ try {
+ return widgetService.load(id);
+ } catch (NotFoundException e) {
+ req.warning(e, "Widget %s not found", id);
+ throw e;
+ }
+}
+```
+
+- A full JUL-level ladder is provided —
`severe`/`warning`/`info`/`config`/`fine`/`finer`/`finest` — each with a
+ `(String pattern, Object...args)` form and a `(Throwable thrown, String
pattern, Object...args)` overload. Patterns are
+ `java.util.Formatter`-style.
+- These are **complementary to**, not a replacement for, the single cumulative
debug record; they produce independent,
+ additional `LogRecord`s.
+- `req.getLogger()` is exposed directly if you need the `RichLogger` itself.
+
+### Correlating records with a request-scoped `LogContext`
+
+While a thread-confined
+[`LogContext`](/docs/topics/JuneauCommonsLogging#request-scoped-context-logcontext)
scope is open on the request thread,
+every record emitted on that thread — the pipeline's debug record *and* any
handler-emitted convenience-method records
+above — automatically carries the scope's entries, retrievable via
`LogRecordContext.of(record).get(key)`:
+
+```java
+@RestGet(path="/widgets/{id}")
+public Widget getWidget(RestRequest req, @Path("id") String id) {
+ try (var s = RichLogger.context().with("widgetId", id)) {
+ req.fine("loading"); // this record carries widgetId
+ return widgetService.load(id);
+ }
+}
+```
+
+For **true asynchronous responses**, the request-thread `LogContext` snapshot
is captured at the async hand-off and
+re-applied to the completion-path debug record, so that record carries the
same entries even though it is emitted on a
+foreign thread whose live `LogContext` is empty. Note that *ad hoc* logging
performed directly from an async-completion
+callback (not through the debug pipeline) does **not** carry the context in v1
— see the thread-confinement note in
+[`LogContext`](/docs/topics/JuneauCommonsLogging#request-scoped-context-logcontext).
+
+### Always-on request correlation (`requestId`)
+
+The framework opens exactly **one** `requestId`
[`LogContext`](/docs/topics/JuneauCommonsLogging#request-scoped-context-logcontext)
+scope per request, during session build — *before* any `@RestStartCall` hook
runs. Because it is anchored at session
+build rather than at a hook, it covers **every** path that produces a debug
record, including 404s and other
+early-error paths that never reach `@RestStartCall`. The scope is closed at
the very end of request processing, after
+the debug record is emitted (emit-before-close), so the emitted record always
carries the id.
+
+The per-request id is resolved **mint-or-honor**: an inbound `X-Request-Id`
request header is honored if present,
+otherwise a fresh id is minted (a
[`Uuid7`](/docs/topics/JuneauCommonsLogging), time-ordered). The honored value
runs
+through a validator that **defaults to sanitize-and-accept** — control
characters and log-forging sequences are
+stripped and the (bounded) remainder is accepted, rather than
rejected-and-reminted — so a caller-supplied id survives
+end-to-end whenever it is safe to log. The resolved id is echoed back on the
`X-Request-Id` **response** header and is
+available synchronously to handler code via `RestRequest.getRequestId()`.
+
+Resolution behavior is configurable through a `RequestIdSettings` bean
resolved by `RestContext` (a `@Bean` factory
+method or default is picked up automatically):
+
+- `validator(Predicate<String>)` — replace the default sanitize-and-accept
policy (e.g. a strict allowlist regex).
+- `attributeKey(String)` — move the servlet-request attribute the resolved id
is published under.
+- `idSupplier(Supplier<String>)` — replace the mint source.
+
+Every record emitted on the request thread — the pipeline's cumulative debug
record *and* any handler-emitted
+convenience-method records — automatically carries the scope's `requestId`
entry (`LogRecordContext.of(record).get("requestId")`).
+The `statusLine` of the basic debug formatter additionally prepends a
human-readable `[requestId=<id>] ` marker to the
+rendered message. For **true asynchronous responses**, the id rides the same
request-thread `LogContext` snapshot
+described above, so the completion-path record carries the identical
`requestId` in both its structured field and its
+rendered prefix even though it is emitted on a foreign thread.
+
+:::note Filter is now a thin façade
+The pluggable request-id filter no longer owns the lifecycle — the always-on
session-build resolver above is the
+primary correlation mechanism. The filter is retained as a thin configuration
façade whose `apply()` is idempotent
+with respect to the already-resolved session id (it honors, it never
double-mints); its per-instance builder knobs
+(`idSupplier`/`validator`/`attributeKey`) are documented **no-ops** —
configure request-id behavior on
+`RequestIdSettings` instead.
+:::
+
## Interaction with marshall-layer `debug`
REST logger levels now control **logging verbosity only** — they no longer
change marshalling behavior.
diff --git a/pages/topics/10.64.AdminConsoleChrome.md
b/pages/topics/10.64.AdminConsoleChrome.md
new file mode 100644
index 0000000000..de40848485
--- /dev/null
+++ b/pages/topics/10.64.AdminConsoleChrome.md
@@ -0,0 +1,191 @@
+---
+title: "Admin-Console Chrome (juneau-rest-server-console-ui)"
+slug: AdminConsoleChrome
+---
+
+Three new opt-in modules add a themeable, security-hardened admin-console
"chrome" (CSS
+theme tokens, a status/domain pill component, and FreeMarker-macro
composition) on top of
+`juneau-rest-server`, without bleeding a template engine or the DataTables
toolkit into the
+core:
+
+| Module | Adds | Depends on |
+|---|---|---|
+| `juneau-rest-server-console-ui` | `ConsoleChromeMixin` (dynamic,
per-mount-themed `chrome.css` endpoint), `Theme`/`ThemeSettings` (`--jc-*`
token allowlist grammar), `@TagDomain` + `Tag.of(...)` + `TagHtmlRender` (enum
pill/badge rendering) | `juneau-rest-server`, `juneau-bean-html5` |
+| `juneau-rest-server-console-ui-freemarker` | `ConsoleFreemarkerMixin`
(reserved-classpath `base.ftlh` + the `<@tag>` macro) |
`juneau-rest-server-view-freemarker` (`provided`),
`juneau-rest-server-console-ui` |
+| `juneau-rest-server-console-ui-freemarker-datatables` |
`ConsoleDataTablesFreemarkerMixin` (the `<@datatable>` macro) |
`juneau-rest-server-console-ui-freemarker`, `juneau-rest-server-datatables` |
+
+```xml
+<dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest-server-console-ui</artifactId>
+ <version>${juneau.version}</version>
+</dependency>
+<!-- FreeMarker composition (base.ftlh, <@tag>): -->
+<dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest-server-console-ui-freemarker</artifactId>
+ <version>${juneau.version}</version>
+</dependency>
+<!-- Only if you also want the <@datatable> macro: -->
+<dependency>
+ <groupId>org.apache.juneau</groupId>
+
<artifactId>juneau-rest-server-console-ui-freemarker-datatables</artifactId>
+ <version>${juneau.version}</version>
+</dependency>
+```
+
+A chrome-only consumer (no FreeMarker, or FreeMarker without DataTables) pulls
in exactly one
+or two of these — never all three, and never the unrelated `-datatables`
module unless it
+explicitly asks for the macro.
+
+## Why a security-hardened theme, not a raw CSS override?
+
+Letting a consumer supply arbitrary CSS custom-property values (colors, fonts,
a page
+background) is an XSS/CSS-injection surface: a naive `var(--jc-page-bg:
url(javascript:...))`-
+style value sunk into an unescaped stylesheet can exfiltrate data or inject
markup. `Theme`
+closes that gap with a **normalize-then-accept-known-safe** pipeline, not a
denylist:
+
+1. Reject the raw value if it contains any C0/C1/DEL control character
(checked *before*
+ trimming, so a value like `"\u0000 url(evil)"` can't hide a control
character at the
+ boundary).
+2. Trim, then strip CSS `/* ... */` comments.
+3. Case-insensitively reject any `url(` production outright — style values in
this module
+ never carry a URL token, structurally.
+4. Match what remains against an accept-known-safe grammar: hex/functional
colors, length
+ units, font-family lists, and a small set of gradient functions
(`linear-gradient`,
+ `radial-gradient`, etc.) with a grammar tight enough that a gradient
argument list cannot
+ itself smuggle a nested function call outside the allowed shape.
+
+Colors, fonts, and lengths sink **only** into non-URL-capable CSS properties
(`color`,
+`background-color`, `border-color`, `font-family`, sizes/spacing, etc.). The
**only**
+URL-capable sink for a `var(--jc-*)` token is `background-image`, and it's
restricted to two
+specific tokens: `--jc-page-bg` and `--jc-avatar-bg`. Since step 3 above
structurally rejects
+any `url()` production before a value is ever accepted, neither of those two
tokens can be
+used to inject a URL anyway — v1 has no `url()` **token form** at all. The
console logo is a
+static constant baked into the shipped `chrome.css`, never a
consumer-suppliable token.
+
+## `ConsoleChromeMixin` — the dynamic `chrome.css` endpoint
+
+```java
+import org.apache.juneau.rest.server.console.*;
+
+@Rest(mixins=ConsoleChromeMixin.class)
+public class AdminResource extends BasicRestServlet {
+
+ @Bean
+ ConsoleChromeMixin consoleChrome() {
+ return ConsoleChromeMixin.create()
+ .theme(ThemeSettings.create()
+ .token("--jc-accent", "#3366cc")
+ .token("--jc-page-bg", "#f4f6f8")
+ .build())
+ .build();
+ }
+}
+```
+
+Mounting the mixin serves a themed `chrome.css` (structural rules from the
shipped
+stylesheet, plus a `:root { --jc-accent: #3366cc; ... }` block built from the
validated
+tokens) at a configurable path. Each independently configured mount gets its
own compiled,
+cached stylesheet — no cross-mount token leakage.
+
+## Enum pills — `@TagDomain`, `Tag.of(...)`, `TagHtmlRender`
+
+```java
+import org.apache.juneau.rest.server.console.*;
+
+@TagDomain(domain="status")
+public enum ReleaseStatus { RELEASED, DRAFT }
+
+public class Release {
+ @Html(render=TagHtmlRender.class)
+ public ReleaseStatus status;
+}
+```
+
+Serializing a `Release` through `HtmlSerializer` (or through
`DataTablesTable`, see below)
+renders `status` as `<span class="tag status released">Released</span>`
instead of the raw
+enum name — `chrome.css` styles `.tag.<domain>.<value>` pairs. `Tag.of(domain,
value)`
+lowercases and validates both arguments against `^[a-z0-9_-]+$`, rejecting
anything else
+(including HTML-significant characters) with an `IllegalArgumentException`
rather than
+emitting unsafe markup.
+
+## FreeMarker composition — `<@tag>` and `<@datatable>`
+
+`ConsoleFreemarkerMixin extends FreemarkerMixin`: register it exactly where
you'd register a
+plain `FreemarkerMixin`, typed as `FreemarkerMixin` (the return type is
load-bearing —
+`FreemarkerViewRenderer` looks it up by that exact type):
+
+```java
+import org.apache.juneau.rest.server.view.freemarker.*;
+import org.apache.juneau.rest.server.view.freemarker.console.*;
+
+@Rest(mixins=FreemarkerMixin.class)
+public class AdminResource extends BasicRestServlet {
+
+ @Bean
+ public FreemarkerMixin freemarker() {
+ return ConsoleFreemarkerMixin.create().basePath("/templates/").build();
+ }
+}
+```
+
+`ConsoleFreemarkerMixin` augments the resolved `Configuration`'s
`TemplateLoader` with a
+second, classpath-**root**-rooted `ClassTemplateLoader` — your own
`basePath`-relative
+templates are tried first, so nothing under your `/templates/` tree is ever
shadowed. Pull in
+the reserved chrome template with a **loader-root-absolute** include (a bare
relative path
+resolves against the *including* template's own directory, not the classpath
root, and would
+miss):
+
+```html
+<#include "/org/apache/juneau/console/base.ftlh">
+...
+<@tag domain="status" value=release.status/>
+```
+
+If you also depend on `juneau-rest-server-console-ui-freemarker-datatables`,
swap in
+`ConsoleDataTablesFreemarkerMixin` (a subclass of `ConsoleFreemarkerMixin`,
same load-bearing
+`FreemarkerMixin` return type) to additionally get the `<@datatable>` macro:
+
+```html
+<#include "/org/apache/juneau/console/datatables/datatable.ftlh">
+...
+<@datatable id="releases" rows=releases rowType="com.example.Release"/>
+```
+
+`<@datatable>` renders a `DataTablesTable.of(...)`-built `<table
class="jc-table"
+data-juneau-datatable ...>`, honoring each row property's `@Html(render=...)`
— so a
+`@TagDomain` enum column renders as pills inside the table, not raw enum names.
+
+**Consumer-supplied `Configuration` is honored, not augmented.** If your
`BeanStore` already
+has a `freemarker.template.Configuration` bean (Spring Boot autoconfig, or a
microservice
+`BasicBeanStore.put`), `resolveConfiguration` returns that exact instance
untouched — the
+console loader (and, transitively, the `<@tag>`/`<@datatable>` macros) is not
spliced in. Add
+the console template location to your own `Configuration`'s loader if you need
it in that
+setup.
+
+## `DataTablesTable` now honors `@Html(render=...)` (behavior change, not
purely additive)
+
+`DataTablesTable.of(MarshallingContext, String, Collection<?>, Class<?>)` — the
+bean-row-type overload — now resolves and applies each property's
`@Html(render=...)` the
+same way the ordinary `HtmlSerializer`/`HtmlDocSerializer` path already does.
This is **not a
+purely additive change**: a bean already using `@Html(render=...)` for the
existing
+serializer path now *also* renders through that same render when passed
through this
+`DataTablesTable` overload, where previously it rendered the raw value.
`Map`-based rows (no
+`BeanPropertyMeta` to resolve a render from) are unaffected and continue to
render raw values.
+
+## Limitations and out-of-scope (v1)
+
+- **No page-level chrome layout.** This release covers the CSS theme, the pill
component, and
+ macro composition — not a shipped nav/header/sidebar shell. That's tracked
as a fast-follow.
+- **No `url()` token form.** Every `var(--jc-*)` value is validated by the
accept-known-safe
+ grammar described above, which structurally excludes `url()` productions; a
future release
+ may add a `javascript:`/`data:`/`url()`-scheme allowlist alongside an actual
`url()` token
+ form, but v1 ships with neither.
+- **The logo is a static `chrome.css` constant**, not a configurable token.
+
+## See also
+
+- [FreeMarker View Support](/docs/topics/FreemarkerViewSupport)
+- [REST Server — Composition (mixins,
paths)](/docs/topics/RestServerComposition)
+- [Using with HTML Beans](/docs/topics/HtmlBeans)
diff --git a/pages/topics/13.10.RestClientLoggingAndDebugging.md
b/pages/topics/13.10.RestClientLoggingAndDebugging.md
index 6867025f01..70bad6abab 100644
--- a/pages/topics/13.10.RestClientLoggingAndDebugging.md
+++ b/pages/topics/13.10.RestClientLoggingAndDebugging.md
@@ -92,9 +92,132 @@ Debug detail is controlled solely by the resolved JUL
logger level:
- `INFO`: basic summary line.
- `FINE`: basic + request/response headers.
-- `FINEST`: basic + headers + bounded request/response body capture.
+- `FINEST`: basic + headers + bounded request/response body capture — **only
if** body dumping has been explicitly
+ opted in via the `JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES` environment variable
(see
+ [Secure by default](#secure-by-default) below); otherwise `FINEST` emits a
suppression placeholder instead of
+ body content.
-`BasicRestClientDebugFormatter` redacts sensitive headers using the shared
redaction utility and captures bodies up to a configurable cap (`bodyCap()`).
+Exactly one `LogRecord` is emitted per request/error, always at JUL `INFO` —
the resolved tier controls message
+detail only, so `FINE`/`FINEST`-selected detail still travels through `INFO`
sinks. This applies uniformly to both
+places the pipeline emits from: the normal request-completion path and the
transport-exception (synthetic error
+response) path.
+
+## Secure by default
+
+`BasicRestClientDebugFormatter` mirrors the NG REST *server*'s
`BasicRestDebugFormatter` secret-hardening design (see
+[REST server Logging/Debugging » Secure by
default](/docs/topics/RestServerLoggingAndDebugging#secure-by-default)):
+raising a client's debug logger to `FINE`/`FINEST` can never, by itself, turn
the log stream into a plaintext-credential
+dump or a log-forging channel.
+
+### No body dumps without an explicit opt-in
+
+Request/response **bodies are never dumped by default** — not at `FINEST`, not
for any content type. Dumping is a
+deliberate operator opt-in behind the **same environment-variable master gate
the server uses**:
+
+```bash
+export JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES=true
+```
+
+Semantics are byte-identical to the server's gate: environment variable only
(no system-property fallback), read
+once per JVM and cached, trim-then-parse truthy (any non-empty trimmed value
enables except case-insensitive
+`false`/`0`). With the gate unset, `formatBody` emits a suppression
placeholder naming the environment variable plus
+the captured byte count and content-type — never body bytes in any
representation (plaintext, hex, or otherwise),
+even if a scrubber is configured. With the gate set, bodies render only when
the content is renderable (see below);
+non-renderable/binary content, or a non-identity `Content-Encoding` (e.g.
`gzip`), yields a distinct "not rendered"
+placeholder instead.
+
+:::danger
+Setting `JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES` re-enables plaintext
request/response body logging at `FINEST` for
+**both** the client and the server. Treat `FINEST` client logs as sensitive,
and supply a `bodyScrubber(...)` if any
+secret-bearing bodies (tokens, PII, card data) are possible. Prefer leaving
the gate unset in production.
+:::
+
+`isBodyRenderable(String contentType)` (on the `RestClientDebugFormatter` SPI)
decides whether a body is text worth
+logging, using the same predicate as the server's
`RestDebugFormatter.isBodyRenderable`: renderable for `text/*`,
+`application/json`, `application/xml`, any `+json`/`+xml` suffix type, and
`application/x-www-form-urlencoded`;
+non-renderable for everything else, including `multipart/form-data`, absent,
or blank content types. The decision is
+made independently per side — the request's own `Content-Type` governs the
request body, the response's own governs
+the response body.
+
+### Body scrubbing SPI
+
+When body dumping is opted in, transform body text before it's logged via the
+<a href="/site/apidocs/org/apache/juneau/http/RestDebugBodyScrubber.html"
target="_blank">RestDebugBodyScrubber</a>
+SPI, registered with `bodyScrubber(...)`:
+
+```java
+new BasicRestClientDebugFormatter().bodyScrubber((contentType, body) ->
body.replaceAll("\"password\":\"[^\"]*\"", "\"password\":\"***\""));
+```
+
+- **Gate-subordinate**: the scrubber runs only after the master gate has
already permitted dumping; it merely
+ chooses *scrubbed-vs-raw* text and never causes a body to be emitted while
the gate is unset.
+- **Fail-closed**: if `scrub(...)` throws or returns `null`, the formatter
emits a suppression placeholder instead of
+ the body — it never falls back to the raw, unscrubbed body.
+- Non-`null` output is still sanitized (control characters escaped) and
length-capped before it reaches the log.
+
+No default scrubber ships: with the gate set and no scrubber, the raw body is
dumped (sanitized + capped).
+
+### Header redaction — widened set, separator-normalized
+
+At the headers tier (`FINE`-or-finer), header values are masked with
`[REDACTED]` against a **widened,
+formatter-local** set — a superset of the shared `RedactedHeaders.DEFAULT`
(`Authorization`, `Cookie`, `Set-Cookie`,
+`Proxy-Authorization`, `X-API-Key`) plus `X-Auth-Token`, `X-Authorization`,
`WWW-Authenticate`, `Referer`, and
+`Location`. Matching is **separator-normalized**: it case-folds and strips
`-`/`_`, so `X-Auth-Token`,
+`X_Auth_Token`, and `XAuthToken` all match. The shared
`RedactedHeaders.DEFAULT` itself is left unchanged; this
+widened set is client-formatter-local, matching the server's own decision to
keep its widened set formatter-local
+rather than merged into the shared default. Configure the masked set with
`redactedHeaders(Collection<String>)`
+(replaces the masked set).
+
+### Log-forging sanitization
+
+The request method and URI, header names and values, and body text (raw or
scrubber-produced) are passed through a
+sanitizer that escapes CR/LF and other control characters to visible, inert
forms (`\r`, `\n`, `\uXXXX`). A remote
+server therefore cannot inject a newline into a response header/body to forge
a second physical log line. The order
+applied to every emitted string is **mask → escape → cap**.
+
+### Bounded body output
+
+Captured body bytes are bounded at capture time by `bodyCap(int)` (default 8
KB), then `…[truncated N bytes]` marks
+what was omitted; a second character-level cap (`bodyCap() * 6`) is applied
after sanitization so a scrubber that
+returns an oversized string is still bounded. Unlike the server's formatter,
the client does not add separate
+header-count/header-scan/field-length/URI-length bounding knobs — the client's
header source is already a finite,
+bounded list rather than an adversarial unbounded input stream, so those extra
knobs have no client-side analog.
+
+## Request correlation (`X-Request-Id`)
+
+`RestClient` automatically participates in end-to-end request correlation. By
default, every outgoing request carries
+an `X-Request-Id` header so a call can be traced through the server logs
(which honor that inbound id — see
+[REST server » Always-on request
correlation](/docs/topics/RestServerLoggingAndDebugging#always-on-request-correlation-requestid)):
+
+- **Auto-send / mint.** If the request has no `X-Request-Id` header, the
client mints a fresh time-ordered
+ [`Uuid7`](/docs/topics/JuneauCommonsLogging) and sends it. A header the
caller already set (globally on the builder
+ or per-request) is **honored, never overwritten**.
+- **Pre-call setter.** `RestRequest.requestId(String)` sets the id for a
single call fluently; `getRequestId()` reads
+ the id that was (or will be) sent.
+- **Echo capture.** The client captures the `X-Request-Id` the server echoes
on the response, exposed via
+ `RestResponse.getRequestId()`. When correlating logs, the client uses the
**echoed** id when present and falls back
+ to the **sent** id otherwise (the *effective* id).
+- **Opt-out.** `RestClient.Builder.sendRequestId(false)` disables auto-sending
entirely; an explicitly caller-supplied
+ `X-Request-Id` header still travels.
+
+The effective id flows into debug logging: `RestClientDebugPipeline` stamps
each emitted `LogRecord` with a structured
+`requestId` field (via `LogRecordContext.attachIfAbsent(...)`), and
`BasicRestClientDebugFormatter` prepends a
+human-readable `[requestId=<id>] ` marker to the rendered message — mirroring
the server's `statusLine` prefix so a
+single request reads identically on both sides.
+
+```java
+try (var client = RestClient.builder().build();
+ var res = client.get("http://localhost/widgets/123").run()) {
+ // Server-honored, echoed back:
+ var id = res.getRequestId();
+}
+
+// Explicit per-call id (honored, not overwritten):
+client.get("/widgets/123").requestId("order-9f2c").run();
+
+// Opt out of auto-sending entirely:
+var quiet = RestClient.builder().sendRequestId(false).build();
+```
For tests, capture emitted records directly via
`RichLogger.captureEvents(...)`:
diff --git a/sidebars.ts b/sidebars.ts
index 164d364220..adf2179954 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1772,6 +1772,11 @@ const sidebars: SidebarsConfig = {
id:
'topics/10.63.RestServerRdf',
label: '10.63. RDF
Support (juneau-rest-server-rdf)',
},
+ {
+ type: 'doc',
+ id:
'topics/10.64.AdminConsoleChrome',
+ label: '10.64.
Admin-Console Chrome (juneau-rest-server-console-ui)',
+ },
],
link: {
type: 'doc',