# `AshOnetime.Telemetry`
[🔗](https://github.com/baselabs/ash_onetime/blob/v1.5.0/lib/ash_onetime/telemetry.ex#L1)

Emits the closed, value-free telemetry surface for keyed-effect admission.

## Out-of-the-box metrics attachment

The library emits events but does NOT attach a handler — a fresh application sees nothing
until it attaches one. Call `attach/0` (or `attach/1` with options) from your application
startup (e.g. your `start/2` callback's supervised children, after the repo is started) to
attach the default handler. The handler is a pure router: it re-emits every closed event —
the 12 admission/business events and the `:uncertain_exception` diagnosis event — as a
downstream `[:ash_onetime, event, :metric]` event carrying the same atoms-only metadata and
a normalized `:count` or `:duration` measurement. It does NOT depend on `telemetry_metrics`;
attach your own `Telemetry.Metrics` reporter or custom aggregator to the `:metric` stream —
`documentation/telemetry.md` carries the runnable example.

    # in your application startup, once per VM
    AshOnetime.Telemetry.attach()

# `admission`

```elixir
@spec admission(non_neg_integer(), atom(), module(), atom(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :admission]` with `:duration` and `result_class` in
`[:admitted, :rejected, :failed]`. The classified outcome of an admission attempt.

# `attach`

```elixir
@spec attach(keyword()) :: :ok | {:error, :already_exists}
```

Attaches the default metrics handler to the closed `[:ash_onetime, *]` event surface —
all 13 events, no silent drops: the 12 admission/business events and the
`:uncertain_exception` diagnosis event (its `%{strategy:, phase:, exception:}` metadata
forwards unchanged, normalized to `count: 1`).

The handler re-emits each event as a downstream `[:ash_onetime, event, :metric]` event
carrying the same atoms-only metadata and a normalized `:count` or `:duration` measurement.
This gives a consumer a single attach point to consume via their own `Telemetry.Metrics`
reporter or custom aggregation, without hand-rolling the event list and the count/duration
split.

The handler is a pure router — it adds no state and emits nothing the upstream validator did
not already permit, so the value-free guarantee is preserved. It does NOT depend on
`telemetry_metrics`; a consumer running a `Telemetry.Metrics` reporter declares the metric
definitions in their own `MyApp.Telemetry` (see the `documentation/telemetry.md` runnable
example for the canonical counter/summary definitions).

Idempotent per `name` — safe to call once at boot; a second attach with the same name
returns `{:error, :already_exists}` without detaching the first.

## Options

  * `:name` — scope the handler to a unique id (default `nil`, one handler per VM). Use a
    distinct name to attach alongside another consumer's handler.

## Examples

    # once at boot, in your app's start/2 after the repo starts
    AshOnetime.Telemetry.attach()

    # attach a second, separately-scoped handler
    AshOnetime.Telemetry.attach(name: :edge)

# `cache`

```elixir
@spec cache(atom(), module(), atom(), atom()) :: :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :cache]` with `result_class` in
`[:hit, :miss, :stale, :corrupt, :failure, :timeout, :stored, :expired, :oversized, :disabled]`.
The optional completed-response cache outcome for an idempotency admission.

# `cleanup`

```elixir
@spec cleanup(atom(), module(), atom(), non_neg_integer(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :cleanup]` with `:count` and `result_class` in
`[:claims_deleted, :partitions_dropped, :partitions_created]`. A bounded cleanup run.

# `conflict`

```elixir
@spec conflict(atom(), module(), atom(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :conflict]` with `result_class` in
`[:complete, :processing, :nonce_used, :malformed]`. A collision on an in-flight or
completed claim under the same logical key.

# `detach`

```elixir
@spec detach(keyword()) :: :ok | {:error, :not_found}
```

Undoes `attach/1` by detaching the handler.

Pass the same `:name` used when attaching to detach a scoped handler.

## Examples

    :ok = AshOnetime.Telemetry.attach()
    :ok = AshOnetime.Telemetry.detach()

    :ok = AshOnetime.Telemetry.attach(name: :edge)
    :ok = AshOnetime.Telemetry.detach(name: :edge)

# `encoding`

```elixir
@spec encoding(non_neg_integer(), atom(), module(), atom(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :encoding]` with `:duration` and `result_class` in
`[:stored, :rejected, :rollback, :failed]`. The outcome of encoding a response payload
for storage/replay.

# `external_recovery`

```elixir
@spec external_recovery(non_neg_integer(), atom(), module(), atom(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :external_recovery]` with `:duration` and `result_class` in
`[:processing_committed, :execute_succeeded, :recover_succeeded, :absence_proven,
:outcome_unknown, :external_effect_unavailable, :finalize_locked, :replayed]`. The stage
outcomes of the committed external-effect recovery protocol.

# `fingerprint_mismatch`

```elixir
@spec fingerprint_mismatch(atom(), module(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :fingerprint_mismatch]` with `result_class: :rejected`. A stored
response whose fingerprint does not match the incoming request is rejected (no replay).

# `handler_id`

```elixir
@spec handler_id(term()) :: binary()
```

The unique id used to attach the default metrics handler.

Without an argument the id is the constant `"ash-onetime-default-metrics"`. When a name
is provided the id is suffixed with the inspected name, which allows multiple scoped
handlers (e.g. one per OTP node in a multi-node setup) to coexist.

# `reap`

```elixir
@spec reap(atom(), module(), atom(), non_neg_integer(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :reap]` with `:count` and `result_class: :claims_reaped`. The count
of abandoned `processing` recovery points reaped by a bounded reaper run.

# `replay`

```elixir
@spec replay(non_neg_integer(), atom(), module(), atom(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :replay]` with `:duration` and `result_class` in
`[:returned, :rejected]`. The replayed-vs-rejected decision on a stored response.

# `store_uncertainty`

```elixir
@spec store_uncertainty(atom(), module(), atom(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :store_uncertainty]` with `result_class` in
`[:sent, :unknown, :disconnected, :lock_timeout, :worker_timeout]`. The authoritative-state-
unavailable conditions on the committed-claim path; all fail closed (see operations.md).

# `uncertain_exception`

```elixir
@spec uncertain_exception(atom(), keyword()) :: :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :uncertain_exception]` — a store-internal exception (a raise inside a
committed-claim transaction) before it is collapsed to `:dispatched_unknown`. This is a
diagnosis surface, NOT an admission/business event: it bypasses `emit/6` (which is shaped for
admission events with strategy/resource/action/result_class and validates a result-class
allowlist) and calls `:telemetry.execute/3` directly with a `%{strategy:, phase:, exception:}`
metadata map. The `exception` is the exception STRUCT MODULE (e.g. `Postgrex.Error`), not the
full struct — to avoid leaking request material that may be embedded in an exception message.

The closed metadata shape is validated, not conventional: `strategy` must be one of the two
strategies and `opts` must carry exactly `:phase` and `:exception`, both atoms — anything else
(extra keys, non-atom values) returns `{:error, :telemetry_invalid}` without emitting. This is
what keeps the value-free guarantee true for every downstream consumer, direct subscribers
and the default `:metric` router alike: an atom cannot carry request material.

A fresh application sees nothing unless it attaches a handler (the lib's telemetry-only
posture). Consumers wanting store-transaction diagnosis attach to this event.

# `untracked_execution`

```elixir
@spec untracked_execution(atom(), module(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :untracked_execution]` with `result_class: :checkout_unavailable`.
An idempotent action executed without a stored admission after a checkout failure — correct
by design but worth visibility (the optional `:execute_untracked` path).

# `verification`

```elixir
@spec verification(non_neg_integer(), atom(), module(), atom(), atom()) ::
  :ok | {:error, AshOnetime.Error.t()}
```

Emits `[:ash_onetime, :verification]` with `:duration` and `result_class` in
`[:verified, :rejected, :timeout]`. The outcome of a token/proof verification.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
