Skip to content

Add logging through slf4j (#6) - #7

Merged
garretpremo merged 7 commits into
Premo-Cloud:mainfrom
xbt-a4224j:feat/logging
Sep 22, 2026
Merged

garretpremo merged 7 commits into
Premo-Cloud:mainfrom
xbt-a4224j:feat/logging

Conversation

@xbt-a4224j

@xbt-a4224j xbt-a4224j commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Motivation:
Make requests, statuses and retries visible at a log level the app chooses, following the Python reference: same log points as transport.py:59-94, same redaction as SensitiveHeadersFilter, each log point one level below Python's so a stock Spring Boot app (root INFO) stays silent as a stock Python app (root WARNING) does.

Changes:

  • Logging: slf4j logger io.github.premocloud.typesafe; wire() is the one redaction point, "***" for SECRET_HEADERS plus token/secret names
  • TypeSafeClient: DEBUG line per request (status, elapsed, request id), per retry, per connection failure; TRACE for headers and bodies both ways
  • build.gradle.kts: api(slf4j-api:2.0.16); logback-classic test-only
  • LoggingTest: 5 tests on a captured appender, incl. key never logged and nothing emitted at INFO
  • README Logging section, CHANGELOG entry

Result:
Stock Spring Boot (root INFO) sees no output. logging.level.io.github.premocloud.typesafe=DEBUG gives one line per request; TRACE adds the wire with Authorization=***. 44/44 tests pass.

Closes: #6

INFO is one line per request with its status and how long it took, plus a
line for each retry and each connection failure. DEBUG adds the wire in both
directions: method, url, headers and body. Nothing at WARN or above, because
a failure is already thrown and the exception carries more than a log line
would. The tiering follows the official Python and JavaScript SDKs.

There is no TYPESAFE_LOG_LEVEL. slf4j has no library-side level setting, and
configuring the logging environment is the application's job; the level is set
on the io.github.premocloud.typesafe logger like any other library's.

Redaction happens in one method, so no call site can leak a credential. The
named header set is the union of what the two official SDKs cover, plus a
substring check for token and secret, since Builder.header lets a caller add
one this SDK never anticipated. The wire calls are guarded by isDebugEnabled
so the redacted string is not built when the level is off.

Adds five tests covering the level tiering, the wire in both directions, the
retry line, silence above INFO, and that the key never appears in any message.
The capturing appender asserts on what was logged, but additivity also sent
every event to logback's default console appender, so each run printed the
request and response bodies.
Java's default root level is INFO and Spring Boot configures a console
appender at it, so a per-request line at INFO is on in every application that
never asked for one: a hundred requests, a hundred lines, no configuration.
The reference SDKs put the summary at INFO, but a bare Python process has no
handler on root and JS defaults its own level to warn, so INFO is off unless
an application opts in. Sitting a tier lower reproduces that behaviour here.

The wire moves to TRACE with it, which also separates 'show me the requests'
from 'show me every body'.
…line

Drop Logging.request: call sites use LOG.debug so the level is visible where
it is logged. Logging keeps wire() as the only redaction point. Fix the class
doc, which still described the INFO/DEBUG tiers.

return attemptAsync(template, type, timeout, retryPolicy, 0);
return attemptAsync(template, type, timeout, retryPolicy, 0,
new Call("req-" + REQUESTS.incrementAndGet(), requestBody));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@garretpremo Should we keep or remove the REQUESTS counter? It's in this PR to help distinguish logs of interleaved async calls, but maybe not worth.

@garretpremo garretpremo Sep 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xbt-a4224j Adding an identifier to requests is super useful! Especially after adding async requests which could add interleaved logs that are indistinguishable from each other.

Suggested change: Generate the identifier as a short random hex id in the logging class rather than a JVM-global counter. This avoids collisions across restarts and replicas at a small cost to readability (which is worth it IMO for a debug line).


// Logging.java

/**
 * A short opaque tag for one logical call, shared by every attempt it makes, so the lines of
 * interleaved async calls can be told apart. Random rather than counted: no shared state, and no
 * collisions across restarts or replicas in an aggregated log.
 */
static String tag() {
    return String.format("req-%06x", ThreadLocalRandom.current().nextInt(1 << 24));
}

Call lines:

Suggested change
new Call("req-" + REQUESTS.incrementAndGet(), requestBody));
new Call(Logging.tag(), requestBody));

@garretpremo garretpremo self-assigned this Sep 22, 2026
# Conflicts:
#	CHANGELOG.md
…bal counter

The tag is what ties a call's send line, retries, failures and response
together and tells interleaved async calls apart. A static counter did
that, but it was shared across every client in the JVM and restarted at
zero on each start, so tags collided across restarts and replicas in an
aggregated log. Six random hex digits from ThreadLocalRandom, generated in
Logging, carry the same information with no shared state.

Also merges main to pick up the 402/413 exception mapping (Premo-Cloud#8) alongside
this entry in the changelog.
@garretpremo

Copy link
Copy Markdown
Contributor

Pushed the random-id tag from the thread above to this branch (maintainer edit) along with a merge of main for the changelog, so this can go in with #9. Thanks for the PR!

@garretpremo
garretpremo merged commit d2e4c05 into Premo-Cloud:main Sep 22, 2026
1 check passed
garretpremo added a commit that referenced this pull request Sep 22, 2026
Requests, retries and connection failures log at DEBUG on the
io.github.premocloud.typesafe logger, with the wire at TRACE and
credential headers masked; nothing at INFO or above. Each call's lines
share a short random tag so interleaved async calls stay readable. Adds
org.slf4j:slf4j-api as an api dependency (#6, #7).

HTTP 402 and 413 now raise TypeSafePaymentRequiredException and
TypeSafePayloadTooLargeException instead of the generic
TypeSafeApiException (#8, #9). READMEs point at 0.4.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add logging

2 participants