Ops / Introspection Mixin Pack
The Juneau REST server ships with a three-class ops / introspection mixin pack in
org.apache.juneau.rest.server.ops
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=...) — 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 |
|---|---|---|---|
EchoMixin | /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 — invaluable for diagnosing proxy / mTLS / auth-header issues without spinning up a packet capture. |
AdminMixin | /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 operational visibility on a deployed service — the bare minimum to triage a stuck request, OOM, or stale cache without redeploying. |
RouteIndexMixin | /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)
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. 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:
@Rest(
path = "/api",
mixins = {
EchoMixin.class,
AdminMixin.class,
RouteIndexMixin.class
}
)
public class ApiResource extends RestServlet {
@RestGet("/items") public List<Item> items() { ... }
// Required: register an auth guard chain. This REPLACES the AdminMixin
// 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();
}
// 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();
}
// Optional: register cache-flush hooks.
@Bean public AdminMixin admin() {
return AdminMixin.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 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 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
Each mixin is a fully-fledged @Rest-annotated resource and can also be subclassed and mounted as
its own top-level servlet:
@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
same way under Spring Boot and under the Jetty microservice — the
EchoMixin_JettyMicroservice_Test
and
EchoMixin_Springboot_Test
parity tests cover both paths end-to-end, including the secure-by-default sensitive-header
redaction surviving the network stack.
Per-mixin notes
EchoMixin
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:
@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 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,
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.
{
"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
}
AdminMixin
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 aRestGuardListvia@Beankeeps 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 AuthN guard pack (bearer-token
guard, API-key guard, JWT verification) requires no change to this mixin: those guards plug into
the same @Bean RestGuardList seam, so dropping in the canonical auth chain unlocks 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 viaBuilder.threadNamePrefixExclude(String...). Each entry:name,id,state,daemon,priority,stack(frames as strings). -
GET /admin/heap— JSON map:heap.{total,free,max,used}fromRuntime,nonHeap.{init,used,committed,max}fromMemoryMXBean.getNonHeapMemoryUsage(), plusavailableProcessors. 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 viaBuilder.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 registeredRateLimitGuard. Each entry has two sub-fields:config(the guard's static configuration:class,limit,permitsPerSecond,xForwardedForAware,exemptPaths) andsnapshot(a sorted-by-key array ofBucketStateentries describing every per-key bucket the storage backend currently tracks). EachBucketStatecarrieskey,tokens(fractional fill level),remaining(integer tokens available — mirrors theX-RateLimit-Remainingadvisory header),throttled(convenience flag fortokens < 1.0), andlastRequest(wall-clockInstantof last activity). Returns404 Not Foundwhen noRateLimitGuardbean is registered. Storage backends that don't overrideStorage.snapshot()(e.g. Redis-backed impls that can't cheaply enumerate buckets) emit"snapshot": []for their bean entry — the SPI method isdefaultreturningMap.of()so externalStorageimplementations stay backwards-compatible. See Rate-Limiting and Request-Id Propagation § Inspecting bucket state at runtime for the full snapshot SPI contract.Example response shape:
{
"guards": {
"rateLimit": {
"config": {
"class": "org.apache.juneau.rest.server.guard.RateLimitGuard",
"limit": 100,
"permitsPerSecond": 1.67,
"xForwardedForAware": false,
"exemptPaths": ["/healthz", "/livez", "/readyz"]
},
"snapshot": [
{"key": "203.0.113.42", "tokens": 23.0, "remaining": 23, "throttled": false, "lastRequest": "2026-05-26T14:00:42Z"},
{"key": "203.0.113.99", "tokens": 0.5, "remaining": 0, "throttled": true, "lastRequest": "2026-05-26T14:00:58Z"}
]
}
}
}
RouteIndexMixin
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 byBasicSwaggerProviderSession. 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 RouteIndexMixin() 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. 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:
@Rest(mixins = AdminMixin.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 @Beans
(@Bean EchoMixin, @Bean AdminMixin) 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
(EchoMixin_Springboot_Test)
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
(EchoMixin_JettyMicroservice_Test)
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 — this is the first cut. If your service hand-rolled an echo / admin / routes endpoint before adopting the pack:
- Replace the hand-rolled
/echohandler withmixins = EchoMixin.class, enable it explicitly via a@Bean EchoMixinthat calls.enabled()(it is404by 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 reflectedAuthorizationandCookieheaders verbatim. - Replace any hand-rolled JVM-introspection endpoints with
mixins = AdminMixin.classplus a@Bean RestGuardList. The cache-flush hooks register via the builder rather than via per-handler subclassing. - Replace any hand-rolled "list my endpoints" handler with
mixins = RouteIndexMixin.class. The output shape is intentionally minimal; if you need the full Swagger/OpenAPI surface, add the api-docs mixin pack instead.
See also
@Rest(mixins=...)— the underlying composition primitive.- Mixin Sub-Contexts — host-to-mixin
RestContextinheritance and bean-store layering. - Convention-Endpoints Mixin Pack — sibling pack of public-facing convention URLs (favicon, robots, sitemap, version, well-known).
- Static-Files Mixin — sibling mixin pack for arbitrary classpath-served assets.
- API-Docs Mixin Pack — Swagger / OpenAPI / Redoc mixins.
- Guards — the
@Rest(guards=...)mechanismAdminMixinbuilds on.