Every structured log line should answer four questions: when it happened, where in your system, who or what acted, and what the outcome was. Add one identifier that ties the line to its request. That small schema makes logs searchable, alertable and useful in an incident without extra parsing.
Why free text stops working
A line like "payment failed for order" is readable by a person and almost useless to a query. Once you have more than a few services, you want to ask questions such as "show every failed payment for this tenant in the last hour". That needs fields, not sentences.
Structured logging means each event is a set of named fields, usually JSON, written one event per line. The message stays short and human. Everything a query might filter on goes in its own field.
The starter schema
The OWASP logging guidance says each entry should answer "when, where, who and what", and may carry an interaction identifier to correlate related events. Turn that into fields:
- timestamp: when, in UTC, in a standard format
- service and version: where the code runs
- environment: production, staging and so on
- level: how serious the event is
- event: a stable short name, such as payment_declined
- actor: an internal user or service ID, not an email address
- outcome: success or failure
- request_id or trace_id: the correlation identifier
- message: a short sentence for humans
Here is one line in that shape.
{"ts":"2026-10-19T09:14:03.221Z","service":"billing","version":"1.8.2","env":"prod","level":"warn","event":"payment_declined","actor":"u_48213","outcome":"failure","request_id":"req_7f3a","message":"card declined by issuer"}
Keep event names stable
The event field is the one you will alert on, so treat it like an API. Pick lowercase names with a category prefix, and do not rename them casually. If a dashboard counts payment_declined and someone ships payment_failed instead, the chart drops to zero and nobody is told.
OWASP publishes a logging vocabulary for security events that uses the same idea, with a category followed by an action. You can borrow its naming even if you do not adopt its field list.
Put variable data in fields, not the message
Do not build the message by pasting values into a sentence. Put the order ID in an order_id field. Two benefits follow: you can filter on it, and you can see at a glance which values you are about to write to disk, which helps when someone asks whether personal data ends up in logs.
Decide the schema once
Wrap your logger in a small shared library so every service emits the same keys. Teams that let each service pick its own field names end up with user, userId, uid and user_id in one search index. A shared wrapper also gives you one place to add redaction later.
Quick check
Should logs be JSON?
Usually yes for services that feed a log platform, because it is easy to parse. Local development can print a friendlier format as long as the fields are the same.
How many fields is too many?
Start with the nine above. Add a field only when you can name the query that needs it, and check whether it carries personal data first.
Sources
Related: what to log in a web app and what never to log.
Last reviewed: 19 October 2026.