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

commit 0f587838a7f9545e78e9b77a0fe8bd7b78bcb8d4
Author: James Bognar <[email protected]>
AuthorDate: Sun Aug 16 11:17:41 2026 -0400

    TODO-372: Decouple REST debug single-signal side effects (372a + 372b)
    
    Splits the former single log-level signal into independent controls and 
fixes
    async/reactive debug capture.
    
    372a — echo + marshalling-debug decoupling:
    - EchoMixin visibility is now controlled by its own explicit .enabled() 
control
      rather than being unhidden when the logger is raised to FINE.
    - debugMarshalling is a first-class annotation on @Rest/@RestOp (+ all verb
      annotations) with cascading op-context resolution, decoupled from log 
level.
    - Marshalling decoupling proven via session-.debug() observables (serializer
      traversal-stack-prefix message + parser buffered-input retention), both
      red-on-main; consumers migrated to .enabled().
    
    372b — async Phase B emit on the completion path:
    - Async/reactive debug records now emit after the response body/headers 
exist,
      via an immutable RestDebugSnapshot captured at handoff + a deferred
      emitOnCompletion, replacing the incomplete synchronous finish() capture.
    - Adopts TODO-368's stable-INFO emit contract; exactly-once completion emit 
for
      both async-dispatch and reactive paths; delayed-async (h01) and
      delayed-reactive (r01) gates confirmed red-on-main then green.
    
    Docs: 10.32.RestServerLoggingAndDebugging, 10.23.OpsIntrospectionMixins,
    27.V10MigrationGuide, release-notes/10.0.0 updated.
---
 pages/release-notes/10.0.0.md                      |  9 +++
 pages/topics/10.23.OpsIntrospectionMixins.md       | 63 +++++++++++--------
 .../topics/10.32.RestServerLoggingAndDebugging.md  | 71 +++++++++++++++++++---
 pages/topics/27.V10MigrationGuide.md               | 31 ++++++++++
 4 files changed, 141 insertions(+), 33 deletions(-)

diff --git a/pages/release-notes/10.0.0.md b/pages/release-notes/10.0.0.md
index 144893b296..d2e1303e8e 100644
--- a/pages/release-notes/10.0.0.md
+++ b/pages/release-notes/10.0.0.md
@@ -492,6 +492,15 @@ for the full removed-symbol mapping.
 - **Bounded body buffering.** Request/response body-caching wrappers are 
installed only when the resolved logger
   is loggable at `FINEST`, and buffer at most the configured cap rather than 
the entire stream — an improvement
   over the prior unbounded debug body caching.
+- **Completion-path emission for async/reactive responses.** True asynchronous 
responses
+  (`CompletableFuture`/`CompletionStage`) and reactive SSE/NDJSON streams now 
emit their single debug record on the
+  response-completion path — after the async body and any completion-time 
headers have been written — instead of
+  during the synchronous request finish. This fixes incomplete capture where 
an async record could be emitted before
+  its late headers/body existed. A resolved logger/formatter/detail-tier 
snapshot is captured on the request thread
+  at the async hand-off and carried across the completion hop; finish-time 
attributes such as `Exec time` are
+  recorded on the completion path. Synchronous responses and the 
`MockRestClient`/non-async-container fallback are
+  unchanged (still emitted during synchronous finish), and `@RestEndCall` 
still runs on the request thread. Exactly
+  one `INFO` record is emitted per request on every path.
 - **Logger level controls verbosity only.** REST-driven marshalling debug 
behavior and `EchoMixin` reachability are
   **decoupled** from the JUL level (see the dedicated entry below). 
`RestRequest.isDebug()` still exists, now derived
   and read-only (`true` when the resolved logger is `FINE`-or-finer), but the 
