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 c9155a5f9d docs: OpsIntrospectionMixins topic page + 9.5 release notes
(FINISHED-77)
c9155a5f9d is described below
commit c9155a5f9d2d0c3d948fcd29fa036dc2aa88c4f1
Author: James Bognar <[email protected]>
AuthorDate: Sun May 24 19:12:34 2026 -0400
docs: OpsIntrospectionMixins topic page + 9.5 release notes (FINISHED-77)
Co-authored-by: Cursor <[email protected]>
---
pages/release-notes/9.5.0.md | 50 +++++
pages/topics/10.14c.OpsIntrospectionMixins.md | 269 ++++++++++++++++++++++++++
sidebars.ts | 5 +
3 files changed, 324 insertions(+)
diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 6ce6ca608d..660fc17b12 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -2300,6 +2300,56 @@ methods on their hosts continue to instantiate via their
builders unchanged.
any `@RestOp`-annotated handler that returns an `HttpResource`, not just the
static-files
mixin.
+#### Ops / Introspection Mixin Pack (work item 77)
+
+`juneau-rest-server` now ships a three-class **ops / introspection mixin
pack** in the new
+`org.apache.juneau.rest.ops` package covering the operational surface every
long-running service
+eventually grows. Each mixin is a single-purpose `@Rest`-annotated resource
designed to be
+grafted into any servlet via `@Rest(mixins=...)` — configure via a `@Bean
MixinClass` factory
+method on the host, leave the rest unmounted.
+
+- **`BasicEchoResource`** — serves `/echo/*` and `/debug/echo/*` request-echo
/ round-trip
+ introspection. Default `404 Not Found` until the host's
+ `@Rest(debug=...)` chain unlocks it (`@Rest(debug="conditional")` paired
with a `Debug: true`
+ request header is the recommended posture). When debug is on, returns a JSON
body reflecting
+ the inbound method, path, query string, headers (with `Authorization` /
`Cookie` /
+ `Set-Cookie` / `Proxy-Authorization` / `X-API-Key` redacted to `[REDACTED]`
by default), query
+ params, attributes, and bounded body capture (1 MB cap, configurable via
+ `Builder.bodyLimit(long)`). Builder methods: `bodyLimit(long)`,
`redactedHeaders(String...)`
+ (replace), `redactHeader(String)` (additive). The handler dispatches on
+ `@RestOp(method="*")` so every HTTP verb hits the same code path.
+- **`BasicAdminResource`** — serves `/admin/threads` (JSON thread dump),
`/admin/heap`
+ (`Runtime` + `MemoryMXBean` stats), `POST /admin/cache/flush` (run
registered hooks),
+ and `/admin/ratelimit` (registered `RateLimitGuard` beans). Default-deny via
the new
+ `org.apache.juneau.rest.guard.DenyAllGuard` until the host registers a
`@Bean RestGuardList`
+ factory; the framework's bean-store override seam **replaces** the entire
annotation-derived
+ guard list (including the deny-all) with the user-supplied chain. Builder
methods:
+ `cacheFlush(String, Runnable)`, `cacheFlushAll(Map)`,
`threadNamePrefixExclude(String...)`.
+ Bucket-level rate-limit inspection (per-key counters) is reserved for a
follow-on once
+ `RateLimitGuard.Storage` exposes a snapshot SPI; v1 emits configuration only
and returns
+ `404 Not Found` when no `RateLimitGuard` bean is registered.
+- **`BasicRouteIndexResource`** — serves `/options` and `/routes` (synonyms)
returning a JSON
+ list of every `@RestOp`-annotated method on the host (and on every mixin on
the host),
+ ordered by path. Each entry has `path`, `methods` (single-element list of
the HTTP method),
+ `summary`, `description`, and `deprecated`. The route-index endpoint omits
itself and every
+ `@OpSwagger(ignore=true)` operation, mirroring the audience separation
between the OpenAPI
+ surface and the operator surface.
+
+All endpoints carry `@OpSwagger(ignore = true)` so they're excluded from any
Swagger / OpenAPI
+spec generated alongside them — ops endpoints are not API-meaningful and
shipping them in the
+spec leaks operator surface to consumers. See
+[Ops / Introspection Mixin Pack](/docs/topics/OpsIntrospectionMixins) for the
full reference,
+including per-mixin builder semantics, default-vs-override behavior, the
deny-all override seam
+for `BasicAdminResource`, sensitive-header-redaction guarantees, and
real-container parity
+tests under both Jetty microservice and Spring Boot.
+
+- **`DenyAllGuard` (new class).** Companion `RestGuard` in the existing
+ `org.apache.juneau.rest.guard` package that rejects every request with `403
Forbidden`. Used
+ as the secure-by-default placeholder on operations or resources that require
an
+ authentication / authorization chain the importer must explicitly opt into.
Reusable on any
+ `@Rest(guards=...)` or `@RestOp(guards=...)` site that wants the same
default-deny posture
+ with a `@Bean RestGuardList` override seam.
+
#### Server-side SSE Helpers (TODO-62)
`juneau-rest-server` now includes an SSE helper layer for streaming endpoints:
diff --git a/pages/topics/10.14c.OpsIntrospectionMixins.md
b/pages/topics/10.14c.OpsIntrospectionMixins.md
new file mode 100644
index 0000000000..d94132e393
--- /dev/null
+++ b/pages/topics/10.14c.OpsIntrospectionMixins.md
@@ -0,0 +1,269 @@
+---
+title: "Ops / Introspection Mixin Pack"
+slug: OpsIntrospectionMixins
+---
+
+The Juneau REST server ships with a three-class **ops / introspection mixin
pack** in
+[`org.apache.juneau.rest.ops`](/site/apidocs/org/apache/juneau/rest/ops/package-summary.html)
+covering the operational surface every long-running service eventually grows:
a request-echo
+endpoint for round-trip debugging, a JVM admin surface for thread / heap /
cache /
+rate-limit visibility, and a route-index endpoint for navigation. Each mixin
is a single-purpose
+`@Rest`-annotated resource designed to be grafted into your servlet via
+[`@Rest(mixins=...)`](/docs/topics/RestServerCompositionMixinsAndPaths) — pick
the URLs you want,
+configure them via a `@Bean` factory, and leave the rest unmounted.
+
+## The three mixins at a glance
+
+| Mixin | Default `paths` | Default behavior | Why it exists |
+|---|---|---|---|
+|
[`BasicEchoResource`](/site/apidocs/org/apache/juneau/rest/ops/BasicEchoResource.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. |
+|
[`BasicAdminResource`](/site/apidocs/org/apache/juneau/rest/ops/BasicAdminResource.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` (configured `RateLimitGuard` beans). | JVM operational
visibility on a deploye [...]
+|
[`BasicRouteIndexResource`](/site/apidocs/org/apache/juneau/rest/ops/BasicRouteIndexResource.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.). |
+
+All endpoints across the pack carry
+[`@OpSwagger(ignore =
true)`](/site/apidocs/org/apache/juneau/rest/annotation/OpSwagger.html#ignore())
+on their handlers — they show up in the runtime URL space but are excluded
from any Swagger /
+OpenAPI spec generated by the [api-docs mixin
pack](/docs/topics/ApiDocsMixins). Ops endpoints
+are not API-meaningful, and shipping them in the spec leaks operator surface
to consumers.
+
+## Composing the pack
+
+Each mixin is independent — drop in only the ones you need. The plan-A
composition mounts all
+three with builder-driven configuration plus the deny-all override seam:
+
+```java
+@Rest(
+ path = "/api",
+ mixins = {
+ BasicEchoResource.class,
+ BasicAdminResource.class,
+ BasicRouteIndexResource.class
+ },
+ debug = "conditional" // gates BasicEchoResource per-request
+)
+public class ApiResource extends RestServlet {
+
+ @RestGet("/items") public List<Item> items() { ... }
+
+ // Required: register an auth guard chain. This REPLACES the
BasicAdminResource
+ // DenyAllGuard default — the framework's bean-store override seam swaps
the entire
+ // annotation-derived guard list for the user-supplied chain.
+ @Bean(name = "guards")
+ public RestGuardList guards(BeanStore bs) {
+ return RestGuardList.create(bs)
+ .append(new MyAuthGuard())
+ .build();
+ }
+
+ // Optional: tighten echo body cap or extend the redacted-header list.
+ @Bean public BasicEchoResource echo() {
+ return BasicEchoResource.create()
+ .bodyLimit(64 * 1024L)
+ .redactHeader("X-Internal-Trace")
+ .build();
+ }
+
+ // Optional: register cache-flush hooks.
+ @Bean public BasicAdminResource admin() {
+ return BasicAdminResource.create()
+ .cacheFlush("primary", () -> primaryCache.invalidateAll())
+ .cacheFlush("session", () -> sessionStore.purge())
+ .build();
+ }
+}
+```
+
+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: `BasicEchoResource`
uses the 1 MB body cap
+and the standard redacted-header set; `BasicAdminResource` runs zero
cache-flush hooks and the
+default thread-name-prefix exclude list; `BasicRouteIndexResource` has no
configurable state.
+
+## Standalone deployment
+
+Each mixin is a fully-fledged `@Rest`-annotated resource and can also be
subclassed and mounted as
+its own top-level servlet:
+
+```java
+@Rest(paths = {"/echo/*"}, debug = "conditional")
+public class EchoResource extends BasicEchoResource { }
+```
+
+Both deployment styles (mixin into an existing servlet vs. mount as a sibling
servlet) work the
+same way under Spring Boot and under the Jetty microservice — the
+[`BasicEchoResource_JettyMicroservice_Test`](https://github.com/apache/juneau/tree/master/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_JettyMicroservice_Test.java)
+and
+[`BasicEchoResource_Springboot_Test`](https://github.com/apache/juneau/tree/master/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_Springboot_Test.java)
+parity tests cover both paths end-to-end, including the secure-by-default
sensitive-header
+redaction surviving the network stack.
+
+## Per-mixin notes
+
+### `BasicEchoResource`
+
+**Debug-gating.** The handler is gated behind the host's
+[`DebugEnablement`](/site/apidocs/org/apache/juneau/rest/debug/DebugEnablement.html)
chain. When
+`Debug` resolves to `OFF` for the current request, the endpoint returns `404
Not Found` so the
+existence of the URL isn't disclosed. The recommended posture for production is
+`@Rest(debug="conditional")` paired with a guard chain so only authorized
operators can flip the
+`Debug: true` request header. `@Rest(debug="always")` is fine for staging /
dev but should never
+hit production — every request to `/echo/*` then reflects the full payload
back to anyone who can
+reach the URL.
+
+**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,
+`Authorization`, `Cookie`, `Set-Cookie`, `Proxy-Authorization`, and
`X-API-Key`. Each redacted
+header surfaces in the echo body with the literal value `[REDACTED]` so the
caller can see the
+header was present without leaking its value. Override via
`Builder.redactedHeaders(String...)`
+(replaces) or `Builder.redactHeader(String)` (additive).
+
+**Body capture and truncation.** The handler reads up to
`Builder.bodyLimit(long)` bytes (default
+1 MB) of the inbound body and emits it as a UTF-8 string in the `content`
field of the JSON
+response. When the body exceeds the cap, the captured portion is truncated and
the `truncated`
+flag is set to `true` so callers can see the response is incomplete.
+
+**Response shape.**
+
+```json
+{
+ "method": "POST",
+ "path": "/echo/foo/bar",
+ "queryString": "x=1",
+ "pathRemainder": "foo/bar",
+ "headers": { "User-Agent": "curl", "Authorization": "[REDACTED]" },
+ "queryParams": { "x": "1" },
+ "attributes": { },
+ "contentLength": 5,
+ "content": "hello",
+ "truncated": false
+}
+```
+
+### `BasicAdminResource`
+
+**Default-deny security posture.** The mixin is annotated with
+`@Rest(guards=DenyAllGuard.class)`, so every admin path returns `403
Forbidden` until the host
+registers a `@Bean RestGuardList` factory. The framework's bean-store override
seam **replaces**
+the entire annotation-derived guard list (including this deny-all) with the
user-supplied chain —
+pair the mixin with whatever authentication / authorization story your service
uses (bearer-token
+guard, API-key guard, Spring Security adapter, etc.).
+
+> **Why deny-all rather than a placeholder role name?** A "non-existent role"
placeholder
+> (e.g. `roleGuard="ROLE_ADMIN_NONE_DEFAULT"`) is overridable but couples the
host's auth
+> strategy to a framework-internal role identifier. Replacing a
`RestGuardList` via `@Bean` keeps
+> the override surface narrow — one factory method on the host — and matches
the way every other
+> Juneau op-context bean is wired.
+
+The integration with Juneau's eventual AuthN guard pack (bearer-token guard,
API-key guard, JWT
+verification — currently slated as work item TODO-69 and not yet landed)
requires no change to
+this mixin: those guards plug into the same `@Bean RestGuardList` seam, so
dropping in the
+canonical auth chain when it ships will unlock the admin paths automatically.
+
+**Endpoints.**
+
+* `GET /admin/threads` — JSON list of currently-live threads. The default
thread filter excludes
+ framework noise (JVM internals, servlet container, Spring Boot
infrastructure); override via
+ `Builder.threadNamePrefixExclude(String...)`. Each entry: `name`, `id`,
`state`, `daemon`,
+ `priority`, `stack` (frames as strings).
+* `GET /admin/heap` — JSON map: `heap.{total,free,max,used}` from `Runtime`,
+ `nonHeap.{init,used,committed,max}` from
`MemoryMXBean.getNonHeapMemoryUsage()`, plus
+ `availableProcessors`. No heap-dump file generation in v1 (security risk;
revisit when
+ paired with an authenticated egress channel).
+* `POST /admin/cache/flush` — runs all registered cache-flush hooks, or just a
comma-separated
+ `?names=...` subset. Hooks register name-keyed via
`Builder.cacheFlush(String, Runnable)`;
+ callers that want async semantics own the threading model. Unknown names are
silently
+ ignored (404-on-unknown would leak the registered hook set).
+* `GET /admin/ratelimit` — JSON map keyed by bean name listing every registered
+
[`RateLimitGuard`](/site/apidocs/org/apache/juneau/rest/guard/RateLimitGuard.html).
Returns
+ `404 Not Found` when no `RateLimitGuard` bean is registered. Bucket-level
inspection
+ (per-key counters) is reserved for a follow-on once `RateLimitGuard.Storage`
exposes a
+ snapshot SPI; v1 emits configuration only.
+
+### `BasicRouteIndexResource`
+
+**Output.** A JSON list of every `@RestOp`-annotated method on the host (and
on every mixin on
+the host), ordered by path. Each entry has `path`, `methods` (single-element
list of the HTTP
+method), `summary`, `description`, and `deprecated`. The two URLs (`/options`
and `/routes`)
+return identical bodies — synonyms for caller convenience.
+
+**Excluded entries.**
+
+* The route-index endpoint itself (it shouldn't echo its own listing).
+* Any operation marked `@OpSwagger(ignore=true)` — consistent with how those
operations are
+ excluded from the OpenAPI spec by `BasicSwaggerProviderSession`. Convention
endpoints (favicon,
+ robots, version, etc.), static-files mixin handlers, and the sibling
ops-pack endpoints all
+ carry that annotation and are therefore omitted from the index, matching the
audience
+ separation: api-docs is for documented public API, route-index is for the
same surface but in
+ machine-readable form.
+* Lifecycle / filter beans — only methods carrying a `@RestOp`-group annotation
+ (`@RestGet` / `@RestPost` / `@RestPut` / `@RestDelete` / `@RestPatch` /
`@RestOptions` /
+ `@RestOp`) are listed.
+
+The mixin has no configurable state — `new BasicRouteIndexResource()` is the
only constructor,
+and the listing is computed off the host `RestContext` at request time.
+
+## Deployment notes
+
+### MockRest
+
+All three mixins work with `MockRestClient`. The Echo mixin's debug-gating
relies on the host's
+`@Rest(debug=...)`; pair `@Rest(debug="always")` 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 = BasicAdminResource.class)
+public static class TestHost extends RestServlet {
+ @Bean public RestGuardList guards(BeanStore bs) { return
RestGuardList.create(bs).build(); }
+}
+```
+
+### Spring Boot
+
+Spring's bean-store adapter resolves both the per-mixin configuration `@Bean`s
+(`@Bean BasicEchoResource`, `@Bean BasicAdminResource`) and the override
`@Bean RestGuardList` /
+`@Bean RateLimitGuard` through the standard
`ApplicationContext.getBean(Class)` path, so all
+override seams behave identically under Spring Boot. The Spring Boot parity
test
+([`BasicEchoResource_Springboot_Test`](https://github.com/apache/juneau/tree/master/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_Springboot_Test.java))
+boots a full embedded Tomcat context to confirm the redaction guarantee
survives the network
+stack and Spring's serialization wrapper.
+
+### Jetty microservice
+
+The bare-Jetty parity test
+([`BasicEchoResource_JettyMicroservice_Test`](https://github.com/apache/juneau/tree/master/juneau-utest/src/test/java/org/apache/juneau/rest/ops/BasicEchoResource_JettyMicroservice_Test.java))
+boots a `Microservice` on an ephemeral port via `MicroserviceTestFixture` to
assert the same
+end-to-end behavior (real HTTP, real `Content-Type` negotiation, real
`Authorization` redaction)
+without Spring in the picture.
+
+## Migration
+
+There's no pre-existing version of this pack to migrate from — TODO-77 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 =
BasicEchoResource.class` plus
+ `@Rest(debug="conditional")` and 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 =
BasicAdminResource.class`
+ plus a `@Bean RestGuardList`. The cache-flush hooks register via the
builder rather than via
+ per-handler subclassing.
+3. Replace any hand-rolled "list my endpoints" handler with
+ `mixins = BasicRouteIndexResource.class`. The output shape is intentionally
minimal; if you
+ need the full Swagger/OpenAPI surface, add the [api-docs mixin
pack](/docs/topics/ApiDocsMixins)
+ instead.
+
+## See also
+
+* [`@Rest(mixins=...)`](/docs/topics/RestServerCompositionMixinsAndPaths) —
the underlying
+ composition primitive.
+* [Mixin Sub-Contexts](/docs/topics/RestServerMixinSubContexts) — host-to-mixin
+ `RestContext` inheritance and bean-store layering.
+* [Convention-Endpoints Mixin Pack](/docs/topics/ConventionEndpointsMixins) —
sibling pack of
+ public-facing convention URLs (favicon, robots, sitemap, version,
well-known).
+* [Static-Files Mixin](/docs/topics/StaticFilesMixin) — sibling mixin pack for
arbitrary
+ classpath-served assets.
+* [API-Docs Mixin Pack](/docs/topics/ApiDocsMixins) — Swagger / OpenAPI /
Redoc mixins.
+* [Guards](/docs/topics/Guards) — the `@Rest(guards=...)` mechanism
`BasicAdminResource`
+ builds on.
diff --git a/sidebars.ts b/sidebars.ts
index bf24d9152c..5bb78b8d23 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1382,6 +1382,11 @@ const sidebars: SidebarsConfig = {
id:
'topics/10.14b.ConventionEndpointsMixins',
label: '10.14b.
Convention-Endpoints Mixin Pack',
},
+ {
+ type: 'doc',
+ id:
'topics/10.14c.OpsIntrospectionMixins',
+ label: '10.14c. Ops /
Introspection Mixin Pack',
+ },
{
type: 'doc',
id:
'topics/10.15.ClientVersioning',