Otel.SDK.Trace.Span (otel v0.2.0)

Copy Markdown View Source

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

CallbackRole
start_span/6SDK (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/2SDK (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 (proto Span field 10)
  • Otel.SDK.Trace.Span.dropped_events_count (proto Span field 12)
  • Otel.SDK.Trace.Span.dropped_links_count (proto Span field 14)
  • Otel.SDK.Trace.Event.dropped_attributes_count (proto Span.Event field 4)
  • Otel.SDK.Trace.Link.dropped_attributes_count (proto Span.Link field 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.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

Summary

Functions

Adds an event to the span.

Adds a link to another span after creation.

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

primitive()

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

primitive_any()

@type primitive_any() ::
  primitive() | [primitive_any()] | %{required(String.t()) => primitive_any()}

t()

@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

add_event(span_context, event)

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

Adds an event to the span.

add_link(span_context, link)

@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.

end_span(span_context, timestamp)

@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.

record_exception(span_ctx, exception, stacktrace, attributes)

@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.

recording?(span_context)

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

Returns whether the span is currently recording.

set_attribute(span_context, key, value)

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

Sets a single attribute on the span.

set_attributes(span_context, new_attributes)

@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.

set_status(span_context, status)

@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.

start_span(ctx, name, sampler, id_generator, span_limits, opts)

@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.

update_name(span_context, name)

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

Updates the name of the span.