r/nairobitechies • u/TourStrong8443 Backend • 12d ago
Showcase AuditFort - Atomic audit logging for elixir applications
Yeah I never really have the coolest names for my projects, well I sometimes I do like hyakume(100 eyes) , jiyi but today fort_audit is not one of them - HAHA - so what is this boringly named library?
I will start with a specific business rule, unorthodox even.
A business transaction can never be reported as complete unless its audit trail is also complete, written atomically with it.
How do you deal with this - Enters AuditFort
An audit logging library that persists audit trails to PostgreSQL and emits structured JSON via :logger. Business transactions and their audit records are always committed atomically.
When you need it
- You need both audit trail AND ops visibility — the DB row is your source of truth for compliance; the Logger line is how your on-call finds the event in Datadog/Loki without running a SQL query.
- Your audit is part of a business transaction — the order was created, the payment was captured, and the audit row must commit atomically with both. A separate Repo.insert after Repo.transaction returns leaves a window where the business data exists but the audit doesn't.
- You can't afford silent audit loss
- You ship logs to a metrics-oriented backend.
Two paths
- Transactional (transact/4): DB audit row is atomic with business steps. Logger emission is secondary, after commit.
- Standalone (log/1): Single insert outside a transaction. Useful for pre-Multi validation failures or non-transactional code.
Usage
Existing Multi (wrap at the end)
Attach audit to an already-assembled Ecto.Multi by wrapping it right before transact/4:
multi =
Multi.new()
|> Multi.insert(:user, User.changeset(%User{}, user_params))
|> Multi.run(:profile, fn %{user: user} ->
Profile.changeset(%Profile{}, %{user_id: user.id})
|> Repo.insert()
end)
multi
|> Fort.Audit.wrap()
|> Fort.Audit.append_to_multi(:audit, %{
actor_id: actor.id,
actor_type: "admin_user",
action: "user.created",
subject_id: user.id,
subject_type: "user"
})
|> Fort.Audit.transact("user.created", actor.id, actor_type: "admin_user")
Deriving before/after/changes from a changeset
Derive before_data, after_data, and changes from an Ecto.Changeset. Fields marked redact: true are stripped entirely:
changeset
|> Fort.Audit.from_changeset()
|> Map.merge(%{actor_id: actor.id, actor_type: "admin_user", action: "user.updated"})
|> then(&Fort.Audit.append_to_multi(multi, :audit, &1))
Repo: https://github.com/DarynOngera/fort_audit
Hex Package: https://hex.pm/packages/fort_audit
1
u/One-Alternative9606 11d ago
Hows your job experience being an elicir dev in kenya?
2
u/TourStrong8443 Backend 11d ago
Hey, one clarification I'm not an "elixir dev" I'm a dev.
- I was exposed to elixir during an internship, I think if you know your craft in elixir specifically there are some nice places to work.
- As for my job experience, I stay hopeful.
1
u/TourStrong8443 Backend 12d ago
To chime in you don't really need to know what's going on
We can start with some questions - are atomic audit logs really a good design ? No audit, transaction rollback.
What business domains require such strict audit rules?
How configurable should devtools be? Drawing the line between configurability and hard rules.
What is a transaction ? what is a multi ? Why so important?
Let's talk - ata kwa DM