Skip to main content

Logging and Debugging

The following methods provide logging of requests and responses:

RestClient.Builderlogger(Logger)logToConsole()logRequests(DetailLevel,Level,BiPredicate)

The following example shows the results of logging all requests that end with /bean.

Examples
// A simple bean we're going to round-trip.
MyBean bean = new MyBean();

bean = RestClient
.create()
.json5()
.logRequests(DetailLevel.FULL, Level.SEVERE, (req,res)->req.getUri().endsWith("/bean"))
.logToConsole()
.build()
.post("http://localhost/bean", bean)
.run()
.getContent().as(MyBean.class);

This produces the following console output:

=== HTTP Call (outgoing) ======================================================
=== REQUEST ===
POST http://localhost/bean
---request headers---
Accept: application/json5
---request entity---
Content-Type: application/json5
---request content---
{f:1}
=== RESPONSE ===
HTTP/1.1 200
---response headers---
Content-Type: application/json
---response content---
{f:1}
=== END =======================================================================",

It should be noted that if you enable request logging detail level DetailLevel.FULL, response bodies will be cached by default which may introduce a performance penalty. Additionally, the following method is also provided for enabling debug mode:

RestClient.Builderdebug()

Enabling debug mode has the following effects:

org.apache.juneau.rest.client.classic.RestClient.Builderlogger(Logger)logToConsole()logRequests(DetailLevel,Level,BiPredicate)debug()

Next-generation REST client (NG)​

The next-generation client (org.apache.juneau.rest.client.RestClient) uses JUL-level-driven debug logging through RichLogger.

org.apache.juneau.rest.client.RestClient.BuilderdebugLoggerName(String)debugFormatter(RestClientDebugFormatter)RestClientDebugFormatterBasicRestClientDebugFormatter

Default logger name: RestClient.class.getName().

Debug detail is controlled solely by the resolved JUL logger level:

  • INFO: basic summary line.
  • FINE: basic + request/response headers.
  • FINEST: basic + headers + bounded request/response body capture — only if body dumping has been explicitly opted in via the JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES environment variable (see Secure by default below); otherwise FINEST emits a suppression placeholder instead of body content.

Exactly one LogRecord is emitted per request/error, always at JUL INFO — the resolved tier controls message detail only, so FINE/FINEST-selected detail still travels through INFO sinks. This applies uniformly to both places the pipeline emits from: the normal request-completion path and the transport-exception (synthetic error response) path.

Secure by default​

BasicRestClientDebugFormatter mirrors the NG REST server's BasicRestDebugFormatter secret-hardening design (see REST server Logging/Debugging » Secure by default): raising a client's debug logger to FINE/FINEST can never, by itself, turn the log stream into a plaintext-credential dump or a log-forging channel.

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 the same environment-variable master gate the server uses:

export JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES=true

Semantics are byte-identical to the server's gate: environment variable only (no system-property fallback), read once per JVM and cached, trim-then-parse truthy (any non-empty trimmed value enables except case-insensitive false/0). With the gate unset, formatBody emits a suppression placeholder naming the environment variable plus the captured byte count and content-type — never body bytes in any representation (plaintext, hex, or otherwise), even if a scrubber is configured. With the gate set, bodies render only when the content is renderable (see below); non-renderable/binary content, or a non-identity Content-Encoding (e.g. gzip), yields a distinct "not rendered" placeholder instead.

danger

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

isBodyRenderable(String contentType) (on the RestClientDebugFormatter SPI) decides whether a body is text worth logging, using the same predicate as the server's RestDebugFormatter.isBodyRenderable: renderable for text/*, application/json, application/xml, any +json/+xml suffix type, and application/x-www-form-urlencoded; non-renderable for everything else, including multipart/form-data, absent, or blank content types. The decision is made independently per side — the request's own Content-Type governs the request body, the response's own governs the response body.

Body scrubbing SPI​

