Skip to main content

Logging / Debugging

REST request/response debug capture is driven by a single signal: the resource's own java.util.logging.Logger level. There's nothing Juneau-specific to configure — the same logging.properties file (or Logger.setLevel(...) call) an operator already uses to turn up verbose logging for a class also turns on REST header/body capture for that class's endpoints, at three cumulative tiers.

Overview​

  • One signal. No debug annotations, no on-demand per-request headers, no separate logging SPI to wire up. The resolved JUL logger's level is the sole control.
  • Cumulative tiers. INFO = basic request/response logging, FINE = adds headers, FINEST = adds bodies — but only when body dumping has been explicitly opted in via the JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES environment variable; otherwise FINEST emits a suppression placeholder instead of body content. Each tier includes everything the tier below it logs.
  • One log record per request, always emitted at JUL INFO. The resolved tier controls content only.
  • Secure by default. Bodies are never dumped unless an operator sets an environment-variable master gate; every value of every credential-bearing header is masked; every client-controlled string is escaped against log-forging; and all output is bounded — see Secure by default below.

Level resolution​

Two JUL loggers are in play for any given request:

  • Resource logger — the resource's resolved logger: Logger.getLogger(resourceClass.getName()) by default, or whatever a @Bean-annotated RichLogger factory method overrides it to (see Bean-overridden resource loggers below).
  • Operation logger — Logger.getLogger(resourceLoggerName + "." + methodName), once the request has been routed to a specific @RestOp method, where resourceLoggerName is the resolved resource logger's name from above (i.e. the operation logger is always a JUL child of the resolved resource logger, override or not).

Juneau resolves the operation logger first, falling back to the resource logger on unrouted/404 paths. Because this is a plain java.util.logging.Logger hierarchy, ordinary JUL parent-inheritance rules apply — an operation logger with no level of its own inherits the resource logger's level.

Default resource logger naming normalizes common proxy-shaped classes (Spring CGLIB, ByteBuddy, JDK proxy-style, Hibernate/Mockito proxy infixes) back to the user class name, while preserving ordinary nested class names unchanged. This avoids logger-name churn when resources are wrapped by runtime proxies.

// logging.properties
com.example.MyResource.level = FINE
com.example.MyResource.getWidget.level = FINEST

With the configuration above:

  • Every operation on MyResource logs at least basic + headers (inherited FINE from the resource logger).
  • getWidget() specifically also logs bodies (FINEST set directly on its own operation-level child logger).

This is exactly the "set the resource logger to FINE, elevate one method to FINEST" pattern — per-operation control falls out of ordinary JUL inheritance, no Juneau-specific per-method attribute required.

Mixin operations resolve against the host​

An operation contributed by a composed @Rest(mixins=...) mixin uses the host / top-level resource that composed the mixin for its resource-logger portion — not the mixin class itself:

public class EchoMixin { @RestGet(path="/echo/*") public ... echo(...) {...} }

@Rest(mixins=EchoMixin.class)
public class MyResource extends RestServlet {...}

Here the mixin-served echo operation's logger is named com.example.MyResource.echo, not com.example.EchoMixin.echo. Consequences:

  • Raising com.example.MyResource's own JUL level cascades to echo (and every other mixin-served operation) — ordinary JUL parent inheritance, same as any other operation on the host.
  • The same mixin class composed into two different host resources resolves to two independent, host-isolated operation loggers (HostA.echo and HostB.echo) — raising one host's level never leaks into the other.

Bean-overridden resource loggers​

If a resource replaces its resolved logger via a @Bean-annotated RichLogger factory method:

public class MyResource extends RestServlet {
@Bean
public RichLogger logger() {
return RichLogger.getLogger("my.app.custom.logger.name");
}
}

every operation logger on that resource is named as a child of the overridden name (my.app.custom.logger.name.<methodName>), not the raw resource class name — so raising the overridden logger's level still cascades to its operations exactly as it would for the default, class-named logger.

404 / unrouted requests​

When no operation is resolved (the request 404s, or otherwise never reaches a matched @RestOp method), Juneau falls back to the resource logger, but only ever emits the basic status-line summary — headers and bodies are never rendered on this path, no matter how high the resolved logger's level is set. Raising a resource to FINEST makes its operations log full detail; it does not retroactively add header/body detail to 404s.

