defmodule Ash.Tracer do @moduledoc """ A behaviour for implementing tracing for an Ash application. """ @type span_type() :: :action | :changeset | :query | :calculate | :request_step | :change | :validation | :preparation | :custom | :notifier | :before_transaction | :before_action | :after_transaction | :after_action | {:custom, atom()} @type metadata() :: %{ domain: nil | module(), resource: nil | module(), actor: term(), tenant: term(), action: atom(), authorize?: boolean() } @type t :: module @callback start_span(span_type(), name :: String.t()) :: :ok @callback stop_span() :: :ok @callback get_span_context() :: term() @callback set_span_context(term()) :: :ok @callback set_error(Exception.t(), Keyword.t()) :: :ok @callback trace_type?(atom) :: boolean() @callback set_handled_error(Exception.t(), Keyword.t()) :: :ok @doc """ Set metadata for the current span. This may be called multiple times per span, and should ideally merge with previous metadata. """ @callback set_metadata(span_type(), metadata()) :: :ok @callback set_error(Exception.t()) :: :ok @optional_callbacks set_error: 2, set_error: 1, trace_type?: 1, set_handled_error: 2 defmacro span(type, name, tracer, block_opts \\ []) do quote generated: true do type = unquote(type) name = unquote(name) tracer = List.wrap(unquote(tracer)) tracer = Enum.filter(tracer, &Ash.Tracer.trace_type?(&1, type)) # no need to use try/rescue/after if no tracers if Enum.empty?(tracer) do unquote(block_opts[:do]) else Ash.Tracer.start_span(tracer, type, name) try do unquote(block_opts[:do]) rescue e -> Ash.Tracer.set_error(tracer, e, stacktrace: __STACKTRACE__) reraise e, __STACKTRACE__ after Ash.Tracer.stop_span(tracer) end end end end defmacro telemetry_span(name, metadata, opts \\ [], block_opts) do quote generated: true do if unquote(opts[:skip?]) do unquote(block_opts[:do]) else telemetry_name = unquote(name) metadata = unquote(metadata) compiling? = Code.can_await_module_compilation?() metadata = if !compiling? do case :telemetry.list_handlers(telemetry_name) do [] -> %{} _ when is_function(metadata) -> apply(metadata, []) _ -> metadata end end start = System.monotonic_time() if !compiling? do :telemetry.execute( telemetry_name ++ [:start], %{system_time: System.system_time()}, metadata ) end try do unquote(block_opts[:do]) after duration = System.monotonic_time() - start if !compiling? do :telemetry.execute( telemetry_name ++ [:stop], %{system_time: System.system_time(), duration: duration}, metadata ) end end end end end def stop_span(nil), do: :ok def stop_span(tracers) when is_list(tracers) do Enum.each(tracers, &stop_span/1) end def stop_span(tracer) do tracer.stop_span() end def trace_type?(tracer, type) do if function_exported?(tracer, :trace_type?, 1) do tracer.trace_type?(type) else true end end def start_span(nil, _type, _name), do: :ok def start_span(tracers, type, name) when is_list(tracers) do name = resolve_lazy(name) Enum.each(tracers, &start_span(&1, type, name)) end def start_span(tracer, type, name) do name = resolve_lazy(name) tracer.start_span(type, name) end def set_handled_error(nil, _, _), do: :ok def set_handled_error(tracers, error, opts) when is_list(tracers) do Enum.each(tracers, &set_handled_error(&1, error, opts)) end def set_handled_error(tracer, error, opts) do if function_exported?(tracer, :set_handled_error, 2) do tracer.set_handled_error(error, opts) else :ok end end def set_error(nil, _, _), do: :ok def set_error(tracers, error, opts) when is_list(tracers) do Enum.each(tracers, &set_error(&1, error, opts)) end def set_error(tracer, error, opts) do if function_exported?(tracer, :set_error, 2) do tracer.set_error(error, opts) else tracer.set_error(error) end end def set_error(nil, _), do: :ok def set_error(tracers, error) when is_list(tracers) do Enum.each(tracers, &set_error(&1, error)) end def set_error(tracer, error) do if function_exported?(tracer, :set_error, 2) do tracer.set_error(error, []) else tracer.set_error(error) end end def get_span_context(nil), do: :ok def get_span_context(tracer) when is_list(tracer) do raise ArgumentError, "Cannot get span context from multiple tracers" end def get_span_context(tracer) do tracer.get_span_context() end def set_span_context(nil, _), do: :ok def set_span_context(tracer, _context) when is_list(tracer) do raise ArgumentError, "Cannot set span context from multiple tracers" end def set_span_context(tracer, context) do tracer.set_span_context(context) end def set_metadata(nil, _type, _metadata), do: :ok def set_metadata(tracers, type, metadata) when is_list(tracers) do tracers = Enum.filter(tracers, &Ash.Tracer.trace_type?(&1, type)) if !Enum.empty?(tracers) do metadata = resolve_lazy(metadata) Enum.each(tracers, & &1.set_metadata(type, metadata)) end end def set_metadata(tracer, type, metadata) do if Ash.Tracer.trace_type?(tracer, type) do metadata = resolve_lazy(metadata) tracer.set_metadata(type, metadata) end end defmacro __using__(_) do quote do @behaviour Ash.Tracer end end defp resolve_lazy(value) do if is_function(value), do: apply(value, []), else: value end end