lib/opentelemetry_finch.ex

defmodule OpentelemetryFinch do
  @moduledoc """
  OpentelemetryFinch uses [telemetry](https://hexdocs.pm/telemetry/) handlers to
  create `OpenTelemetry` spans.

  ## Usage

  In your application start:

      def start(_type, _args) do
        OpentelemetryFinch.setup()

        # ...
      end

  """

  require OpenTelemetry.Tracer

  @typedoc "Setup options"
  @type opts :: []

  @doc """
  Initializes and configures the telemetry handlers.
  """
  @spec setup(opts()) :: :ok
  def setup(_opts \\ []) do
    :telemetry.attach(
      {__MODULE__, :request_stop},
      [:finch, :request, :stop],
      &__MODULE__.handle_request_stop/4,
      %{}
    )
  end

  @doc false
  def handle_request_stop(_event, measurements, meta, _config) do
    duration = measurements.duration
    end_time = :opentelemetry.timestamp()
    start_time = end_time - duration

    status =
      case meta.result do
        {:ok, response} -> response.status
        _ -> 0
      end

    url = build_url(meta.request.scheme, meta.request.host, meta.request.port, meta.request.path)

    attributes = %{
      "http.url": url,
      "http.scheme": meta.request.scheme,
      "net.peer.name": meta.request.host,
      "net.peer.port": meta.request.port,
      "http.target": meta.request.path,
      "http.method": meta.request.method,
      "http.status_code": status,
    }

    s =
      OpenTelemetry.Tracer.start_span("HTTP #{meta.request.method}", %{
        start_time: start_time,
        attributes: attributes,
        kind: :client
      })

    if meta.result |> elem(0) == :error do
      OpenTelemetry.Span.set_status(s, OpenTelemetry.status(:error, format_error(meta.result |> elem(1))))
    end

    OpenTelemetry.Span.end_span(s)
  end

  defp build_url(scheme, host, port, path), do: "#{scheme}://#{host}:#{port}#{path}"

  defp format_error(%{__exception__: true} = exception), do: Exception.message(exception)
  defp format_error(reason), do: inspect(reason)
end