Audit logging tends to get built against a requirement that reads something like: record security-relevant events, retain them for a period, make them tamper evident. That requirement gets met. Everybody signs it off.
Then a real question arrives. Someone asks what happened to one specific request last Thursday, and whether the person who approved it was the person the record names, and whether the value that reached the third party was the real one or the substituted one.
And the log, which meets its requirement completely, cannot answer.
The shift: from retention to replay
An audit trail that satisfies retention answers "did anything security-relevant happen". An audit trail worth having answers "walk me through this one request".
Those need different data. The first needs events. The second needs to reconstruct a sequence. It should read like this, from one identifier, without joining anything, which is what Squidder writes:
09:14:02 request received app: contract-review
09:14:02 claimed / determined tool-call / tool-call agree
09:14:02 destination classed device hold
09:14:03 inspection passed 412 ms
09:14:03 refused, not final rule: egress.device-required
09:14:41 question delivered j.okonkwo (device 7c41)
09:17:55 answered approve
09:18:02 retry received matched to question
09:18:03 completed destination returned 200
Which checks ran in which order, what each decided, what was still unknown when a decision was taken, who was asked, what they said, and what the destination finally returned. If your log records outcomes but not the path to them, you have the first kind. It will pass an audit and fail the afternoon you actually need it.
Four things a trail has to carry, that most do not
A refusal has to say what arrived. This is the one that catches everybody. Rows recording that something was allowed are usually rich, because the code had finished gathering context by then. Rows recording a refusal are thin, because the refusal happened early, before the context existed. So the most important rows in your log are the least informative ones, and you find out when you cannot tell whether a refused call was even well-formed.
Who was asked, and who answered. If a human decision is part of the path, the record must name the person who decided, not the role and not the session. And it must be reconcilable with whoever the request claimed to be acting for, because those are two different facts and if they can differ you need to see that they did.
What was claimed, alongside what was determined. A caller usually asserts something about itself. Recording only what you concluded loses the disagreement, and the disagreement is the interesting part. Two fields, always, even when they agree.
One identifier that survives the whole path. A request that crosses several components, is refused, retried, approved and completed has to be one story. Without a single thread through it you have fragments and a good guess.
Tamper evidence is necessary and not sufficient
Chaining records so that alteration is detectable is worth doing, and it produces a useful demonstration: change a row, run the check, watch it fail.
It also creates an obligation people underestimate. A chained log is a decision you cannot take back. Every value in it is permanent, including the vocabulary. Rename a category and old records now say something you did not mean. Reuse a code for a new purpose and the history becomes unreadable in a way no migration can fix, because fixing it means breaking the chain.
So the discipline is not only about protecting the log. It is about being careful what you write into it, because you are writing it forever. A retired code should stay retired rather than be recycled. Reads should map old spellings rather than rewrite them.
And the thing that must never be in there
Values. A trail that records what was refused should name the field, not its contents. The most common way sensitive data ends up somewhere permanent, chained and exported for compliance is a well-meaning log line written to help somebody debug a detection.
The record of a credential in the wrong place must not itself be a copy of the credential in the wrong place.
The test
Pick one request from last month. Ask your log to tell you its whole story: every gate, every decision, who approved it, what came back.
If you can, verify the chain and export it, and you are in good shape.
If you find yourself joining three systems by timestamp and inferring the middle, you have a retention system. That is worth knowing before somebody asks.