Tier table​

LevelContent
below INFO, or INFO inherited from the root loggerNothing emitted. Stock JUL leaves the root at INFO, so a level must be set explicitly on the resource's logger (or an enclosing logger) to turn the line on.
INFO (set explicitly)Basic request/response summary (method, path, status, timing).
FINEBasic + request/response headers (every value of every credential-bearing header masked).
FINESTBasic + headers + request/response body content — only if JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES is set (and the content is renderable); otherwise a suppression placeholder + byte count.

FINER resolves through the FINE tier (headers), while CONFIG resolves through INFO (basic line only), matching JUL isLoggable(...) semantics used by the pipeline.

The tier is re-derived at request completion via isLoggable(...) against the resolved logger, and is stable for the life of the request. Content is cumulative — FINEST always includes everything FINE and INFO log, in one LogRecord.

Two-phase capture (internal)​

Internal implementation detail — not a public SPI

The two-phase pipeline below is described for context only. It is not an extension point; the only supported customization surface is the RestDebugFormatter SPI.

  • Phase A (request start): the operation logger is resolved as soon as routing completes. If it's loggable at FINEST, bounded body-caching wrappers are installed around the request/response streams; otherwise no wrapping happens at all, so requests below FINEST pay no buffering overhead.
  • Phase B (request completion): the tier is re-derived, the resolved RestDebugFormatter is invoked cumulatively (formatBasic always, + formatHeaders at FINE-or-finer, + formatBody at FINEST), header redaction and log-forging sanitization are applied, and exactly one LogRecord is emitted at INFO through the resolved logger. formatBody only ever emits actual body bytes when the JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES gate is set (see Secure by default); otherwise it emits a placeholder.
  • Bounded capture. The caching wrapper buffers at most the configured body cap (default 8 KB), not the entire stream, so memory use is bounded even for large uploads/downloads.
  • Edge case: if the logger's level is changed during a request (between Phase A and Phase B), the two phases 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​

RestDebugFormatter is the one public extension point for how a captured request/response is rendered into a log message:

package org.apache.juneau.rest.server.logging;

public interface RestDebugFormatter {
String formatBasic(RestRequest req, RestResponse res); // INFO tier
default String formatHeaders(RestRequest req, RestResponse res) {...} // added at FINE
default String formatBody(RestRequest req, RestResponse res) {...} // added at FINEST (gated)
default String statusLine(HttpServletRequest req, HttpServletResponse res) {...} // sanitized + URI-capped
default boolean isBodyRenderable(String contentType) {...} // text-vs-binary predicate
}

Override only the tier(s) you care about — the other tiers keep the shipped defaults.

Default implementation​

BasicRestDebugFormatter ships working defaults for all three tiers and owns the header-redaction set, the body-dump gate, the RestDebugBodyScrubber hook, and the output bounds (see Secure by default below). It's the zero-configuration default — you don't need to register anything to get useful, secure debug output.

Resolution​

A resource's formatter resolves in the following precedence order:

  1. Resource class implements RestDebugFormatter — the resource itself is used directly.
  2. BeanStore-registered bean — a @Bean-annotated method/field (or, in Spring Boot, a Spring @Bean) supplying a RestDebugFormatter.
  3. BasicRestDebugFormatter — the shipped default.
Example
// Option 1 - resource implements the SPI directly
public class MyResource extends BasicRestServlet implements RestDebugFormatter {
@Override
public String formatBody(RestRequest req, RestResponse res) {
return "custom body rendering...";
}
}

// Option 2 - supply a formatter bean
public class MyResource extends BasicRestServlet {
@Bean
public RestDebugFormatter debugFormatter() {
return new MyCustomFormatter();
}
}

Secure by default​

BasicRestDebugFormatter is designed so that raising a resource/operation logger to FINE/FINEST can never, by itself, turn the log stream into a plaintext-credential dump or a log-forging channel controlled by a remote client. Four independent protections apply.

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 a single environment-variable master gate:

export JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES=true

