lib/appsignal/instrumentation.ex

defmodule Appsignal.Instrumentation do
  require Appsignal.Utils

  @tracer Appsignal.Utils.compile_env(:appsignal, :appsignal_tracer, Appsignal.Tracer)
  @span Appsignal.Utils.compile_env(:appsignal, :appsignal_span, Appsignal.Span)

  @spec instrument(function()) :: any()
  @doc false
  def instrument(fun) do
    span = @tracer.create_span("background_job", @tracer.current_span)

    result = call_with_optional_argument(fun, span)
    @tracer.close_span(span)

    result
  end

  @spec instrument(String.t(), function()) :: any()
  @doc """
  Instrument a function.

      def call do
        Appsignal.instrument("foo.bar", fn ->
          :timer.sleep(1000)
        end)
      end

  When passing a function that takes an argument, the function is called with
  the created span to allow adding extra information.

      def call(params) do
        Appsignal.instrument("foo.bar", fn span ->
          Appsignal.Span.set_sample_data(span, "params", params)
          :timer.sleep(1000)
        end)
      end

  """
  def instrument(name, fun) do
    instrument(name, name, fun)
  end

  @spec instrument(String.t(), String.t(), function()) :: any()
  @doc """
  Instrument a function, and set the `"appsignal:category"` attribute to the
  value passed as the `category` argument.
  """
  def instrument(name, category, fun) do
    instrument(fn span ->
      _ =
        span
        |> @span.set_name(name)
        |> @span.set_attribute("appsignal:category", category)

      call_with_optional_argument(fun, span)
    end)
  end

  @deprecated "Use Appsignal.instrument/3 instead."
  def instrument(_, name, category, fun) do
    instrument(name, category, fun)
  end

  @spec instrument_root(String.t(), String.t(), function()) :: any()
  @doc false
  def instrument_root(namespace, name, fun) do
    span = @tracer.create_span(namespace, nil)

    span
    |> @span.set_name(name)
    |> @span.set_attribute("appsignal:category", name)

    result = fun.()

    @tracer.close_span(span)

    result
  end

  @spec set_error(Exception.t(), Exception.stacktrace()) :: Appsignal.Span.t() | nil
  @doc """
  Set an error in the current root span.
  """
  def set_error(%_{__exception__: true} = exception, stacktrace) do
    @span.add_error(@tracer.root_span(), exception, stacktrace)
  end

  @spec set_error(Exception.kind(), any(), Exception.stacktrace()) :: Appsignal.Span.t() | nil
  @doc """
  Set an error in the current root span by passing a `kind` and `reason`.
  """
  def set_error(kind, reason, stacktrace) do
    @span.add_error(@tracer.root_span(), kind, reason, stacktrace)
  end

  @spec send_error(Exception.t(), Exception.stacktrace()) :: Appsignal.Span.t() | nil
  @doc """
  Send an error in a newly created `Appsignal.Span`.
  """
  def send_error(%_{__exception__: true} = exception, stacktrace) do
    send_error(exception, stacktrace, & &1)
  end

  @spec send_error(Exception.t(), Exception.stacktrace(), function()) :: Appsignal.Span.t() | nil
  @doc """
  Send an error in a newly created `Appsignal.Span`. Calls the passed function
  with the created `Appsignal.Span` before closing it.
  """
  def send_error(%_{__exception__: true} = exception, stacktrace, fun) when is_function(fun) do
    @span.create_root("http_request", self())
    |> @span.add_error(exception, stacktrace)
    |> fun.()
    |> @span.close()
  end

  @spec send_error(Exception.kind(), any(), Exception.stacktrace()) :: Appsignal.Span.t() | nil
  def send_error(kind, reason, stacktrace) do
    send_error(kind, reason, stacktrace, & &1)
  end

  def send_error(kind, reason, stacktrace, fun) do
    @span.create_root("http_request", self())
    |> @span.add_error(kind, reason, stacktrace)
    |> fun.()
    |> @span.close()
  end

  defp call_with_optional_argument(fun, _argument) when is_function(fun, 0), do: fun.()
  defp call_with_optional_argument(fun, argument) when is_function(fun, 1), do: fun.(argument)
end