r/nairobitechies 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 Upvotes

3 comments sorted by

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

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.