defmodule OpentelemetryProcessPropagator do
@moduledoc """
`OpentelemetryProcessPropagator` provides helpers for dealing
with context propagation across process boundaries.
## Context Propagation
Erlang and Elixir do not have a mechanism for transparently passing
context between processes. This requires the user to explicitly
pass data between processes. In order to continue a trace across
processes, the user must start a new span and pass it to the
spawned process.
```
span_ctx = OpenTelemetry.Tracer.start_span("child")
ctx = OpenTelemetry.Ctx.get_current()
task =
Task.async(fn ->
OpenTelemetry.Ctx.attach(ctx)
OpenTelemetry.Tracer.set_current_span(span_ctx)
# do work here
OpenTelemetry.Tracer.end_span(span_ctx)
end)
_result = Task.await(task)
```
### Reverse Propagation
It's not always possible to have full control over traces, such as
when using `telemetry` events emitted from a library you don't control
to create a span. In such cases, a mechanism to fetch a context from a
calling process is necessary. This is effectively context propagation
in reverse.
As an example, Ecto uses the `Task` module to execute preloads which are
each a separate query. Since a task is a spawned process, creating an otel
span results in orphan spans. To correctly connect these spans we must
find the otel context which spawned the process.
## Usage
Example of using `fetch_parent_ctx/1` to find a parent context.
```elixir
OpenTelemetry.with_span :span_started_in_your_app do
# some span being created in a process spawned by a library
# you don't control, e.g. Ecto preloads
Task.async(fn ->
parent_ctx = OpentelemetryProcessPropagator.fetch_parent_ctx(:"$callers")
OpenTelemetry.Ctx.attach(parent_ctx)
attrs = %{some_attr: :from_telemetry_event}
span =
OpenTelemetry.Tracer.start_span(:span_created_in_lib, %{attributes: attrs})
OpenTelemetry.Span.end_span(span)
end)
end
```
```erlang
?with_span(span_started_in_your_app, #{}, fun() ->
%% some span being created in a process spawned by a library
%% you don't control
proc_lib:spawn_link(fun() ->
Tracer = opentelemetry:get_tracer(test_tracer),
ParentCtx = opentelemetry_process_propagator:fetch_parent_ctx(),
otel_ctx:attach(ParentCtx),
Span = otel_tracer:start_span(Tracer, span_created_in_lib, #{}),
otel_tracer:end_span(Span).
).
end
```
"""
@doc """
Attempt to fetch an otel context from a give pid.
"""
@spec fetch_ctx(pid) :: OpenTelemetry.span_ctx() | :undefined
defdelegate fetch_ctx(pid), to: :opentelemetry_process_propagator
@doc """
Attempt to find an otel context in the spawning process.
This is equivalent to calling `fetch_parent_ctx(1, :"$ancestors")`
"""
@spec fetch_parent_ctx() :: OpenTelemetry.span_ctx() | :undefined
defdelegate fetch_parent_ctx(), to: :opentelemetry_process_propagator
@doc """
Attempt to find an otel context in a spawning process within `n` number of parent
processes
"""
@spec fetch_parent_ctx(non_neg_integer()) :: OpenTelemetry.span_ctx() | :undefined
defdelegate fetch_parent_ctx(depth), to: :opentelemetry_process_propagator
@doc """
Attempt to find an otel context under a given process dictionary key
within `n` number of parent processes. The first context found will be
returned.
Processes spawned by `proc_lib` are stored under `:"$ancestors`. The
Elixir `Task` module uses the `:"$callers` key.
"""
@spec fetch_parent_ctx(non_neg_integer(), atom()) :: OpenTelemetry.span_ctx() | :undefined
defdelegate fetch_parent_ctx(max_depth, key), to: :opentelemetry_process_propagator
end