Skip to content

Audit log

Somebody just tried something they were not allowed to do. It is already written down.

Every critical action funnels through one writer, so the log has one shape rather than fifteen. And the write is wrapped, so a failure in the log can never stop the thing it was logging — nobody gets an error at the desk because an audit table was busy. That is a trade, and it was made with eyes open.

What one row actually contains

One row, taken apart

Four questions. Four columns that answer them.

  1. 07:41:02Z
  2. auth.permission_denied
  3. m.okonkwo@clinic.ca
  4. Admin

CreatedAt

Written by the database in UTC, not by the machine that did the thing. A workstation with a wrong clock cannot move an audit row, which is the only reason a timestamp in a log is worth anything.

Action · Detail

Two fields, deliberately. Action is a dot-namespaced machine code, 64 characters, stable enough to still filter on in three years. Detail is up to 2,000 characters of the same event written as a sentence a person can read. A log with only codes cannot be understood; a log with only prose cannot be queried.

UserId · ActorEmail

Taken from the signed-in request at the moment of the action, not looked up afterwards. When nobody is signed in — a failed login, an invite being accepted — the actor is passed in explicitly instead, so the row still knows who it was about.

ActorRole

The role is captured on the row, but the feed shows the person’s current role from your member list and falls back to the captured one only if they have left. So somebody promoted last month shows as their new role against an old action. Worth knowing before you read one in anger.

Not shown above Every row also carries Ip and UserAgent — the first X-Forwarded-For hop with the port stripped, IPv6-safe, so addresses group instead of scattering. The tail does not display them. The row has them.

The boundary

You cannot read another clinic’s log. There is no setting for it.

WHERE l.ClinicId = @C

  1. ClinicId 4471 07:38:41Z auth.login_failed
  2. ClinicId 2210 07:38:44Z auth.login_success
  3. ClinicId 4471 07:38:59Z auth.login_lockout
  4. ClinicId 8803 07:39:12Z auth.logout
  5. ClinicId 2210 07:39:55Z auth.permission_denied
  6. ClinicId 4471 07:41:02Z auth.permission_denied
  7. ClinicId 8803 07:41:20Z auth.login_success
  8. ClinicId 4471 07:44:05Z auth.login_success
  9. ClinicId 2210 07:45:31Z auth.logout
Whose
The clinic id is not a filter the reader chooses, it is in the query. There is no view that spans clinics because there is no query that spans clinics — the scope is upstream of the screen.
Who
PermAuditView — Owner and Admin. The live rail and the log page share one gate and one projection, so a card that arrived in real time and a card that was loaded on open read identically. One gate is also one place to get it wrong, which is the point of there being one.
And when it fails
The feed is read-only and never writes. If it cannot answer it returns an empty list rather than an error, so the rail goes quiet instead of going red. An audit trail that shouts when it is unavailable teaches everybody to ignore it.

The live rail

Some audit rows never reach your dashboard. That is the feature.

  1. Log / LogAs

    Your rail One card, one second later

  2. LogAs(con, tran)

    Your rail Nothing. It never happened.

Off the thread
The client address is read on the request thread, because that is the only place it exists. Everything after it — the cross-process post to the hub — runs on a background work item, so a hub that is slow, or absent, never adds a millisecond to the thing the clinician is waiting for.
No phantom cards
A write inside a transaction can still roll back. Broadcasting it first and apologising afterwards is exactly how a dashboard learns to lie, so the transactional path is deliberately silent — it is the one that gets the audit row committed atomically with the action, and it buys that with saying nothing until it is real.
Same shape, both ways
A card that arrives live and a card that was already there when you opened the page come from one gate and one projection, so they read identically. Two code paths producing two slightly different cards is how people stop trusting the one they did not watch arrive.

What is not claimed

Here is what this page could have said. And what it says instead.

  1. Tamper-evident. It is an ordinary SQL table. There is no hash chain, no append-only storage and no write-once media. What it does have is a single writer and no update path through the application — which is a discipline, not a cryptographic guarantee, and the difference matters if anyone ever asks you to prove a row in front of a regulator.
  2. Every action, logged. The authentication family is enumerated in the service today; other families are added alongside the guards that raise them. The honest claim is about the mechanism — one writer, one shape, one gate — not about coverage, and you should ask which actions write a row before you rely on one that does not.
  3. Export it whenever you like. There is no export in the source. If you need the log out of the system, ask us and we will tell you what is actually possible today rather than what would sound better on this page.
  4. Retained for seven years. No retention period is written into the audit service. Your obligations under the CPSA standard and the Health Information Act are yours, and a vendor default is not a substitute for knowing what they are.

Four questions worth asking anyone, including us

Show me one row.
Not a screenshot of a dashboard. One row, with its columns and their limits.
What happens when the log cannot write?
If the answer is “the action fails”, ask what that means at eleven on a Monday. If it is “we carry on”, ask how you would ever know.
Is who-can-read-it a permission or a setting?
A setting can be changed by whoever is annoyed by it this afternoon.
Does a rolled-back action appear anywhere?
If it does, the dashboard is capable of showing you something that did not happen.

An audit log is not a feature you buy once and stop thinking about. It is a promise that somebody wrote something down, kept it out of the way of the work, and will still be able to show it to you in three years. Everything on this page is either in the code or is not claimed.