SDK implementation of the Otel.API.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.API.Trace.Span dispatches to the functions
defined here via the Otel.API.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/6 | 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 and processors_key
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.
processors_key is the :persistent_term key under which
the TracerProvider published the projected processor list.
end_span/2 reads from that key fresh — so if a processor
crashed between start and end, the TracerProvider's EXIT
handler has already removed it from the persistent_term
list and on_end/2 skips it. Mirrors the Logger.emit
pattern (Otel.SDK.Logs.Logger).
This diverges from opentelemetry-erlang, which threads
limits through otel_span_utils per call
(opentelemetry/src/otel_span_utils.erl) and stores
processors on the span_ctx.span_sdk tuple
(otel_span_ets.erl L60, L77).
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.SDK.Trace.Span.dropped_attributes_count(protoSpanfield 10)Otel.SDK.Trace.Span.dropped_events_count(protoSpanfield 12)Otel.SDK.Trace.Span.dropped_links_count(protoSpanfield 14)Otel.SDK.Trace.Event.dropped_attributes_count(protoSpan.Eventfield 4)Otel.SDK.Trace.Link.dropped_attributes_count(protoSpan.Linkfield 5)
Otel.SDK.Trace.Event and Otel.SDK.Trace.Link are SDK
wrapper structs constructed from the API-layer
Otel.API.Trace.Event / Otel.API.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/6 (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.
is_recording, instrumentation_scope on the span
Both fields exist on the span (lines 34-35) but neither
appears in the proto Span message. They mirror erlang's
otel_span.hrl (L60, L62) — is_recording is an
implementation optimization not propagated to wire format,
and instrumentation_scope is held on the span for
grouping into ScopeSpans at export time.
References
- OTel Trace SDK §Span:
opentelemetry-specification/specification/trace/sdk.mdL692-L944 - OTel Trace API §Span:
opentelemetry-specification/specification/trace/api.mdL449-L705 - OTLP proto Span:
opentelemetry-proto/opentelemetry/proto/trace/v1/trace.proto
Summary
Functions
Adds an event to the span.
Adds a link to another span after creation.
Ends the span.
Records an exception as an event on the span.
Returns whether the span is currently recording.
Sets a single attribute on the span.
Sets multiple attributes on the span.
Sets the status of the span.
Creates a span following the SDK creation flow (spec L339).
Updates the name of the span.
Types
@type primitive_any() :: primitive() | [primitive_any()] | %{required(String.t()) => primitive_any()}
@type t() :: %Otel.SDK.Trace.Span{ attributes: %{required(String.t()) => 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.SDK.Trace.Event.t()], instrumentation_scope: Otel.API.InstrumentationScope.t() | nil, is_recording: boolean(), kind: Otel.API.Trace.SpanKind.t(), links: [Otel.SDK.Trace.Link.t()], name: String.t(), parent_span_id: Otel.API.Trace.SpanId.t() | nil, parent_span_is_remote: boolean() | nil, processors_key: term() | nil, span_id: Otel.API.Trace.SpanId.t(), span_limits: Otel.SDK.Trace.SpanLimits.t(), start_time: non_neg_integer(), status: Otel.API.Trace.Status.t(), trace_flags: Otel.API.Trace.SpanContext.trace_flags(), trace_id: Otel.API.Trace.TraceId.t(), tracestate: Otel.API.Trace.TraceState.t() }
Functions
@spec add_event( span_ctx :: Otel.API.Trace.SpanContext.t(), event :: Otel.API.Trace.Event.t() ) :: :ok
Adds an event to the span.
@spec add_link( span_ctx :: Otel.API.Trace.SpanContext.t(), link :: Otel.API.Trace.Link.t() ) :: :ok
Adds a link to another span after creation.
@spec end_span( span_ctx :: Otel.API.Trace.SpanContext.t(), timestamp :: non_neg_integer() ) :: :ok
Ends the span.
Removes the span from ETS, sets end_time and is_recording=false, then calls on_end on all processors.
@spec record_exception( span_ctx :: Otel.API.Trace.SpanContext.t(), exception :: Exception.t(), stacktrace :: list(), attributes :: %{required(String.t()) => primitive_any()} ) :: :ok
Records an exception as an event on the span.
@spec recording?(span_ctx :: Otel.API.Trace.SpanContext.t()) :: boolean()
Returns whether the span is currently recording.
@spec set_attribute( span_ctx :: Otel.API.Trace.SpanContext.t(), key :: String.t(), value :: primitive_any() ) :: :ok
Sets a single attribute on the span.
@spec set_attributes( span_ctx :: Otel.API.Trace.SpanContext.t(), attributes :: %{required(String.t()) => primitive_any()} | [{String.t(), primitive_any()}] ) :: :ok
Sets multiple attributes on the span.
@spec set_status( span_ctx :: Otel.API.Trace.SpanContext.t(), status :: Otel.API.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.
@spec start_span( ctx :: Otel.API.Ctx.t(), name :: String.t(), sampler :: Otel.SDK.Trace.Sampler.t(), id_generator :: module(), span_limits :: Otel.SDK.Trace.SpanLimits.t(), opts :: Otel.API.Trace.Span.start_opts() ) :: {Otel.API.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.
@spec update_name(span_ctx :: Otel.API.Trace.SpanContext.t(), name :: String.t()) :: :ok
Updates the name of the span.