Developer reviewing structured application logs with sensitive values hidden

Editorial illustration; it does not show a real application or customer data.

Published on October 21, 2026.

When an application breaks, the first question is often “What happened?” The answer is usually somewhere in a log. A timestamp, request identifier, route and error message can turn a vague complaint into a reproducible incident.

But logs are also easy to overfill. A developer prints the whole request body while investigating a bug. A payment response is recorded because it is convenient. A failed login includes an email address and an authentication header. Months later, an old log archive contains a detailed copy of somebody’s private activity, even though nobody intended to build a second database of it.

Good logging is not about recording everything. It is about recording enough context to answer useful questions while treating logs as sensitive operational data.

Quick Answer

Before adding a log statement, define the question it should help answer. Prefer structured fields such as event, request_id, actor_type, status and duration_ms over a sentence containing an entire object.

Do not log passwords, access tokens, session identifiers, encryption keys or full payment details. Be careful with email addresses, phone numbers, identity numbers, health information and user-submitted content. Mask, hash, truncate or omit values when the original value is not required.

A useful review order is:

  1. Decide what operational or security event matters.
  2. Record the smallest useful set of fields.
  3. Sanitise values before they reach the logger.
  4. Restrict access, retention and export of the logs.
  5. Test both the success and failure paths.

Start with questions, not print statements

“Log more” is not a monitoring strategy. Start with the questions your team asks during an incident:

  • Did the request reach the application?
  • Which route or background job failed?
  • Was the failure caused by validation, a dependency or a timeout?
  • Did one customer see a problem, or did the whole service?
  • Can we connect the error to a trace, deployment or support ticket?

Each question suggests a small amount of context. A request log might need a request ID, route name, method, response status, duration and deployment version. A background job log might need the job type, attempt number, outcome and a non-sensitive record reference.

The goal is correlation, not duplication. If a database already stores a customer’s full profile, the application log normally needs a stable internal reference, not another copy of the name, address and phone number.

Prefer structured events

Structured logging makes logs easier to search and less tempting to fill with prose. A JSON event might look like this:

{
  "event": "invoice_generation_failed",
  "request_id": "req_7f3c",
  "invoice_id": "inv_2048",
  "account_ref": "acct_91a2",
  "dependency": "pdf-service",
  "error_class": "timeout",
  "duration_ms": 3100
}

The identifiers above are examples, not secrets. A person investigating the incident can search for the request or invoice in an authorised system. The event does not need to contain the invoice’s full content or a bearer token used to call the PDF service.

Choose field names and values consistently. timeout, timed_out, gateway-timeout and PDF service took too long may all describe the same condition, but inconsistent labels make dashboards and alerts harder to maintain. A small event vocabulary is easier to document and query.

Treat secrets as forbidden by default

Some values should never be copied into ordinary application logs. That includes passwords, API keys, access tokens, session cookies, private encryption material and database connection strings. The same rule applies to request headers and framework objects that may contain those values.

The dangerous pattern is often indirect:

logger.info("incoming request", { headers: req.headers, body: req.body });

It looks helpful during debugging, but it hands the logger every field the client can send and every header the client sends back. A safer pattern constructs an allow-list:

logger.info("incoming request", {
  event: "request_received",
  request_id: requestId,
  method: req.method,
  route: routeName,
  content_length: req.headers["content-length"]
});

If a temporary diagnostic genuinely requires a sensitive value, use a controlled, short-lived process with an explicit owner and expiry. Do not leave a debug flag permanently enabled, and do not assume that deleting a line later removes copies from central log storage, backups or exported incident files.

Decide whether to omit, mask, hash or truncate

Different fields need different treatment.

Omit a value when it is not needed for the question. For example, a password reset event usually needs the result and a request ID, not the reset URL.

Mask a value when an operator needs to recognise its shape. A payment card might be represented by a last-four display if the business and legal requirements allow it. An email address might be partially masked, but even a masked address can be personal data.

Hash a stable identifier when correlation is useful but the raw value is unnecessary. Use an appropriate keyed or protected approach where guessing attacks are a concern; a plain unsalted hash is not automatically anonymous.

Truncate free text and external error messages. User-controlled text can contain private information, enormous payloads or characters that make log searching confusing. Sanitising line breaks and delimiters also helps prevent one user-supplied value from forging several apparent log events.

These choices are not a substitute for knowing your organisation’s privacy and retention obligations. A value can remain sensitive after masking, and a hash can still be linkable.

Make failures as useful as successes

Teams often log a successful checkout but only record “something went wrong” for a failed one. That is backwards. Failure paths should include a safe reason category, dependency name, retry state and correlation ID.

Avoid dumping stack traces to a public response, but keep the server-side diagnostic detail behind access controls. Separate the message shown to a user from the event stored for operators. “We could not complete that request” may be appropriate in the interface; payment_provider_timeout with a request ID is more useful in the log.

Be precise about retry and idempotency. If a job can be attempted several times, record the attempt number and final outcome. Do not write a new alert for every harmless retry if the important event is that the job eventually failed or succeeded after recovery.

Protect the log system itself

A careful application can still leak data through an unsafe log pipeline. Review who can search logs, which services receive them, whether transport is protected, how long records are retained and where exports are stored. Developers should not automatically receive production-wide access just because logs are useful.

Add a redaction test to the pipeline. Send a deliberately fake token, password-shaped value and sample personal data through common error paths, then verify that the resulting events do not contain them. Test framework access logs too; an application may be safe while a proxy records a query string containing a secret.

Set a retention period that matches operational needs. A log that is never deleted becomes an unmanaged historical database. If an incident requires longer preservation, record who approved it and when the extended copy should be removed.

Common mistakes to avoid

The most common mistake is logging an entire object because it is faster than choosing fields. Another is assuming internal logs are harmless because they are not visible on the public website. Logs are copied, indexed, backed up and sometimes sent to third-party services.

Also avoid putting secrets in URLs. URLs are commonly recorded by browsers, reverse proxies, analytics tools and access logs. A reset token or API key in a query string can escape your intended redaction rules before your application code runs.

Finally, do not make logs the only source of truth. A log can say that an action was attempted, but an audited business record should establish what was actually committed. Use logs for diagnosis and security signals, and use purpose-built data stores for durable records.

A practical review checklist

For each new event, ask:

  1. What question does this event answer?
  2. Is the event name and severity consistent with existing events?
  3. Can every field be explained and justified?
  4. Does any field contain a secret, personal data or user content?
  5. Is the value omitted, masked, hashed or truncated appropriately?
  6. Can an operator correlate it with a request, job or deployment?
  7. What happens when the event is generated many times per minute?
  8. Who can access it, and how long does it need to exist?
  9. Have success, failure, timeout and retry paths been tested?
  10. Can the event be removed or changed without breaking incident response?

My Take

The best log is not the longest log. It is the smallest reliable record that helps a person make a decision during a stressful incident. Allow-list fields, consistent event names and short retention require a little more thought at the start, but they reduce both debugging noise and privacy risk later.

If you inherit an application with noisy logs, do not try to redesign everything in one sprint. Pick the five events used most often during support and incidents. Remove obvious secrets, add correlation IDs, standardise their fields and measure whether the next investigation becomes easier. Repeat from there.

Bottom Line

Application logs are part of your security boundary. Record events and context that help you operate the system, but do not turn every request, header and user object into a permanent copy. Use explicit fields, protect the pipeline, test redaction and delete data when its operational purpose ends.

For further guidance, see the OWASP Logging Cheat Sheet.