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 |
