What does good audit logging for security-sensitive actions actually look like?
A debug log helps engineers understand the system; an audit log must convince a human, after the fact, exactly who did what.
LEARN BY DOINGPut your intuition to work3 steps
What does good audit logging for security-sensitive actions actually look like?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#What audit logging is actually for
Audit logging exists to answer one question with confidence, after the fact: who did what, to which resource, when, from where, and did it succeed? That question comes up during a security incident, a compliance audit, a customer dispute ("I never approved that refund"), or a routine access review. It is a fundamentally different job from application or debug logging, even though both end up as lines of structured text somewhere. Debug logs exist to help engineers understand how the system behaved technically — they can be verbose, sampled, reformatted freely between releases, and thrown away after a few days. Audit logs exist to prove what happened to a human who wasn't there when it happened, so they need a stable schema, a defined retention period, and — critically — enough integrity that they remain credible evidence even if the account that generated them is later compromised.
#What belongs in a good entry
A useful audit entry is structured, not a sentence of prose, so it can be queried, alerted on, and correlated at scale. Here's a realistic example for a permission change:
{"event_id": "01J8X6M6Z3TB6R6E6E9G6P9K4V","timestamp": "2026-08-21T14:32:07.418Z","action": "user.role.granted","outcome": "success","actor": {"user_id": "usr_9f3e2b","email": "admin@company.com","session_id": "sess_7d1a9c22","ip_address": "203.0.113.42","user_agent": "Mozilla/5.0 (compatible; internal-admin-console)"},"target": {"type": "user","id": "usr_1a2b3c","display": "j.doe@company.com"},"changes": {"role": { "before": "member", "after": "billing_admin" }},"request_id": "req_4c9f7a10","source": "admin-console","reason": "quarterly access review, ticket OPS-4821"}
| Field | Why it matters |
|---|---|
event_id | A unique, ideally sortable identifier (a ULID works well) so entries can be deduplicated and referenced individually — for example, in an incident timeline |
timestamp | UTC, millisecond precision; audit logs from services in different time zones must be comparable without conversion |
actor | Who did it — including a distinct field for impersonation, since "support agent acting as customer" is a materially different fact from "customer acting as themselves" |
action | A stable, machine-readable verb (user.role.granted, not "changed a user's role") so it can be filtered, aggregated, and alerted on without parsing free text |
target | What resource was acted on, identified unambiguously (an ID, not just a display name that can change) |
outcome | Success or failure — a burst of outcome: "failure" events for the same action is often the first visible sign of an attack in progress |
changes | A before/after diff, so a reviewer doesn't have to reconstruct state from other systems — but never the full new record, only the fields that actually changed |
request_id | Correlates the audit entry with the same request's traces, metrics, and debug logs elsewhere in your observability stack |
reason | Optional, but valuable for any action your policy requires a stated justification for (access grants, data exports, account deletions) |
#What must never appear in an audit entry
Passwords, session tokens, API keys, and full payment-card or government-ID numbers should never appear in an audit log, including inside a "before/after" diff — mask or truncate them (last four digits only) if the fact that a value changed is itself worth recording. It's also worth resisting the temptation to log entire request or response bodies "just in case": that turns the audit trail into an unofficial second copy of your production data, usually with weaker access controls than the primary database has, which defeats the purpose of having an audit trail at all.
#Storage properties that matter as much as the schema
An audit log is itself a target the moment an attacker gets a foothold inside your systems — if they can edit or delete their own entries, the log stops being usable as evidence. That argues for write-once storage in a separate system or account the compromised service credentials cannot reach with delete permission, or a hash-chained log where each entry embeds a hash of the previous one, so any retroactive edit is detectable even if outright deletion isn't prevented. Retention should be driven by whatever compliance framework applies (SOC 2, PCI-DSS, and HIPAA all specify minimums, commonly a year or more of readily queryable storage plus longer cold storage). And access to the audit trail itself is a security-sensitive action worth auditing in turn — "who looked at the audit log, and when" is a real question incident responders ask.