Chronicle (chronicle v0.1.0)
Copy MarkdownTamper-evident audit facts and restorable Ecto record versions.
Configure the common single-repository case:
config :chronicle,
repo: MyApp.Repo,
signing_key: {:system, "CHRONICLE_SIGNING_KEY_BASE64", :base64}Ecto writes mirror Ecto.Repo and commit their audit version atomically:
Chronicle.insert(changeset, actor: current_user)
Chronicle.update(changeset, actor: current_user)
Chronicle.delete(record, actor: current_user)Read or reconstruct record history:
Chronicle.history(record)
Chronicle.at(record, version: 3)
Chronicle.revert(record, version: 3)record/3 captures facts that are not database mutations, while
transaction/3 groups related Ecto writes and arbitrary facts into one
signed unit.
Block syntax
Every construct that wraps a section of your code takes a do block:
transaction/3, span/3, run/3, group/3,
Chronicle.Phoenix.with_context/3, Chronicle.Phoenix.run/4, and
Chronicle.Oban.with_context/2. They are macros, so use Chronicle once per
calling module — the same arrangement as Logger:
defmodule MyApp.Orders do
use Chronicle
def checkout(user, order) do
Chronicle.transaction "order.checkout", actor: user do
Chronicle.record!("payment.captured", %{order_id: order.id})
end
end
endOne use Chronicle covers the optional Phoenix and Oban integrations too,
when those dependencies are present. Every construct also accepts an explicit
zero-arity function in the same position, so existing function-form calls
keep working:
Chronicle.transaction("order.checkout", [actor: user], fn -> ... end)Why the block forms are macros
do/end is syntax, not a value. A function cannot receive a block, so
every construct that reads as Chronicle.transaction "type" do ... end has to
be a macro — and macros must be required before use, exactly as Logger is.
That is the only reason use Chronicle exists; it requires this module and
whichever optional integrations are present, and does nothing else.
The consequence worth knowing is the failure mode. Calling a block form
without requiring the module does not produce a helpful "you must require"
message: Elixir parses the block into a keyword list with a :do key, finds
no matching function head, and raises FunctionClauseError from somewhere
that looks unrelated. If a block form fails that way, the missing require
is the first thing to check.
Both forms exist because the block reads better in application code and the
function form composes better when the callback is already a value. They
route to the same implementation — __split_block__/3 decides which one it
was handed — so there is one behaviour and one place to change it.
Summary
Functions
Requires Chronicle so its block-form macros can be used.
Alias of ref/2, for readability at actor call sites.
Reconstructs an Ecto record at a version, event, timestamp, or its latest audited state.
Reads the current unverified ledger head. This does not establish integrity.
Deletes an Ecto record and records a signed tombstone atomically.
Raising variant of delete/2.
Executes one grouped unit without establishing context.
Marks a value to be stored as a deterministic SHA-256 fingerprint.
Checks signing keys, background-verifier status, and external anchor lag without performing a complete verification.
Returns signed versions for an Ecto record, newest first.
Inserts an Ecto changeset and its signed version in one transaction.
Raising variant of insert/2.
Returns signing-key inventory and availability for a named store.
Marks a map field or list item to be dropped entirely.
Persists or buffers a pre-built event.
Records one audit fact.
Raising variant of record/3.
Creates a stable audit reference from a value implementing Chronicle.Identity.
Creates a stable audit reference from an explicit type and identifier.
Returns a changeset that transitions the current record to a historical version. It never persists the change.
Executes one durable grouped unit, establishing context from its options.
Marks a value to be stored as "[REDACTED]".
Records one timed operation and its outcome.
Runs domain changes and all nested audit facts as one signed group in the configured repository transaction.
Updates an Ecto changeset and records its signed version atomically.
Raising variant of update/2.
Verifies the configured ledger of a named store.
Verifies every ledger in a named store, including unsigned-row coverage.
Types
@type audit_options() :: keyword()
@type outcome() :: Chronicle.Event.outcome()
Functions
Requires Chronicle so its block-form macros can be used.
Alias of ref/2, for readability at actor call sites.
@spec at(struct() | module(), audit_options() | term()) :: {:ok, struct()} | {:error, Chronicle.Error.t()}
Reconstructs an Ecto record at a version, event, timestamp, or its latest audited state.
Fails with :snapshot_incomplete rather than inventing values when the
version omits protected fields, and with :record_deleted for a tombstone.
@spec at(module(), term(), audit_options()) :: {:ok, struct()} | {:error, Chronicle.Error.t()}
@spec checkpoint( atom(), keyword() ) :: {:ok, Chronicle.Integrity.Checkpoint.t()} | {:error, Chronicle.Error.t()}
Reads the current unverified ledger head. This does not establish integrity.
@spec delete(Ecto.Changeset.t() | struct(), audit_options()) :: {:ok, struct()} | {:error, Ecto.Changeset.t() | Chronicle.Error.t()}
Deletes an Ecto record and records a signed tombstone atomically.
@spec delete!(Ecto.Changeset.t() | struct(), audit_options()) :: struct()
Raising variant of delete/2.
Executes one grouped unit without establishing context.
Takes a do block or a zero-arity function.
@spec hash(term()) :: Chronicle.Sensitive.t()
Marks a value to be stored as a deterministic SHA-256 fingerprint.
A fingerprint answers correlation questions — did this field change, did it revert to an earlier value, does it match another record — without storing the value.
It is not secrecy. The digest is unsalted, so any value drawn from a small or
guessable set (an email address, a phone number, a postcode, a status, a
short numeric code) can be recovered by hashing candidates until one matches.
Use secret/1 or omit/0 when the value itself must not be recoverable.
@spec health( atom(), keyword() ) :: {:ok, map()} | {:error, Chronicle.Error.t()}
Checks signing keys, background-verifier status, and external anchor lag without performing a complete verification.
@spec history(struct() | module(), audit_options() | term()) :: {:ok, [Chronicle.Version.t()]} | {:error, Chronicle.Error.t()}
Returns signed versions for an Ecto record, newest first.
Accepts a loaded struct, or a schema module plus a primary key. Composite
primary keys take a map or keyword list. Defaults to 50 entries; :limit
and :offset page through older versions.
@spec history(module(), term(), audit_options()) :: {:ok, [Chronicle.Version.t()]} | {:error, Chronicle.Error.t()}
@spec insert(Ecto.Changeset.t(), audit_options()) :: {:ok, struct()} | {:error, Ecto.Changeset.t() | Chronicle.Error.t()}
Inserts an Ecto changeset and its signed version in one transaction.
Returns {:error, changeset} for invalid domain changes and
{:error, %Chronicle.Error{}} when the audit write fails, in which case the
domain mutation is rolled back.
@spec insert!(Ecto.Changeset.t(), audit_options()) :: struct()
Raising variant of insert/2.
@spec keys(atom()) :: {:ok, map()} | {:error, Chronicle.Error.t()}
Returns signing-key inventory and availability for a named store.
@spec omit() :: Chronicle.Sensitive.t()
Marks a map field or list item to be dropped entirely.
@spec persist(Chronicle.Event.t(), audit_options()) :: {:ok, Chronicle.Event.t()} | {:error, Chronicle.Error.t()}
Persists or buffers a pre-built event.
Set immediate: true to bypass an active group. This is primarily intended
for transactional adapters such as Chronicle.Multi.
@spec record(String.t(), map() | audit_options()) :: {:ok, Chronicle.Event.t()} | {:error, Chronicle.Error.t()}
Records one audit fact.
Data is a map; options are a keyword list. Unknown option keys raise, so a keyword list passed where data was intended is reported rather than silently dropped.
@spec record(String.t(), map(), audit_options()) :: {:ok, Chronicle.Event.t()} | {:error, Chronicle.Error.t()}
@spec record!(String.t(), map() | audit_options()) :: Chronicle.Event.t()
Raising variant of record/3.
@spec record!(String.t(), map(), audit_options()) :: Chronicle.Event.t()
Creates a stable audit reference from a value implementing Chronicle.Identity.
Creates a stable audit reference from an explicit type and identifier.
@spec revert( struct(), audit_options() ) :: {:ok, Ecto.Changeset.t()} | {:error, Chronicle.Error.t()}
Returns a changeset that transitions the current record to a historical version. It never persists the change.
Executes one durable grouped unit, establishing context from its options.
The lower-level primitive behind transaction/3, for non-Ecto providers.
Takes a do block or a zero-arity function.
@spec secret(term()) :: Chronicle.Sensitive.t()
Marks a value to be stored as "[REDACTED]".
Records one timed operation and its outcome.
Takes a do block or a zero-arity function. The callback's value is
returned unchanged; an exception is recorded as a failure and re-raised.
Chronicle.span "payment.authorize", subject: order do
Payments.authorize(order)
end
Runs domain changes and all nested audit facts as one signed group in the configured repository transaction.
Takes a do block or a zero-arity function:
Chronicle.transaction "order.checkout", actor: user do
Chronicle.update!(changeset)
endThe block's value is the transaction result. By default the outcome is a
failure only when the block returns :error or {:error, reason}; pass
:classify to change that.
@spec update(Ecto.Changeset.t(), audit_options()) :: {:ok, struct()} | {:error, Ecto.Changeset.t() | Chronicle.Error.t()}
Updates an Ecto changeset and records its signed version atomically.
@spec update!(Ecto.Changeset.t(), audit_options()) :: struct()
Raising variant of update/2.
@spec verify( atom(), keyword() ) :: {:ok, Chronicle.Integrity.Checkpoint.t()} | {:error, Chronicle.Error.t()}
Verifies the configured ledger of a named store.
@spec verify_all( atom(), keyword() ) :: {:ok, map()} | {:error, Chronicle.Error.t()}
Verifies every ledger in a named store, including unsigned-row coverage.