framework no longer keys any behavior
diff --git a/pages/topics/10.23.OpsIntrospectionMixins.md 
b/pages/topics/10.23.OpsIntrospectionMixins.md
index 5c803b6a83..c06fbd7b1e 100644
--- a/pages/topics/10.23.OpsIntrospectionMixins.md
+++ b/pages/topics/10.23.OpsIntrospectionMixins.md
@@ -16,7 +16,7 @@ configure them via a `@Bean` factory, and leave the rest 
unmounted.
 
 | Mixin | Default `paths` | Default behavior | Why it exists |
 |---|---|---|---|
-| 
[`EchoMixin`](/site/apidocs/org/apache/juneau/rest/server/ops/EchoMixin.html) | 
`/echo/*`, `/debug/echo/*` | `404 Not Found` until `Debug` is enabled. When 
debug-on, returns a JSON body reflecting the inbound method, path, query 
string, headers (sensitive ones redacted), query params, attributes, and 
bounded body capture. | Round-trip request introspection — invaluable for 
diagnosing proxy / mTLS / auth-header issues without spinning up a packet 
capture. |
+| 
[`EchoMixin`](/site/apidocs/org/apache/juneau/rest/server/ops/EchoMixin.html) | 
`/echo/*` | `404 Not Found` by default — reachable only when **explicitly 
enabled** (`EchoMixin.create().enabled()` or the `${juneau.echo.enabled:false}` 
fallback), independent of logger level. When enabled, returns a JSON body 
reflecting the inbound method, path, query string, headers (sensitive ones 
redacted), query params, attributes, and bounded body capture. | Round-trip 
request introspection — invalua [...]
 | 
[`AdminMixin`](/site/apidocs/org/apache/juneau/rest/server/ops/AdminMixin.html) 
| `/admin/threads`, `/admin/heap`, `/admin/cache/flush`, `/admin/ratelimit` | 
`403 Forbidden` until the host registers a `@Bean RestGuardList`. Once 
unlocked: `GET /admin/threads` (JSON thread dump), `GET /admin/heap` (Runtime + 
MemoryMXBean stats), `POST /admin/cache/flush` (run registered hooks), `GET 
/admin/ratelimit` (registered `RateLimitGuard` beans — config + live per-key 
bucket snapshot). | JVM oper [...]
 | 
[`RouteIndexMixin`](/site/apidocs/org/apache/juneau/rest/server/ops/RouteIndexMixin.html)
 | `/options`, `/routes` | JSON list of every `@RestOp`-annotated method on the 
host (and its mixins), excluding `@OpSwagger(ignore=true)` ops and itself. Each 
entry: `path`, `methods`, `summary`, `description`, `deprecated`. | 
Machine-readable navigation index for tooling that needs a non-Swagger view of 
the URL surface (smoke-test scripts, auto-generated nav, etc.). |
 
@@ -38,8 +38,7 @@ three with builder-driven configuration plus the deny-all 
override seam:
         EchoMixin.class,
         AdminMixin.class,
         RouteIndexMixin.class
