Otel.Trace.Span (otel v0.4.1)

Copy Markdown View Source

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

CallbackRole
start_span/4SDK (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

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

Summary

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.

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()}

start_opts()

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

  • :kindOtel.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()

@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

add_event(span_context, event)

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

Adds an event to the span.

add_link(span_context, link)

@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(span_ctx, timestamp \\ System.system_time(:nanosecond))

@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(span_ctx)

@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(opts \\ %{})

@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(span_ctx, exception, stacktrace \\ [], attributes \\ %{})

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

recording?(span_context)

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

Returns whether the span is currently recording.

set_attribute(span_context, key, value)

@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(span_context, new_attributes)

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

set_status(span_context, status)

@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(ctx, name, span_limits, opts)

@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(span_context, name)

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

Updates the name of the span.