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(protoSpanfield 10)Otel.Trace.Span.dropped_events_count(protoSpanfield 12)Otel.Trace.Span.dropped_links_count(protoSpanfield 14)Otel.Trace.Event.dropped_attributes_count(protoSpan.Eventfield 4)Otel.Trace.Link.dropped_attributes_count(protoSpan.Linkfield 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.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
Types
Options accepted by Otel.Trace.Tracer.start_span/3.
Functions
Adds an event to the span.
Adds a link to another span after creation.
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.
SDK — Construct a Span. Caller provides identifying
fields (trace_id, span_id, name) via opts; the
remaining fields default to spec-aligned zero values.
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 start_opts() :: [ kind: Otel.Trace.SpanKind.t(), attributes: %{required(String.t()) => 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—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.
@type t() :: %Otel.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.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() }
Functions
@spec add_event( span_ctx :: Otel.Trace.SpanContext.t(), event :: Otel.Trace.Event.t() ) :: :ok
Adds an event to the span.
@spec add_link( span_ctx :: Otel.Trace.SpanContext.t(), link :: Otel.Trace.Link.t() ) :: :ok
Adds a link to another span after creation.
@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_timestamp).SpanExporterpicks it up on the next timer tick.
@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.
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.
@spec record_exception( span_ctx :: Otel.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.Trace.SpanContext.t()) :: boolean()
Returns whether the span is currently recording.
@spec set_attribute( span_ctx :: Otel.Trace.SpanContext.t(), key :: String.t(), value :: primitive_any() ) :: :ok
Sets a single attribute on the span.
@spec set_attributes( span_ctx :: Otel.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.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.
@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.
@spec update_name(span_ctx :: Otel.Trace.SpanContext.t(), name :: String.t()) :: :ok
Updates the name of the span.