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 &raquo; 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 &rarr; escape &rarr; 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 &raquo; 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',

Reply via email to