When body dumping is opted in, transform body text before it's logged via the RestDebugBodyScrubber SPI, registered with bodyScrubber(...):

new BasicRestClientDebugFormatter().bodyScrubber((contentType, body) -> body.replaceAll("\"password\":\"[^\"]*\"", "\"password\":\"***\""));
  • Gate-subordinate: the scrubber runs only after the master gate has already permitted dumping; it merely chooses scrubbed-vs-raw text and never causes a body to be emitted while the gate is unset.
  • 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.

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

Header redaction — widened set, separator-normalized​

At the headers tier (FINE-or-finer), header values are masked with [REDACTED] 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; this widened set is client-formatter-local, matching the server's own decision to keep its widened set formatter-local rather than merged into the shared default. Configure the masked set with redactedHeaders(Collection<String>) (replaces the masked set).

Log-forging sanitization​

The request method and URI, header names and values, and body text (raw or scrubber-produced) are passed through a sanitizer that escapes CR/LF and other control characters to visible, inert forms (\r, \n, \uXXXX). A remote server therefore cannot inject a newline into a response header/body to forge a second physical log line. The order applied to every emitted string is mask → escape → cap.

Bounded body output​

Captured body bytes are bounded at capture time by bodyCap(int) (default 8 KB), then …[truncated N bytes] marks what was omitted; a second character-level cap (bodyCap() * 6) is applied after sanitization so a scrubber that returns an oversized string is still bounded. Unlike the server's formatter, the client does not add separate header-count/header-scan/field-length/URI-length bounding knobs — the client's header source is already a finite, bounded list rather than an adversarial unbounded input stream, so those extra knobs have no client-side analog.

Request correlation (X-Request-Id)​

RestClient automatically participates in end-to-end request correlation. By default, every outgoing request carries an X-Request-Id header so a call can be traced through the server logs (which honor that inbound id — see REST server » Always-on request correlation):

  • Auto-send / mint. If the request has no X-Request-Id header, the client mints a fresh time-ordered Uuid7 and sends it. A header the caller already set (globally on the builder or per-request) is honored, never overwritten.
  • Pre-call setter. RestRequest.requestId(String) sets the id for a single call fluently; getRequestId() reads the id that was (or will be) sent.
  • Echo capture. The client captures the X-Request-Id the server echoes on the response, exposed via RestResponse.getRequestId(). When correlating logs, the client uses the echoed id when present and falls back to the sent id otherwise (the effective id).
  • Opt-out. RestClient.Builder.sendRequestId(false) disables auto-sending entirely; an explicitly caller-supplied X-Request-Id header still travels.

The effective id flows into debug logging: RestClientDebugPipeline stamps each emitted LogRecord with a structured requestId field (via LogRecordContext.attachIfAbsent(...)), and BasicRestClientDebugFormatter prepends a human-readable [requestId=<id>] marker to the rendered message — mirroring the server's statusLine prefix so a single request reads identically on both sides.

try (var client = RestClient.builder().build();
var res = client.get("http://localhost/widgets/123").run()) {
// Server-honored, echoed back:
var id = res.getRequestId();
}

// Explicit per-call id (honored, not overwritten):
client.get("/widgets/123").requestId("order-9f2c").run();

// Opt out of auto-sending entirely:
var quiet = RestClient.builder().sendRequestId(false).build();

For tests, capture emitted records directly via RichLogger.captureEvents(...):

var logger = RichLogger.getLogger("my.client.debug");
logger.setLevel(Level.FINEST);

try (var capture = logger.captureEvents(Level.FINEST);
var client = RestClient.builder()
.debugLoggerName("my.client.debug")
.debugFormatter(new BasicRestClientDebugFormatter().bodyCap(8 * 1024))
.build();
var res = client.post("http://localhost/bean").bodyString("{\"x\":1}").run()) {
res.body().asString();
var record = capture.last();
// assert on record.getLevel() and record.getMessage()
}