-    },
-    debug = "conditional"   // gates EchoMixin per-request
+    }
 )
 public class ApiResource extends RestServlet {
 
@@ -55,9 +54,10 @@ public class ApiResource extends RestServlet {
             .build();
     }
 
-    // Optional: tighten echo body cap or extend the redacted-header list.
+    // Enable echo (default OFF) and, optionally, tighten the body cap or 
extend the redacted-header list.
     @Bean public EchoMixin echo() {
         return EchoMixin.create()
+            .enabled()                       // reachable regardless of logger 
level; omit to keep it 404
             .bodyLimit(64 * 1024L)
             .redactHeader("X-Internal-Trace")
             .build();
@@ -76,9 +76,10 @@ public class ApiResource extends RestServlet {
 The framework's mixin walk picks up each `@Bean <MixinClass>` factory 
**before** falling back to
 no-arg construction (see [Mixin 
Sub-Contexts](/docs/topics/RestServerMixinSubContexts) for the
 underlying lookup), so the host controls the configuration end-to-end without 
subclassing. A
-mixin without a `@Bean` factory gets default behavior: `EchoMixin` uses the 1 
MB body cap
-and the standard redacted-header set; `AdminMixin` runs zero cache-flush hooks 
and the
-default thread-name-prefix exclude list; `RouteIndexMixin` has no configurable 
state.
+mixin without a `@Bean` factory gets default behavior: `EchoMixin` is 
**disabled by default**
+(`404`) and, once enabled, uses the 1 MB body cap and the standard 
redacted-header set;
+`AdminMixin` runs zero cache-flush hooks and the default thread-name-prefix 
exclude list;
+`RouteIndexMixin` has no configurable state.
 
 ## Standalone deployment
 
@@ -86,8 +87,11 @@ Each mixin is a fully-fledged `@Rest`-annotated resource and 
can also be subclas
 its own top-level servlet:
 
 ```java
-@Rest(paths = {"/echo/*"}, debug = "conditional")
-public class EchoResource extends EchoMixin { }
+@Rest(paths = {"/echo/*"})
+public class EchoResource extends EchoMixin {
+    // Enable echo (default OFF) — reachable regardless of logger level.
+    @Bean public EchoMixin echo() { return 
EchoMixin.create().enabled().build(); }
+}
 ```
 
 Both deployment styles (mixin into an existing servlet vs. mount as a sibling 
servlet) work the
@@ -102,13 +106,24 @@ redaction surviving the network stack.
 
 ### `EchoMixin`
 
-**Debug-gating.** The handler is gated behind the resolved JUL logger level: 
when the logger is below
-`FINE`, the endpoint returns `404 Not Found` so the existence of the URL isn't 
disclosed. Raising the
-resource's (or the `echo` operation's) logger to `FINE`-or-finer turns the 
endpoint on — see
-[Logging / Debugging](/docs/topics/RestServerLoggingAndDebugging) for the full 
level model, which is the
-same signal that controls REST debug capture generally. Because every request 
to `/echo/*` then reflects
-the full payload back to anyone who can reach the URL, pair the elevated 
logger with a guard chain in
-production so only authorized operators can reach the endpoint at all.
+**Enablement (default OFF).** The handler is **disabled by default** and 
returns `404 Not Found` so the
+existence of the URL isn't disclosed. Reachability is an explicit, non-logging 
decision — **logger level has no
+effect** on it. Enablement is tri-state in `EchoMixin.Builder`: an explicit 
`enabled(true)`/`enabled(false)` wins;
+when unset, it resolves the `${juneau.echo.enabled:false}` SVL fallback 
(system property or relaxed environment
+variable) per request:
+
+```java
+@Bean public EchoMixin echo() {
+    return EchoMixin.create().enabled().build();   // reachable; independent 
of JUL level
+}
+```
+
+Because the `juneau.echo.enabled` fallback is a **JVM-global** signal, a 
single property flips echo on for *every*
+resource that mixes `EchoMixin` in across the JVM — prefer per-host builder 
`enabled()` to bound the blast radius.
+Because every request to `/echo/*` then reflects the full payload back to 
anyone who can reach the URL, pair
+enablement with the host's inherited `@Rest(guards=...)` chain in production 
so only authorized operators can reach
+the endpoint at all. See [Logging / 
Debugging](/docs/topics/RestServerLoggingAndDebugging) for how logger levels
+now control verbosity only.
 
 **Sensitive-header redaction.** Token-bearing headers MUST never be reflected 
back; that would
 defeat any auth scheme in front of the endpoint. The default redacted list is, 
case-insensitively,
@@ -239,11 +254,9 @@ and the listing is computed off the host `RestContext` at 
request time.
 
 ### MockRest
 
-All three mixins work with `MockRestClient`. The Echo mixin's debug-gating 
relies on the host's
-resolved JUL logger level; set it to `FINE`-or-finer (e.g. via the mock 
request's `logLevel(Level)`
-helper — see [Logging / 
Debugging](/docs/topics/RestServerLoggingAndDebugging#testing)) and pair with
-`MockRestClient.buildLax(...)` to exercise the full echo. For the Admin mixin, 
register an empty
-`@Bean RestGuardList` to bypass the deny-all in unit tests:
+All three mixins work with `MockRestClient`. Echo is disabled by default, so 
register a `@Bean EchoMixin` that
+calls `.enabled()` (logger level is irrelevant) and pair with 
`MockRestClient.buildLax(...)` to exercise the full
+echo. For the Admin mixin, register an empty `@Bean RestGuardList` to bypass 
the deny-all in unit tests:
 
 ```java
 @Rest(mixins = AdminMixin.class)
@@ -275,10 +288,10 @@ without Spring in the picture.
 There's no pre-existing version of this pack to migrate from — this is the 
first cut. If your
 service hand-rolled an echo / admin / routes endpoint before adopting the pack:
 
-1. Replace the hand-rolled `/echo` handler with `mixins = EchoMixin.class`, 
raise the resource's
-   (or the `echo` operation's) logger to `FINE`-or-finer, and pair with a 
guard chain.
-   **Audit your old echo for sensitive-header leaks** — the pre-pack 
hand-rolls almost always
-   reflected `Authorization` and `Cookie` headers verbatim.
+1. Replace the hand-rolled `/echo` handler with `mixins = EchoMixin.class`, 
enable it explicitly via a
+   `@Bean EchoMixin` that calls `.enabled()` (it is `404` by default and 
logger level does not affect it),
+   and pair with a guard chain. **Audit your old echo for sensitive-header 
leaks** — the pre-pack hand-rolls
+   almost always reflected `Authorization` and `Cookie` headers verbatim.
 2. Replace any hand-rolled JVM-introspection endpoints with `mixins = 
AdminMixin.class`
    plus a `@Bean RestGuardList`. The cache-flush hooks register via the 
builder rather than via
    per-handler subclassing.
diff --git a/pages/topics/10.32.RestServerLoggingAndDebugging.md 
b/pages/topics/10.32.RestServerLoggingAndDebugging.md
index 248d958012..0219f0bf09 100644
--- a/pages/topics/10.32.RestServerLoggingAndDebugging.md
+++ b/pages/topics/10.32.RestServerLoggingAndDebugging.md
@@ -137,6 +137,27 @@ customization surface is the 
[`RestDebugFormatter`](#the-restdebugformatter-spi)
   can disagree. The only symptom is a body that wasn't cached — Phase B simply 
degrades to headers-only for that
   one request. This isn't a correctness bug, just a best-effort consequence of 
resolving the tier twice.
 
+### When Phase B runs for async and reactive responses
+
+Phase B emission timing depends on whether a request obtains a real servlet 
`AsyncContext`:
+
+- **Synchronous responses** (and any request handled through the 
`MockRestClient` / non-async-container fallback)
+  emit their single record during synchronous request finish, exactly as 
before.
+- **True asynchronous responses** (`CompletableFuture` / `CompletionStage` 
return types) emit on the
+  response-completion thread, *after* the async body and any headers set 
during completion have been written — not
+  during the synchronous finish of the request thread. The resolved 
logger/formatter/detail-tier is snapshotted on
+  the request thread at the async hand-off and carried across the completion 
hop, so completion-path rendering never
+  re-resolves state from foreign-thread context. This fixes the previous 
behavior where an async record could be
+  emitted before its late headers/body existed, capturing an incomplete 
picture. Finish-time attributes such as
+  `Exec time` are recorded on the completion path so they reflect the true 
request duration.
+- **Reactive SSE / NDJSON streams** emit once after the stream terminates 
(normal completion or error), through the
+  same completion path. At `FINEST` the captured stream prefix is bounded by 
the body cap and shows a visible
+  truncation marker when the stream exceeds it.
+
+Regardless of timing, exactly one `LogRecord` is emitted per request, always 
at `INFO` (the resolved tier controls
+message detail only). Note that `@RestEndCall` methods still run on the 
request thread at lifecycle time (before the
+async body completes); completion-path emission is a debug-pipeline detail, 
not a general lifecycle migration.
+
 ## The `RestDebugFormatter` SPI
 
 <a 
href="/site/apidocs/org/apache/juneau/rest/server/logging/RestDebugFormatter.html"
 target="_blank">RestDebugFormatter</a>
@@ -294,16 +315,50 @@ for operational guidance on raising logger levels in 
production.
 
 ## Interaction with marshall-layer `debug`
 
-`RestRequest.isDebug()` still exists, but it's now **derived and read-only**: 
it returns `true` when the resolved
-logger is at `FINE`-or-finer, and there is no setter. The marshall layer's own 
`debug` forwarding (recursion
-detection during serialization, and richer serialization exceptions) reads 
this same derived flag, so that
-behavior remains available while a resource is logging at `FINE`-or-finer — no 
separate toggle needed.
+REST logger levels now control **logging verbosity only** — they no longer 
change marshalling behavior.
+`RestRequest.isDebug()` still exists as a derived, read-only logger query 
(`true` when the resolved logger is at
+`FINE`-or-finer), but the framework no longer keys any behavior off it.
+
+Serializer/parser debug behavior — recursion detection during serialization, 
richer/stack-prefixed serialization
+exceptions, and retained/quoted parse input in error messages — is now 
controlled by a dedicated cascading
+`debugMarshalling` setting that defaults **OFF at every log level**:
+
+```java
+@Rest(debugMarshalling="true")               // resource-wide
+public class MyResource extends RestServlet {
+    @RestGet(path="/a", debugMarshalling="true")   // op opts in
+    public MyBean a() {...}
+
+    @RestGet(path="/b")                             // blank ("") ⇒ inherit 
resource setting
+    public MyBean b() {...}
 
-## `EchoMixin` gating
+    @RestGet(path="/c", debugMarshalling="false")   // op opts out even if the 
resource is on
+    public MyBean c() {...}
+}
+```
+
+`debugMarshalling` is a `String` on all eight REST annotation types (`@Rest`, 
`@RestOp`, and the six HTTP-method
+annotations), so a blank `""` value means *inherit* (op inherits the resource, 
which resolves the
+`${juneau.debugMarshalling:false}` default). It is queryable at runtime via 
`RestRequest.isDebugMarshalling()`. A
+serializer/parser configured with its own marshall-layer `Context.debug` 
remains independent — the REST layer only
+forwards its `debugMarshalling` decision and never clobbers a directly-set 
`Context.debug`.
+
+## `EchoMixin` reachability
+
+The [`EchoMixin`](/docs/topics/OpsIntrospectionMixins) echo-back endpoint is 
**disabled by default** and returns
+`404 Not Found` regardless of logger level. Raising a logger to 
`FINE`/`FINEST` no longer unhides it — reachability
+is an explicit, non-logging decision:
+
+```java
+@Bean public EchoMixin echo() {
+    return EchoMixin.create().enabled().build();   // or .enabled(true)
+}
+```
 
-The [`EchoMixin`](/docs/topics/OpsIntrospectionMixins) echo-back endpoint is 
gated on the same signal: it returns
-`404 Not Found` unless the resolved logger is at `FINE`-or-finer, so the 
endpoint's existence isn't disclosed to
-callers who haven't been given debug access. See
+Unset enablement resolves the `${juneau.echo.enabled:false}` SVL fallback 
(system property / relaxed environment
+variable), and an explicit `enabled(false)` always wins over that default. 
Because that fallback is a JVM-global
+signal, prefer per-host builder enablement so one host's echo doesn't silently 
reach across every resource that
+mixes `EchoMixin` in; pair it with the host's inherited `@Rest(guards=...)` 
chain for authorization. See
 [Ops / Introspection Mixin Pack](/docs/topics/OpsIntrospectionMixins) for the 
full behavior, including the
 sensitive-header redaction that applies to the echoed-back request as well.
 
diff --git a/pages/topics/27.V10MigrationGuide.md 
b/pages/topics/27.V10MigrationGuide.md
index ea276b53cc..b6af292537 100644
--- a/pages/topics/27.V10MigrationGuide.md
+++ b/pages/topics/27.V10MigrationGuide.md
@@ -186,6 +186,37 @@ See [Logging / 
Debugging](/docs/topics/RestServerLoggingAndDebugging) for the fu
 ["Migrating from 
pre-10.0"](/docs/topics/RestServerLoggingAndDebugging#migrating-from-pre-100) 
section, which
 links back here.
 
+### Log level no longer controls `EchoMixin` reachability or marshalling debug
+
+Two behaviors that previously rode the JUL logger level are now decoupled from 
it — raising a logger changes
+verbosity only. This is an unreleased-10.0 **hard break** (no compatibility 
shim, no new echo annotation) and a
+deliberate **security** behavior change.
+
+| Old | New | Notes |
+|-----|-----|-------|
+| Raising a resource/op logger to `FINE`-or-finer un-hid `EchoMixin`'s 
`/echo/*` endpoint (it returned `404` below `FINE`). | `EchoMixin` is 
**disabled by default** and returns `404` at every log level. Enable 
explicitly: `EchoMixin.create().enabled().build()`. | Enablement is tri-state: 
explicit `enabled(true)`/`enabled(false)` wins; unset resolves the 
`${juneau.echo.enabled:false}` SVL fallback (system property / relaxed env 
var). The fallback is JVM-global — prefer per-host builder `e [...]
+| Raising a logger to `FINE`-or-finer engaged serializer/parser debug behavior 
(richer/stack-prefixed serialization exceptions, retained/quoted parse input). 
| Controlled by the dedicated cascading `debugMarshalling` setting, **OFF** by 
default at every level. | `String` attribute on all eight REST annotation types 
(`@Rest`, `@RestOp`, six HTTP-method annotations); blank `""` means *inherit* 
(op inherits resource, which resolves `${juneau.debugMarshalling:false}`). 
Query at runtime via ` [...]
+
+**Migration.** If you relied on raising a logger to reach `/echo/*`, add a 
`@Bean EchoMixin` factory that calls
+`.enabled()` (or set `-Djuneau.echo.enabled=true` for a JVM-global default). 
If you relied on a raised logger for
+richer marshalling errors, set `@Rest(debugMarshalling="true")` / 
`@RestOp(debugMarshalling="true")` (or the SVL
+default) on the relevant resource/op. See [Ops / Introspection Mixin 
Pack](/docs/topics/OpsIntrospectionMixins) and
+[Logging / 
Debugging](/docs/topics/RestServerLoggingAndDebugging#interaction-with-marshall-layer-debug).
+
+### Async/reactive debug records now emit on the completion path
+
+For true asynchronous responses (`CompletableFuture`/`CompletionStage`) and 
reactive SSE/NDJSON streams, the single
+debug/access record is now emitted on the **response-completion path** — after 
the async body and any headers set
+during completion have been written — rather than during the synchronous 
request finish. Previously an async record
+could be emitted before its late headers/body existed, producing an incomplete 
capture. Synchronous responses and
+the `MockRestClient`/non-async-container fallback are unchanged (still emitted 
during synchronous finish).
+
+**Migration.** None for typical usage. Two behavioral notes if you assert on 
async debug output: (1) an async
+record's headers/body/`Exec time` now reflect the *completed* response, so 
previously-empty async captures will now
+be populated; and (2) `@RestEndCall` still runs on the request thread at 
lifecycle time (before the async body
+completes) — it was not moved to the completion thread. Exactly one 
`INFO`-level record is still emitted per request
+on every path. See [Logging / 
Debugging](/docs/topics/RestServerLoggingAndDebugging) for the full model.
+
 ## Health Probe Routing (`mixins` + `paths`)
 
 | Old | New | Notes |

Reply via email to