# `Ankusa.Trace`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/trace.ex#L1)

W3C trace context for a hook, and the spans every stage opens (ground rule 8).

The edge reads `traceparent` / `tracestate` off the request (`from_headers/1`)
and stores them on the hook as `Ankusa.Envelope.trace`, a map
`%{traceparent: "00-<trace-id>-<parent-id>-<flags>", tracestate: String.t() | nil}`.
Every sink then carries that value to the consumer: `traceparent` on
`Ankusa.Sink.Http`, the `traceparent` / `tracestate` fields and the
broker-native header pair on `Ankusa.Sink.Message`, the CloudEvents
`traceparent` extension on lifecycle events.

## No SDK, no cost

Core depends on `opentelemetry_api` only. Without an SDK started (the default
for a library user; `ankusa_server` starts one when `tracing.enabled`), `span/3`
calls its function directly and `current/1` returns the context the request
carried, so a `traceparent` sent to the edge reaches the consumer **byte for
byte** and nothing else happens.

## With an SDK

  * `ankusa.ingest` is a `:server` span, a child of the provider's span (the
    request's `traceparent`); `ankusa.verify` and `ankusa.commit` are its
    children. With an SDK the hook stores the *ingest span's* context
    (`current/1`): same trace id, our span as the parent id, `tracestate`
    carried through, the sampled flag as the sampler decided it. The value is
    fixed at ingest, so every retry, replay and sink carries the same one.
  * `ankusa.dispatch.attempt` (one per attempt, a `:producer` span) is a child
    of the stored context and **links** to it, so the whole life of a hook is
    one trace however long the retries take. `ankusa.claim_check` is the pack
    write of a large body; `ankusa.replay` is one step of a replay job;
    `ankusa.lifecycle.event` builds a lifecycle event.

Only the sampled bit of the trace flags is kept (`00` or `01`): the other bits
are reserved by the specification. A request whose `traceparent` is malformed,
repeated, version `ff`, or all-zero in its trace or parent id carries no trace
(the header stays an ordinary forwarded request header, as before). A
`tracestate` over 512 bytes is dropped; the `traceparent` still travels.

# `attributes`

```elixir
@type attributes() :: %{optional(String.t()) =&gt; String.t() | integer() | boolean()}
```

Span attributes: string keys, scalar values (an OpenTelemetry attribute).

# `span_opt`

```elixir
@type span_opt() ::
  {:attributes, attributes() | (-&gt; attributes())}
  | {:kind, :internal | :server | :client | :producer | :consumer}
  | {:parent, t() | nil}
  | {:link, t() | nil}
```

# `t`

```elixir
@type t() :: %{traceparent: String.t(), tracestate: String.t() | nil}
```

The trace context stored on a hook (`Ankusa.Envelope.trace`).

# `active?`

```elixir
@spec active?() :: boolean()
```

Whether an SDK is running (spans are recorded); `false` is the no-op API.

# `current`

```elixir
@spec current(t() | nil) :: t() | nil
```

The context to store on a hook being ingested: the current span's, when an SDK
runs and the process is inside a valid span; otherwise `fallback` (the context
the request carried, verbatim).

# `error`

```elixir
@spec error(atom() | String.t()) :: :ok
```

Mark the current span failed. `reason` is a short class (an atom or string),
never a raw error term: those can carry URLs and credentials.

# `from_headers`

```elixir
@spec from_headers([{String.t(), String.t()}]) :: t() | nil
```

The trace context of a request's headers, or `nil`. One pass over the list;
header names are matched without case. A hook without a `traceparent` pays for
nothing but that pass.

# `set_attributes`

```elixir
@spec set_attributes(attributes()) :: :ok
```

Add attributes to the calling process's current span (a no-op without one).

# `span`

```elixir
@spec span(String.t(), [span_opt()], (-&gt; result)) :: result when result: term()
```

Run `fun` inside a span named `name`; returns what `fun` returns.

Options: `:attributes` (a map, or a function that builds it: it runs only when
an SDK records the span, so the no-op path allocates nothing), `:kind`
(default `:internal`), `:parent` (a stored context to continue; default the
calling process's current span) and `:link` (a stored context to link to). An
exception in `fun` is recorded on the span and re-raised. With no SDK started
this is `fun.()`.

# `trace_id`

```elixir
@spec trace_id(t() | nil) :: String.t() | nil
```

The 32-hex trace id of a trace context, `nil` for `nil`. It is what a log line
carries as `trace_id` (`Ankusa.Edge.Ingest`), so a line can be found from the
trace and the other way round.

---

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