Quick start
# mix.exs
{:otel, "~> 0.1"}Otel.Trace.with_span("checkout", fn _span_ctx ->
process_order()
end)The SDK ships traces to http://localhost:4318/v1/traces by default;
override the endpoint via :req_options (see
README).
Minikube hardcodes the instrumentation scope to the SDK identity
(name: "otel", version: <SDK vsn>) — there is no Tracer
handle to obtain via get_tracer/0 first; call Otel.Trace
functions directly.
with_span/4 — automatic lifecycle
The recommended form. Starts the span, makes it the current span for the block, ends it on exit, and records any exception that escapes.
Otel.Trace.with_span("checkout", fn span_ctx ->
process_order()
end)With options:
Otel.Trace.with_span(
"checkout",
[
kind: :server,
attributes: %{"user.id" => 42}
],
fn span_ctx ->
process_order()
end
)Manual lifecycle
For spans that don't fit a single function scope.
span_ctx = Otel.Trace.start_span("checkout", kind: :server)
# … do work, possibly across processes / messages …
Otel.Trace.Span.end_span(span_ctx)Attributes
Otel.API.Trace.Span.set_attribute(span_ctx, "http.method", "GET")
Otel.API.Trace.Span.set_attributes(span_ctx, %{
"http.status_code" => 200,
"http.url" => "/orders/42"
})Events
event = Otel.API.Trace.Event.new("cart.validated", %{"item.count" => 3})
Otel.API.Trace.Span.add_event(span_ctx, event)Links
linked_ctx = Otel.API.Trace.SpanContext.new(trace_id, span_id, 1)
link = %Otel.API.Trace.Link{context: linked_ctx, attributes: %{"reason" => "fork"}}
Otel.API.Trace.Span.add_link(span_ctx, link)Status
Otel.API.Trace.Span.set_status(span_ctx, Otel.API.Trace.Status.new(:ok))
Otel.API.Trace.Span.set_status(
span_ctx,
Otel.API.Trace.Status.new(:error, "payment declined")
)with_span/4 automatically sets :error status and records any
exception that escapes the function — manual set_status(:error) is
only needed when the operation failed without raising.
Exceptions
try do
process_order()
rescue
exception ->
Otel.API.Trace.Span.record_exception(span_ctx, exception, __STACKTRACE__)
reraise exception, __STACKTRACE__
endwith_span/4 does this automatically; reach for record_exception/3,4
only inside manual lifecycle code or when recording without re-raising.
Update name
Otel.API.Trace.Span.update_name(span_ctx, "checkout (premium tier)")Span kinds
| Kind | Use for |
|---|---|
:internal (default) | in-process work |
:server | inbound RPC / HTTP server |
:client | outbound RPC / HTTP client |
:producer | message produced (Kafka, RabbitMQ, …) |
:consumer | message consumed |
Pass via kind: option on start_span / with_span.
Across processes (Task, GenServer.cast, …)
BEAM processes don't inherit the parent's process dictionary, so the
current span context doesn't follow Task.async/spawn automatically.
Capture and re-attach explicitly:
Otel.Trace.with_span("parent", fn _ ->
ctx = Otel.Ctx.current()
Task.async(fn ->
Otel.Ctx.attach(ctx)
Otel.Trace.with_span("child", fn _ -> :work end)
end)
end)Across services (HTTP / RPC)
Inject the active context into outgoing request headers; extract on the server side. The SDK is hardcoded to W3C TraceContext + W3C Baggage — no other propagators (B3, Jaeger, etc.) are shipped.
Outbound (client)
ctx = Otel.Ctx.current()
headers = Otel.API.Propagator.TextMap.inject(ctx, %{})
# => %{"traceparent" => "00-...-...", "tracestate" => "..."}
HTTPClient.post("https://api.example.com/orders", body, headers)Inbound (server)
ctx = Otel.API.Propagator.TextMap.extract(Otel.Ctx.new(), conn.req_headers)
Otel.Ctx.attach(ctx)
Otel.Trace.with_span("POST /orders", [kind: :server], fn _ ->
handle_request()
end)Baggage
Baggage propagates name-value pairs alongside the span context — visible to downstream services but not auto-attached as span attributes.
ctx =
Otel.Ctx.current()
|> Otel.API.Baggage.set_value("tenant.id", "acme")
|> Otel.API.Baggage.set_value("feature.flag", "fast-checkout")
Otel.Ctx.attach(ctx)Reading on the receiving side:
ctx = Otel.API.Propagator.TextMap.extract(Otel.Ctx.new(), headers)
{value, _metadata} = Otel.API.Baggage.get_value(Otel.API.Baggage.current(ctx), "tenant.id")
# value => "acme"Sampling
Hardcoded parentbased_always_on — root spans always sample, children
inherit the parent's decision (sampled flag set → record; not set →
drop). Not configurable.
For finer control (head ratio sampling, custom samplers), use
opentelemetry-erlang.
For tail sampling (latency / error / rate), configure your collector's
tail_sampling_processor.
Limits
Hardcoded to spec defaults: 128 attributes / events / links per span, no string-length truncation. Not user-configurable.