asobi_ops_audit (asobi v0.75.1)

View Source

Core-wrapped audit for ops-plane mutations (ADR 0007).

mutation/4 runs the operation and writes the row from the operation's own return value. A call site therefore cannot forget to audit and cannot misreport the outcome, which is the whole point: the console's broadcast handler used to pass a hand-built {ok, SentTo} to its logger while the backing function had silently dropped every failed insert, so a half-delivered broadcast was recorded as a success.

The outcome contract

{ok, Succeeded, Failed} | {error, Reason}

Succeeded and Failed are lists of subjects the operation acted on - Failed entries carry their reason. A single-subject mutation is the one-element case, not a different contract, so nothing has to widen later when the first bulk endpoint lands. The stored outcome column is ok, partial or error, which is what makes "show me everything that did not fully succeed" an index scan rather than a jsonb parse.

{error, Reason} is the operation that never got as far as having subjects - a rejected parameter, a lost connection.

When the audit write itself fails

The audit never fails the operation. It runs after the mutation has already happened, so refusing the response cannot undo a ban or un-send a notification; it would only invite a retry that applies the change twice. A write-ahead row could fail closed, but it would record intent rather than outcome, and outcome - specifically partial failure - is what ADR 0007 asks for.

The cost of that choice is paid where it is cheapest: a failed insert is re-emitted at error level with every field the row would have carried, so the record degrades from queryable to greppable rather than disappearing. Nothing is dropped silently. A raise inside the audit path is caught for the same reason - the audit must not be able to fail an operation that already happened.

Erasure is the stated exception, and record_strict/4 is how it is taken. An erasure destroys the data it is about, so the row is the only surviving evidence the request was honoured; there is no degraded-but-still-there state for it to fall back to. record_strict/4 reports whether the insert landed so the caller can write it inside its own transaction and roll the erasure back when it did not. Nothing else should use it.

Successful writes are not also logged. The row is the record, and mirroring it into logs would double the retention surface holding player ids for no new information.

A mutation that raises is audited as an error and then re-raised with its original stacktrace. A crash is an outcome, and the plane should not be able to hide one by exploding.

Summary

Functions

Run Fun and audit whatever it returns.

Write one audit row for an outcome the caller already has.

Write one audit row and say whether it landed.

Types

failure()

-type failure() :: {subject(), term()}.

outcome()

-type outcome() :: {ok, [subject()], [failure()]} | {error, term()}.

subject()

-type subject() :: binary().

target()

-type target() :: {binary(), subject() | undefined} | undefined.

Functions

mutation(Actor, Action, Target, Fun)

-spec mutation(asobi_ops_auth:actor(), binary(), target(), fun(() -> outcome())) -> outcome().

Run Fun and audit whatever it returns.

Returns Fun's own value unchanged, so wrapping a call site is a substitution rather than a rewrite.

record(Actor, Action, Target, Outcome)

-spec record(asobi_ops_auth:actor(), binary(), target(), outcome()) -> ok.

Write one audit row for an outcome the caller already has.

Prefer mutation/4. This exists for a call site that performs its own sequencing and genuinely holds the real outcome; it can still be handed a lie, which mutation/4 cannot.

record_strict(Actor, Action, Target, Outcome)

-spec record_strict(asobi_ops_auth:actor(), binary(), target(), outcome()) -> ok | {error, term()}.

Write one audit row and say whether it landed.

For asobi_player_erase only, and the reason is in the moduledoc: an erasure's audit row is the only evidence left that the request was honoured, so it commits with the erasure or the erasure does not happen. Every other call site wants mutation/4, which cannot fail the operation it describes.