A useful security log event answers who did what, to which resource, from where, when, and whether it worked. Write it as structured JSON with a consistent key set, a UTC timestamp and a correlation ID. Free text lines are fine for humans and poor for queries, alerts and audits.
The minimum field set
Think of each event as a sentence with a fixed grammar.
- timestamp: when the event happened, in UTC, in a standard format.
- event name: a stable, machine-readable label such as
auth.login.failure, not a sentence. - actor: the user, service account or agent that did it, by a stable ID.
- action and target: what was attempted and on which resource.
- outcome: success, failure or denied.
- source: IP address, client type and, for services, the host or pod.
- correlation ID: the request or trace ID that ties this event to others.
- reason: a short code for failures, such as
bad_passwordorpolicy_denied.
Here is one event:
{
"timestamp": "2026-10-19T09:14:07.512Z",
"event": "auth.login.failure",
"actor": "user_8841",
"target": "web_console",
"outcome": "failure",
"reason": "bad_password",
"source_ip": "203.0.113.24",
"request_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"service": "auth-api",
"version": "2.14.0"
}
Why stable names matter
Alert rules match on event names and field values. If one developer writes login failed and another writes Login Failure, your detection misses half the events. Pick a naming scheme once, document it in the repo, and review new event names the way you review API changes.
Which events deserve this treatment
The OWASP Logging Cheat Sheet lists the categories to cover where possible: authentication successes and failures, authorization failures, session management failures, input validation failures, application errors and configuration changes, startups and shutdowns, and higher risk functions such as administrator actions, use of shared accounts, access to sensitive data and key rotation. It also warns against "alarm fog" from logging everything by checklist, so treat the list as a starting point and tune it to your threats.
What to keep out
The same cheat sheet says to remove, mask, hash or encrypt session identifiers, access tokens, passwords, encryption keys, connection strings and payment card data before logging. Our never log list goes deeper. A few practical habits:
- Log the user ID, not the email address, unless you need the email.
- Log that a token was rejected, never the token.
- Truncate or hash user-supplied strings that could be large.
Neutralize untrusted values
Any field filled from user input can carry newlines or delimiters. With JSON output, let the logging library do the encoding rather than building strings by hand. See log injection explained.
Make it a library, not a convention
The best way to get consistent fields is to hide them behind a helper. A small audit(event, actor, target, outcome) function that fills in timestamp, request ID and service name means developers cannot forget them. Add a unit test that fails if a security event lacks an actor.
Quick check
Should security events share a log stream with debug output?
Preferably not. Separate streams let you apply longer retention, tighter access and stricter integrity controls to the events you will need for investigations and audits.
Is JSON the only valid structured format?
No. Key-value pairs and syslog structured data also work. JSON is common because most log tools parse it without extra configuration.
Sources
Last reviewed: 19 October 2026.