Back to Articles

Spring Boot · Logging

Production Logging: 7 Things I Never Log

2026-09-247 min read
LoggingSecurity

Logs are the one place almost every engineer on a team, plus whoever gets paged at 3am, will look first when something breaks. That also makes them the easiest place to accidentally create a compliance incident, because nothing about a log.info() call stops you from logging exactly the data you are not supposed to store.

The problem

Logging aggregators retain data for months, get shipped to third-party tools, and get read by more engineers than your database does. A password or card number that would never make it into a database column unencrypted gets logged in plain text without a second thought, because it "was just for debugging."

The naive approach

AuthService.java (bad)
1log.info("Login attempt: email={}, password={}", email, password);
2log.info("Charging card {} for order {}", cardNumber, orderId);
3log.debug("Full request body: {}", request.getBody());

Three separate problems in three lines: a password in plaintext, a full card number, and an entire request body logged without knowing what fields it contains this week or next week.

Seven things that never reach a log line

  • Passwords and secrets, even hashed ones, even in debug builds. A hash logged today is a hash an attacker can spend time on later.
  • Full card numbers and CVVs. Log the last four digits and the card network if you need to reference a transaction, never the full PAN.
  • Auth tokens and session IDs. A leaked JWT or session cookie in a log is a valid credential until it expires.
  • Government IDs and other direct PII (SSNs, passport numbers). Log a masked or hashed reference instead.
  • Entire request or response bodies on authentication and payment endpoints, since you cannot audit every field that gets added to that body in the future.
  • Raw SQL with bind values. The query shape is useful for debugging; the actual balances and account numbers in it are not.
  • Stack traces sent to the client. This is the inverse case: stack traces belong in the log, tagged with a trace ID, and never in the HTTP response body (see the first article in this series).

The production approach

Log identifiers and outcomes, not the sensitive values themselves:

AuthService.java (production)
1log.info("Authentication failed. userId={}, reason={}", userId, reason);
2log.info("Payment charged. orderId={}, cardLast4={}, amount={}",
3 orderId, card.last4(), amount);
4log.debug("Request received. method={}, path={}, contentLength={}",
5 method, path, contentLength);

Every line here answers "what happened and to whom" without reproducing anything an attacker or auditor would flag. The same principle applies to SQL logging when it is turned on for debugging:

Repository logging
1// Bad: bind values in the log
2log.debug("UPDATE accounts SET balance = 4200 WHERE id = 91");
3
4// Good: parameterized query logged without values
5log.debug("UPDATE accounts SET balance = ? WHERE id = ?");

Making it reusable

Rather than trusting every developer to remember this list on every log call, a Logback TurboFilter or a Logstash PatternLayout converter can regex-mask common shapes (16-digit numbers, JWT-looking strings, Bearer headers) as a last line of defense. It should not be the only line of defense, since a filter cannot know your domain-specific PII fields, but it catches the mistakes.

Testing it

A log-appender test (attach an in-memory ListAppender to the logger under test, trigger the code path, assert on the captured events) can assert two things: that a message was logged at the expected level, and that none of the captured messages contain the raw secret value passed into the method. That second assertion is the one worth writing once and reusing across every service that handles credentials.

What I learned

The rule of thumb that has never failed me: if you would not want a specific log line printed on a projector during a company all-hands, it does not belong in the log at that level of detail. Log the decision and the identifiers. Log the reason something failed. Never log the thing that made it sensitive in the first place.