AuroraMeter.Exporter.Journal (Aurora Meter v1.0.0)

View Source

The reference AuroraMeter.Exporter: it records what it was handed and returns the answer it was told to.

It exists for three jobs.

Teaching. It is the whole of an exporter with the provider taken out, so the shape of one is readable in a few dozen lines rather than inferred from an SDK wrapper.

Testing. A test scripts an answer for a subject reference and then asserts what the caller did with it. Scripting a rate limit or a lost acknowledgement is a line of code rather than a mocked HTTP layer.

A dry run. A host that wants to see what would be exported, without exporting it, can configure this as its exporter and read deliveries/1. That is a legitimate production use, and the one thing to know before choosing it is in the next paragraph.

It is memory, not a ledger

The journal is an Agent. Its log lives in that process and is gone when the process stops, when the node restarts, or when anybody calls reset/1. It writes no file and no row. Do not use it as a record of what was billed.

It is an Agent rather than the process dictionary on purpose: an answer scripted by a test process has to be visible to the worker process that calls AuroraMeter.Exporter.deliver/2, which is usually an Oban job or a Task, and a process-dictionary fake is invisible there. That defect exists in code this package's programme is replacing, and it is the reason two fakes could not be driven from the tests that needed them.

Using it

{:ok, _pid} = AuroraMeter.Exporter.Journal.start_link([])

AuroraMeter.Exporter.Journal.script("usage:ai:2026-09-01:0:12", [{:retry, 30}, :accepted])

AuroraMeter.Exporter.Journal.deliver([item], %{})
#=> [{"row-1", {:retry, 30}}]
AuroraMeter.Exporter.Journal.deliver([item], %{})
#=> [{"row-1", :accepted}]

AuroraMeter.Exporter.Journal.deliveries_for("usage:ai:2026-09-01:0:12")
#=> [%{item: %AuroraMeter.Exporter.Item{}, outcome: {:retry, 30}, attempt: 0, at: ~U[...]}, ...]

Scripted answers are consumed in order and then the default takes over, so "fail twice then succeed" is one call.

It is deterministic

No randomness, no sleeping, no message back to the caller. It records; it does not signal. A test that needs to observe a delivery while it is happening attaches its own :telemetry handler to the caller's event rather than asking the journal to tell it.

The one exception is {:raise, message}, which is not an outcome and is the only way to script a provider call that blows up. It is here because AuroraMeter.ExporterCase has to prove that a caller survives one, and a conformance area that no adapter can reach is a conformance area that proves nothing. A journal that has not been scripted with it never raises.

What it is scripted with

script/2 takes any term, not only the five documented outcomes. That is deliberate: the point of the malformed-response and unknown-status areas of AuroraMeter.ExporterCase is that a provider can say something nobody planned for, and AuroraMeter.Exporter.normalize/2 has to reduce it to :uncertain. An adapter that could only return valid outcomes could not exercise that path.

Summary

Types

One recorded delivery, in the order the journal saw it.

Functions

The child specification, so a host can start the journal from its own supervision tree.

The description a journal falls back to when it was started with none.

Records every item and answers each one.

Every delivery this journal recorded, oldest first.

As deliveries/0, against a journal started under name.

The deliveries recorded for one subject reference, oldest first.

As deliveries_for/1, against a journal started under name.

The description this journal was started with, or the default when it is not running.

Empties the log and the scripted queues, keeping the description and the default.

As reset/0, against a journal started under name.

Queues one outcome, or a list of them, for subject_ref.

As script/2, against a journal started under name.

Starts the journal.

Types

entry()

@type entry() :: %{
  item: AuroraMeter.Exporter.Item.t(),
  outcome: term(),
  attempt: non_neg_integer(),
  at: DateTime.t()
}

One recorded delivery, in the order the journal saw it.

Functions

child_spec(opts)

@spec child_spec(keyword()) :: Supervisor.child_spec()

The child specification, so a host can start the journal from its own supervision tree.

Written out rather than generated by use Agent, because the id is the journal's name: a host that runs a second journal under another name must be able to start both.

default_description()

@spec default_description() :: AuroraMeter.Exporter.description()

The description a journal falls back to when it was started with none.

deliver(items, context)

Records every item and answers each one.

The answer for an item is the next scripted entry for its subject_ref, or the journal's default outcome when the queue for that reference is empty. Entries come back in the caller's input order.

Exits with :noproc when the journal is not running: an exporter that cannot reach its provider must not quietly report success, and the caller's rescue turns the exit into :uncertain, which is the truthful answer.

deliveries()

@spec deliveries() :: [entry()]

Every delivery this journal recorded, oldest first.

deliveries(name)

@spec deliveries(Agent.agent()) :: [entry()]

As deliveries/0, against a journal started under name.

deliveries_for(subject_ref)

@spec deliveries_for(String.t()) :: [entry()]

The deliveries recorded for one subject reference, oldest first.

deliveries_for(name, subject_ref)

@spec deliveries_for(Agent.agent(), String.t()) :: [entry()]

As deliveries_for/1, against a journal started under name.

describe()

@spec describe() :: AuroraMeter.Exporter.description()

The description this journal was started with, or the default when it is not running.

reset()

@spec reset() :: :ok

Empties the log and the scripted queues, keeping the description and the default.

reset(name)

@spec reset(Agent.agent()) :: :ok

As reset/0, against a journal started under name.

script(subject_ref, outcomes)

@spec script(String.t(), term() | [term()]) :: :ok

Queues one outcome, or a list of them, for subject_ref.

Any term is accepted. A list is consumed one delivery at a time, in order, and the journal falls back to its default outcome once the queue is empty.

script(name, subject_ref, outcomes)

@spec script(Agent.agent(), String.t(), term() | [term()]) :: :ok

As script/2, against a journal started under name.

start_link(opts \\ [])

@spec start_link(keyword()) :: Agent.on_start()

Starts the journal.

Options: :name (default AuroraMeter.Exporter.Journal), :describe (a AuroraMeter.Exporter.description/0, defaulting to %{supports: [:usage_window, :event, :correction], max_batch: 100, idempotency_horizon: 86400, timestamp_window: %{future: 300, past: 2592000}}) and :default_outcome (the answer for a subject reference nothing was scripted for, default :accepted).