# `Otel.Trace.Span`
[🔗](https://github.com/yangbancode/otel/blob/main/lib/otel/trace/span.ex#L1)

SDK implementation of the `Otel.Trace.Span` behaviour
(`trace/sdk.md` §Span L692-L944) — data + lifecycle
operations.

Holds all span data during its lifecycle. Creation is a pure
operation; all mutating operations read/write the span in ETS via
`SpanStorage` and are no-ops when the span is not in ETS (already
ended or dropped), satisfying spec `trace/api.md` L478-L481
(an ended span SHOULD become non-recording).

Registered as the global span module on SDK application start;
the API layer's `Otel.Trace.Span` dispatches to the functions
defined here via the `Otel.Trace.Span` behaviour.

All functions are safe for concurrent use — every mutation
goes through `:ets.update_element` / `:ets.insert` against
the public `SpanStorage` table, satisfying spec L883
(*"Span — all methods MUST be documented that
implementations need to be safe for concurrent use by
default."*).

## Public API

| Callback | Role |
|---|---|
| `start_span/4` | **SDK** (lifecycle) — sampler + id-generator + storage insert |
| `recording?/1`, `set_attribute/3`, `set_attributes/2`, `add_event/2`, `add_link/2`, `set_status/2`, `update_name/2`, `record_exception/4`, `end_span/2` | **SDK** (OTel API MUST/SHOULD) — `trace/api.md` §Span operations L449-L705 |

## Design notes

### Span-resident SpanLimits

`span_limits` is stored as a field on each span rather than
threaded through call arguments or fetched from a global
registry, so `set_attribute/3`, `add_event/2`, etc. operate
on the span fetched from `SpanStorage` without a second
lookup. The value comes from `Otel.Trace.Tracer`'s compile-time
`@span_limits` literal — minikube hardcodes the spec defaults
and exposes no override.

This diverges from `opentelemetry-erlang`, which threads
limits through `otel_span_utils` per call
(`opentelemetry/src/otel_span_utils.erl`).

### Dropped-count tracking on SDK types

Spec `common/mapping-to-non-otlp.md` L75-L77 (linked from
`trace/sdk.md` L260-L262) MUST: *"OpenTelemetry dropped
attributes count MUST be reported as a key-value pair
associated with the corresponding data entity (e.g. Span,
Span Link, Span Event, Metric data point, LogRecord)."*

Five counters are tracked, all on SDK-layer types:

- `Otel.Trace.Span.dropped_attributes_count` (proto
  `Span` field 10)
- `Otel.Trace.Span.dropped_events_count` (proto `Span`
  field 12)
- `Otel.Trace.Span.dropped_links_count` (proto `Span`
  field 14)
- `Otel.Trace.Event.dropped_attributes_count` (proto
  `Span.Event` field 4)
- `Otel.Trace.Link.dropped_attributes_count` (proto
  `Span.Link` field 5)

`Otel.Trace.Event` and `Otel.Trace.Link` are SDK
wrapper structs constructed from the API-layer
`Otel.Trace.Event` / `Otel.Trace.Link` at the
moment limits are applied. Keeping the count off the API
types preserves API↛SDK layer independence
(`.claude/rules/code-conventions.md`); the API spec
(`trace/api.md` §Add Events L520-L558, §Link L803-L834)
does not define `dropped_attributes_count` on Event/Link.

Counters are incremented at every callsite where
`SpanLimits` causes a discard: `start_span/4` (initial
attributes/events/links), `set_attribute/3`,
`set_attributes/2`, `add_event/2`, and `add_link/2`. Per
spec `common/README.md` L262-L274, value-length truncation
is **not** a drop — only count-limit overflow is.

### `instrumentation_scope` on the span

This field exists on the span but does not appear in the
proto `Span` message — it is held on the span for grouping
into `ScopeSpans` at export time.

Recording status is **not** a struct field. spec
`trace/api.md` §IsRecording L463-L495 requires only a
*function returning bool* (no struct shape mandated);
`Otel.Trace.Span.recording?/1` derives it from
`Otel.Trace.SpanStorage.get/1` (presence of an
`:active` row). Storage status is the single source of
truth, avoiding stale-replica risk between the struct field
and storage.

## References

- OTel Trace SDK §Span: `opentelemetry-specification/specification/trace/sdk.md` L692-L944
- OTel Trace API §Span: `opentelemetry-specification/specification/trace/api.md` L449-L705
- OTLP proto Span: `opentelemetry-proto/opentelemetry/proto/trace/v1/trace.proto`

# `primitive`

```elixir
@type primitive() ::
  String.t() | {:bytes, binary()} | boolean() | integer() | float() | nil
```

# `primitive_any`

```elixir
@type primitive_any() ::
  primitive() | [primitive_any()] | %{required(String.t()) =&gt; primitive_any()}
```

# `start_opts`

```elixir
@type start_opts() :: [
  kind: Otel.Trace.SpanKind.t(),
  attributes: %{required(String.t()) =&gt; primitive_any()},
  links: [Otel.Trace.Link.t()],
  start_time: non_neg_integer(),
  is_root: boolean()
]
```

Options accepted by `Otel.Trace.Tracer.start_span/3`.

- `:kind` — `t:Otel.Trace.SpanKind.t/0`. Spec L405-L406.
- `:attributes` — initial attributes. Spec L407-L409.
- `:links` — initial Links. Spec L410-L412.
- `:start_time` — explicit start timestamp (nanoseconds since
  the Unix epoch). Spec L413-L414.
- `:is_root` — boolean indicator that this Span should be a
  root Span, ignoring whatever current span the resolved
  Context carries. Spec L390-L391.

# `t`

```elixir
@type t() :: %Otel.Trace.Span{
  attributes: %{required(String.t()) =&gt; primitive_any()},
  dropped_attributes_count: non_neg_integer(),
  dropped_events_count: non_neg_integer(),
  dropped_links_count: non_neg_integer(),
  end_time: non_neg_integer() | nil,
  events: [Otel.Trace.Event.t()],
  instrumentation_scope: Otel.InstrumentationScope.t(),
  kind: Otel.Trace.SpanKind.t(),
  links: [Otel.Trace.Link.t()],
  name: String.t(),
  parent_span_id: Otel.Trace.SpanId.t() | nil,
  resource: Otel.Resource.t(),
  span_id: Otel.Trace.SpanId.t(),
  span_limits: Otel.Trace.SpanLimits.t(),
  start_time: non_neg_integer(),
  status: Otel.Trace.Status.t(),
  trace_flags: Otel.Trace.SpanContext.trace_flags(),
  trace_id: Otel.Trace.TraceId.t(),
  tracestate: Otel.Trace.TraceState.t()
}
```

# `add_event`

```elixir
@spec add_event(
  span_ctx :: Otel.Trace.SpanContext.t(),
  event :: Otel.Trace.Event.t()
) :: :ok
```

Adds an event to the span.

# `add_link`

```elixir
@spec add_link(
  span_ctx :: Otel.Trace.SpanContext.t(),
  link :: Otel.Trace.Link.t()
) :: :ok
```

Adds a link to another span after creation.

# `end_span`

```elixir
@spec end_span(span_ctx :: Otel.Trace.SpanContext.t(), timestamp :: non_neg_integer()) ::
  :ok
```

Ends the span.

Marks the span as `:completed` in `SpanStorage` (status flip
+ `end_time` stamp). `SpanExporter` picks it up on the next
timer tick.

# `get_context`

```elixir
@spec get_context(span_ctx :: Otel.Trace.SpanContext.t()) ::
  Otel.Trace.SpanContext.t()
```

Returns the SpanContext as-is. Identity function — on BEAM the
SpanContext is itself the handle, satisfying spec `trace/api.md`
L461 *"returned value MUST be the same for the entire Span
lifetime"* automatically by value semantics.

# `new`

```elixir
@spec new(opts :: map()) :: t()
```

**SDK** — Construct a Span. Caller provides identifying
fields (`trace_id`, `span_id`, `name`) via `opts`; the
remaining fields default to spec-aligned zero values.

Used by `Otel.Trace.Span.start_span/4` to build the SDK
Span after sampling and by tests / fixtures that need
partially-filled spans.

# `record_exception`

```elixir
@spec record_exception(
  span_ctx :: Otel.Trace.SpanContext.t(),
  exception :: Exception.t(),
  stacktrace :: list(),
  attributes :: %{required(String.t()) =&gt; primitive_any()}
) :: :ok
```

Records an exception as an event on the span.

# `recording?`

```elixir
@spec recording?(span_ctx :: Otel.Trace.SpanContext.t()) :: boolean()
```

Returns whether the span is currently recording.

# `set_attribute`

```elixir
@spec set_attribute(
  span_ctx :: Otel.Trace.SpanContext.t(),
  key :: String.t(),
  value :: primitive_any()
) :: :ok
```

Sets a single attribute on the span.

# `set_attributes`

```elixir
@spec set_attributes(
  span_ctx :: Otel.Trace.SpanContext.t(),
  attributes ::
    %{required(String.t()) =&gt; primitive_any()} | [{String.t(), primitive_any()}]
) :: :ok
```

Sets multiple attributes on the span.

# `set_status`

```elixir
@spec set_status(
  span_ctx :: Otel.Trace.SpanContext.t(),
  status :: Otel.Trace.Status.t()
) :: :ok
```

Sets the status of the span.

Status priority: Ok > Error > Unset. Once set to :ok, status is final.
Setting :unset is always ignored.

# `start_span`

```elixir
@spec start_span(
  ctx :: Otel.Ctx.t(),
  name :: String.t(),
  span_limits :: Otel.Trace.SpanLimits.t(),
  opts :: start_opts()
) :: {Otel.Trace.SpanContext.t(), t() | nil}
```

Creates a span following the SDK creation flow (spec L339).

Returns `{span_ctx, span | nil}` where span is nil for dropped spans.

# `update_name`

```elixir
@spec update_name(span_ctx :: Otel.Trace.SpanContext.t(), name :: String.t()) :: :ok
```

Updates the name of the span.

---

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