lib/decorators.ex

if Code.ensure_loaded?(Decorator.Define) do
  defmodule Spandex.Decorators do
    @moduledoc """
    Provides a way of annotating functions to be traced.

    Span function decorators take an optional argument which is the attributes to update the span with. One of those attributes can be the `:tracer` in case you want to override the default tracer (e.g., in case you want to use multiple tracers).

    IMPORTANT If you define multiple clauses for a function, you'll have to decorate all of the ones you want to span.

    Note: Decorators don't magically do everything. It often makes a lot of sense to use `Tracer.update_span` from within your function to add details that are only available inside that same function.

        defmodule Foo do
          use Spandex.Decorators

          @decorate trace()
          def bar(a) do
            a * 2
          end

          @decorate trace(service: "ecto", type: "sql")
          def databaz(a) do
            a * 3
          end
        end
    """

    @tracer Application.get_env(:spandex, :decorators)[:tracer]

    use Decorator.Define, span: 0, span: 1, trace: 0, trace: 1

    def trace(body, context) do
      trace([], body, context)
    end

    def trace(attributes, body, context) do
      name = Keyword.get(attributes, :name, default_name(context))
      tracer = Keyword.get(attributes, :tracer, @tracer)
      attributes = Keyword.delete(attributes, :tracer)

      quote do
        require unquote(tracer)

        unquote(tracer).trace unquote(name), unquote(attributes) do
          unquote(body)
        end
      end
    end

    def span(body, context) do
      span([], body, context)
    end

    def span(attributes, body, context) do
      name = Keyword.get(attributes, :name, default_name(context))
      tracer = Keyword.get(attributes, :tracer, @tracer)

      attributes =
        attributes
        |> Keyword.delete(:tracer)
        |> Keyword.put_new(:resource, name)

      quote do
        require unquote(tracer)

        unquote(tracer).span unquote(name), unquote(attributes) do
          unquote(body)
        end
      end
    end

    defp default_name(%{module: module, name: function, arity: arity}) do
      module =
        module
        |> Atom.to_string()
        |> String.trim_leading("Elixir.")

      "#{module}.#{function}/#{arity}"
    end
  end
end