lib/opentelemetry_process_propagator.ex

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