# `ExSandbox.Telemetry`
[🔗](https://github.com/FoundryStack/ex_sandbox/blob/v1.2.0/lib/ex_sandbox/telemetry.ex#L1)

Events both libraries emit, carrying the opaque `owner_ref` (012 T043).

## Attribution is the host's, not the library's

Every event carries `owner_ref` verbatim and nothing else identifying. The
library has no tenant concept, no project concept, and no way to decompose the
value — `FR-007` makes it opaque precisely so it cannot try. A host attaching a
`:telemetry` handler knows what its own `owner_ref` means and resolves it to
whatever attribution its observability stack wants.

This is the stated resolution of the plan's Constitution VII risk: the library
emits, the host attributes.

## ⚠️ Recorded mismatch with `010-observability` (T043)

T043 asks that this be checked against `010`'s actual requirements and that any
mismatch be **recorded rather than adapted to silently**. There is one, and it
is not resolvable inside this library:

**`010-FR-002`** requires telemetry be attributed to *"a tenant, project, and
thread where each applies"*. These events carry a single opaque `owner_ref`.
Three consequences follow:

  1. A host whose `owner_ref` encodes only a tenant cannot satisfy `010-FR-002`
     from these events alone — the project and thread are simply not present.
  2. The library **cannot** fix this by splitting `owner_ref` into parts. That
     would require parsing it, which `FR-007` forbids and which `012`'s opacity
     tests actively check for.
  3. Therefore the resolution must be at the host: either the host's
     `owner_ref` resolves to full attribution through a lookup it owns, or the
     host enriches these events in its own handler.

**`010-FR-006`** forbids secrets and raw tenant data in telemetry, *including
within error text*. These events pass mechanism error reasons through
unredacted, because the library cannot know what a mechanism's error term
contains. A host forwarding these to a telemetry backend is responsible for
redaction. Recorded here rather than solved because solving it in the library
would mean inspecting the opaque values `FR-007` forbids inspecting.

Neither mismatch is a defect in this module; both are boundary consequences
that `010`'s implementation will need to handle explicitly. They are written
down so that work starts from a known position rather than discovering it.

## Events

| Event | Measurements | Metadata |
|---|---|---|
| `[:ex_sandbox, :provision, :stop]` | `:duration` | `:owner_ref`, `:mechanism`, `:result` |
| `[:ex_sandbox, :start, :stop]` | `:duration` | `:owner_ref`, `:mechanism`, `:result` |
| `[:ex_sandbox, :stop, :stop]` | `:duration` | `:owner_ref`, `:mechanism`, `:result` |
| `[:ex_sandbox, :destroy, :stop]` | `:duration` | `:owner_ref`, `:mechanism`, `:result` |
| `[:ex_sandbox, :capability, :unavailable]` | `:count` | `:owner_ref`, `:mechanism`, `:missing` |

`:result` is `:ok` or `{:error, reason}`, kept distinguishable per `010-FR-004`
— an emitting capability may not collapse distinct causes into one generic
error.

# `capability_unavailable`

```elixir
@spec capability_unavailable(module(), ExSandbox.Sandbox.t() | nil, [
  ExSandbox.Capability.t()
]) :: :ok
```

Records that a host could not provide what a mechanism requires.

Emitted where the refusal happens rather than left to the caller: a mechanism
refusing to start is the correct behaviour but an invisible one, and an
operator seeing no sandboxes start needs to know it was a capability decision
rather than a crash.

# `hardening_verified`

```elixir
@spec hardening_verified(
  module(),
  ExSandbox.Sandbox.t(),
  :ok | {:error, atom()},
  map()
) :: :ok
```

Records the outcome of verifying that confinement actually applied
(`005` T045, `010` Emission Review).

## Why this event exists separately from the lifecycle span

A provision span reports whether provisioning *succeeded*. This reports
whether the sandbox that resulted is **confined**, and the two are not the
same question: `005` R9b measured a limiter invoked with correct arguments,
present in the process tree, named in configuration, and silently not applied.
A span would have called that a success.

Emitted on **both** outcomes deliberately. A failure event alone gives an
operator no way to distinguish "confinement is being verified and holding"
from "verification stopped running" — and those look identical in a dashboard
that only ever plots failures.

# `sandbox_placed`

```elixir
@spec sandbox_placed(module(), ExSandbox.Sandbox.t(), map()) :: :ok
```

Records where a sandbox was placed, for a host that tracks placements
(`005` T041, `FR-016`).

## Why an event rather than a call into the host

`012-FR-001` requires this library to reference no host module. Writing a
placement row directly — or taking a host module from config and calling it —
would put `Axonn.Sandbox.Beam.Placement` on this library's conscience, and
`ex_sandbox` would no longer be usable without Axonn's schema.

Telemetry inverts that: the mechanism announces what happened, and a host that
cares attaches a handler. A host that does not care attaches nothing and pays
nothing.

## ⚠️ `cookie_ref`, never the cookie

The per-sandbox cookie is defence in depth for `FR-003`, and telemetry
metadata reaches log aggregators, error trackers, and APM vendors. Passing the
cookie here would scatter it across systems chosen for searchability. This
takes a **reference**; resolving it stays the host's business.

# `span`

```elixir
@spec span(atom(), module(), ExSandbox.Sandbox.t(), (-&gt; result)) :: result
when result: term()
```

Runs `fun`, emitting a span for `operation` with `sandbox`'s owner attached.

Uses `:telemetry.span/3` so the start, stop, and exception events follow the
conventions a host's handlers already expect.

---

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