Semantics:

  • Environment variable only. There is no system-property fallback. It is read once per JVM and cached.
  • Trim-then-parse truthy. The value is trimmed, then any non-empty value enables except case-insensitive false or 0, which disable. Unset, empty, or all-whitespace disables. This is a fail-safe kill switch: " false " and " " both resolve to disabled and never silently enable dumping.
  • Gate off ⇒ placeholder only. With the gate unset, formatBody emits only a suppression placeholder plus the captured byte count and content-type — never body bytes in any representation — even if a scrubber is configured.
  • Gate on ⇒ only at FINEST, only on renderable content. Even opted in, bodies render only at the FINEST tier and only when the content is renderable text (see below). Non-renderable/binary content yields a distinct "not rendered" placeholder.
danger

Setting JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES re-enables plaintext request/response body logging at FINEST. Treat FINEST logs as sensitive: restrict their sink, and supply a RestDebugBodyScrubber if any secret-bearing bodies (tokens, PII, card data) are possible. Prefer leaving the gate unset in production.

Renderable-content predicate​

isBodyRenderable(String contentType) decides whether a body is text worth logging. It parses the media type (ignoring parameters such as ; charset=utf-8) and treats as renderable: text/*, application/json, application/xml, any +json/+xml suffix type, and application/x-www-form-urlencoded. Everything else — including multipart/form-data, absent, or blank content types — is non-renderable. A non-identity Content-Encoding (e.g. gzip) is also treated as non-renderable. The decision is made per side: the request content type governs the request body, the response content type governs the response body.

Body scrubbing SPI​

When body dumping is opted in, you can transform body text before it is logged via the RestDebugBodyScrubber SPI (String scrub(String contentType, String body)), registered with bodyScrubber(...):

  • It is gate-subordinate: a scrubber is invoked only after the master gate has already permitted dumping. It merely chooses scrubbed-vs-raw text; it never causes a body to be emitted while the gate is unset.
  • It is 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.
  • Implementations must be thread-safe — the formatter may call scrub(...) concurrently.

No default scrubber ships: with the gate set and no scrubber, the raw body is dumped (sanitized + capped).

Header redaction — all values, widened set​

At the headers tier (FINE-or-finer), every value of every credential-bearing header is masked with [REDACTED] (multi-valued headers such as repeated Set-Cookie are masked value-by-value, not just the first). The formatter matches 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.

Configure the masked set with:

  • redactedHeaders(Collection<String>) — replaces the masked set.
  • addRedactedHeaders(Collection<String>) — adds to the built-in widened set without dropping the built-ins.

Log-forging sanitization​

Every client-controlled string — the request method and URI (including on the 404 / unrouted path), header names and values, and body text (raw or scrubber-produced) — is passed through a sanitizer that escapes CR/LF and other control characters (NEL, line/paragraph separators, bidi controls, C0/C1 controls; TAB is preserved) to visible, inert forms (\r, \n, \uXXXX). A remote client therefore cannot inject a newline to forge a second physical log line or a fake status banner. The order applied to every emitted string is mask → escape → cap.

Bounded output​

All output axes are bounded so a flood cannot exhaust the log sink:

BoundKnobDefault
Captured body bytes (Phase A capture time), then …[truncated N bytes]bodyCap(int)8 KB
Emitted header values per block, then …[+K more headers omitted]maxHeaders(int)100
Header values scanned while counting omissionsmaxHeaderScan(int)1000
Per header name/value length (after masking/escaping)maxFieldLength(int)1024
Rendered URI lengthmaxUriLength(int)2048

The body byte cap is enforced at capture time (Phase A above), so oversized bodies never sit fully buffered in memory; a second character-level cap is applied after sanitization so a scrubber that returns a huge string is still bounded. There is no separate body-dump cap knob — bodyCap() is the single source of truth.

See the migration guide's footgun callout for operational guidance on raising logger levels in production.

Using Log4j for logging​

The REST API uses Java logging by default.

If you wish to use LOG4J logging, you simple need to add the following to your JVM arguments and maven dependencies:

Command-line argument​

-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager

Maven dependency​

log4j-jul is only the bridge that routes java.util.logging calls into Log4j2 — it does not include a Log4j2 implementation. You also need log4j-core (or log4j-to-slf4j plus an SLF4J binding) on the classpath, or nothing will actually process the routed log records:

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jul</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
See Also

Management Logging — Logback & Log4j2 Backends covers the more thorough juneau-rest-server-management-logging module, which lets the /loggers runtime management endpoint drive Logback or Log4j2 directly instead of just redirecting JUL output.

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 RestRequest. 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:

@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 LogRecords.
  • req.getLogger() is exposed directly if you need the RichLogger itself.

Correlating records with a request-scoped LogContext​

While a thread-confined 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):

@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.

Always-on request correlation (requestId)​

The framework opens exactly one requestId 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, 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.

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. 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:

@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() {...}

@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 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:

@Bean public EchoMixin echo() {
return EchoMixin.create().enabled().build(); // or .enabled(true)
}

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 for the full behavior, including the sensitive-header redaction that applies to the echoed-back request as well.

Testing​

Capture debug output by logger name with RichLogger:

try (var c = RichLogger.getLogger(MyResource.class).captureEvents(Level.FINEST)) {
client.get("/who").run();
assertTrue(c.last().getMessage().contains("[200] HTTP GET /api/who"));
}

captureEvents(Level.FINEST) is the right threshold for .debug()/logLevel(FINEST) mock flows, since debug records are emitted at INFO, and the capture threshold is >=-based. captureEvents(Level.INFO) is the precise threshold; captureEvents(Level.FINEST) still captures these records for existing tests. Pair this with the mock request's logLevel(Level) helper to set the resolved logger level for one call (and restore it afterward) — the mock equivalent of editing logging.properties without introducing a separate boolean debug flag.

Keeping expected-error stack traces out of the log​

When a request fails, the per-request record carries the exception behind the failure as its thrown, so a test that provokes a 404, 405, 415 or other framework-level error on purpose prints a stack trace for an outcome it expects. Send No-Trace: true, or set the NoTrace request attribute (RestRequest.setNoTrace() on the server), to leave the stack trace off that record. The one-line summary (status, method, path) is still logged.

  • If both the header and the attribute are present, the attribute wins (so NoTrace=false beats No-Trace: true).
  • The setting is opt-in per request; nothing turns it on by default.
  • It affects this per-request record only. Errors your own code logs, and the FINE exception log written by RestContext.handleError, are not changed.

In tests, use the mock client's noTrace(); see NG Mock REST Client.

Spring/back-end notes​

  • Spring Boot + Logback can propagate logging.level.* into JUL through LevelChangePropagator when enabled.
  • The propagator's inherit-then-parent-change synchronization hole still applies: a JUL child logger set to inherit can remain stale after a later parent-level change.
  • For nested resource class names that contain $, logging.level.<fqcn> property keys may need escaping in Spring property sources because $... can be interpreted as a placeholder.
  • Backend fallback matrix:
    • Logback: auto-propagation support.
    • Log4j2 + log4j-jul: JUL facade path works.
    • Log4j2 without log4j-jul: no JUL back-propagation.
    • Plain JUL / other SLF4J backends: configure JUL directly.
danger

Stable-INFO emission means FINE/FINEST-selected detail travels through INFO sinks. Headers are always masked and bodies are suppressed by default (see Secure by default), but a broad FINEST raise on a production resource logger still emits full header names and metadata through INFO — and, if JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES is set, plaintext bodies. Raise levels deliberately and keep the body-dump gate unset in production.

Migrating from pre-10.0​

If you're coming from a pre-10.0 REST debug/logging setup — @Rest(debug=...), @Debug, DebugEnablement, CallLogger, or the juneau.restLogger.* config keys — none of that exists anymore. See the V10 Migration Guide's REST debug/logging redesign section for the full removed-symbol mapping and a prominent warning about one operational footgun worth reading before you flip a resource's logger to FINE/FINEST.

Separately from request/response debug capture, thrown exceptions are logged with a stack-trace hash and an occurrence counter:

WARNING: [500,9b85cc96.13] HTTP POST /foo?foo=bar

This is controlled by the ThrownStore bean, an orthogonal mechanism unaffected by the logging model described above. It's resolved the same way as other framework beans — via a @Bean(name="thrownStore")-annotated method/field, or (in Spring Boot) a Spring @Bean.

One case worth knowing: a BeanQueryExecutionException (an engine-side failure in a Queryable or BeanQuery-backed operation, as opposed to a client-caused BeanQuerySyntaxException) is mapped to HTTP 500 with the fixed body Query execution failed.. Its cause, which may name SQL or tables, stays attached to the exception, so the call logger and ThrownStore record it while the client never sees it.