defmodule Spandex.Tracer do
@moduledoc """
A module that can be used to build your own tracer.
Example:
```
defmodule MyApp.Tracer do
use Spandex.Tracer, otp_app: :my_app
end
```
"""
alias Spandex.{
Span,
SpanContext,
Trace
}
@type tagged_tuple(arg) :: {:ok, arg} | {:error, term()}
@type span_name() :: String.t()
@type opts :: Keyword.t() | :disabled
@callback configure(opts) :: :ok
@callback start_trace(span_name, opts) :: tagged_tuple(Trace.t())
@callback start_span(span_name, opts) :: tagged_tuple(Span.t())
@callback update_span(opts) :: tagged_tuple(Span.t())
@callback update_top_span(opts) :: tagged_tuple(Span.t())
@callback finish_trace(opts) :: tagged_tuple(Trace.t())
@callback finish_span(opts) :: tagged_tuple(Span.t())
@callback span_error(error :: Exception.t(), stacktrace :: [term], opts) :: tagged_tuple(Span.t())
@callback continue_trace(span_name :: String.t(), trace_context :: SpanContext.t(), opts) :: tagged_tuple(Trace.t())
@callback continue_trace_from_span(span_name, span :: term, opts) :: tagged_tuple(Trace.t())
@callback current_trace_id(opts) :: nil | Spandex.id()
@callback current_span_id(opts) :: nil | Spandex.id()
@callback current_span(opts) :: nil | Span.t()
@callback current_context(opts) ::
{:ok, SpanContext.t()}
| {:error, :disabled}
| {:error, :no_span_context}
| {:error, :no_trace_context}
| {:error, [Optimal.error()]}
@callback distributed_context(Plug.Conn.t(), opts) :: tagged_tuple(map)
@callback inject_context(Spandex.headers(), opts) :: Spandex.headers()
@macrocallback span(span_name, opts, do: Macro.t()) :: Macro.t()
@macrocallback trace(span_name, opts, do: Macro.t()) :: Macro.t()
@tracer_opts Optimal.schema(
opts: [
adapter: :atom,
service: :atom,
disabled?: :boolean,
env: :string,
service_version: :string,
services: {:keyword, :atom},
strategy: :atom,
sender: :atom,
trace_key: :atom
],
required: [:adapter, :service],
defaults: [
disabled?: false,
services: [],
strategy: Spandex.Strategy.Pdict
],
describe: [
adapter: "The third party adapter to use",
trace_key: "Don't set manually. This option is passed automatically.",
sender:
"Once a trace is complete, it is sent using this module. Defaults to the `default_sender/0` of the selected adapter",
service: "The default service name to use for spans declared without a service",
service_version: "The version of the service, used for tracking deployments.",
disabled?: "Allows for wholesale disabling a tracer",
env: "A name used to identify the environment name, e.g `prod` or `development`",
services: "A mapping of service name to the default span types.",
strategy: "The storage and tracing strategy. Currently only supports local process dictionary."
]
)
@all_tracer_opts @tracer_opts
|> Optimal.merge(
Span.span_opts(),
annotate: "Span Creation",
add_required?: false
)
|> Map.put(:extra_keys?, false)
@doc """
A schema for the opts that a tracer accepts.
#{Optimal.Doc.document(@all_tracer_opts)}
All tracer functions that take opts use this schema.
This also accepts defaults for any value that can
be given to a span.
"""
def tracer_opts(), do: @all_tracer_opts
defmacro __using__(opts) do
# credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks
quote do
@otp_app unquote(opts)[:otp_app] || raise("Must provide `otp_app` to `use Spandex.Tracer`")
@behaviour Spandex.Tracer
@opts Spandex.Tracer.tracer_opts()
@doc """
Use to create and configure a tracer.
"""
@impl Spandex.Tracer
@spec configure(Spandex.Tracer.opts()) :: :ok
def configure(opts) do
case config(opts, @otp_app) do
:disabled ->
Application.put_env(@otp_app, __MODULE__, merge_config(opts, @otp_app))
config ->
Application.put_env(@otp_app, __MODULE__, config)
end
end
@impl Spandex.Tracer
defmacro trace(name, opts \\ [], do: body) do
quote do
opts = unquote(opts)
name = unquote(name)
_ = unquote(__MODULE__).start_trace(name, opts)
try do
unquote(body)
rescue
exception ->
stacktrace = __STACKTRACE__
_ = unquote(__MODULE__).span_error(exception, stacktrace, opts)
reraise exception, stacktrace
after
_ = unquote(__MODULE__).finish_trace()
end
end
end
@impl Spandex.Tracer
defmacro span(name, opts \\ [], do: body) do
quote do
opts = unquote(opts)
name = unquote(name)
_ = unquote(__MODULE__).start_span(name, opts)
try do
unquote(body)
rescue
exception ->
stacktrace = __STACKTRACE__
_ = unquote(__MODULE__).span_error(exception, stacktrace, opts)
reraise exception, stacktrace
after
_ = unquote(__MODULE__).finish_span()
end
end
end
@impl Spandex.Tracer
def start_trace(name, opts \\ []) do
Spandex.start_trace(name, config(opts, @otp_app))
end
@impl Spandex.Tracer
def start_span(name, opts \\ []) do
Spandex.start_span(name, config(opts, @otp_app))
end
@impl Spandex.Tracer
def update_span(opts) do
Spandex.update_span(validate_update_config(opts, @otp_app))
end
@impl Spandex.Tracer
def update_top_span(opts) do
Spandex.update_top_span(validate_update_config(opts, @otp_app))
end
@impl Spandex.Tracer
def finish_trace(opts \\ []) do
opts
|> validate_update_config(@otp_app)
|> Spandex.finish_trace()
end
@impl Spandex.Tracer
def finish_span(opts \\ []) do
opts
|> validate_update_config(@otp_app)
|> Spandex.finish_span()
end
@impl Spandex.Tracer
def span_error(error, stacktrace, opts \\ []) do
Spandex.span_error(error, stacktrace, config(opts, @otp_app))
end
@impl Spandex.Tracer
def continue_trace(span_name, span_context, opts \\ [])
def continue_trace(span_name, %SpanContext{} = span_context, opts) do
Spandex.continue_trace(span_name, span_context, config(opts, @otp_app))
end
# This is just to get around the ambiguous defaults until we fully remove this API
def continue_trace(span_name, trace_id, span_id) do
continue_trace(span_name, trace_id, span_id, [])
end
@deprecated "please use continue_trace/3 instead."
def continue_trace(span_name, trace_id, span_id, opts) do
continue_trace(span_name, %SpanContext{trace_id: trace_id, parent_id: span_id}, opts)
end
@impl Spandex.Tracer
def continue_trace_from_span(span_name, span, opts \\ []) do
Spandex.continue_trace_from_span(span_name, span, config(opts, @otp_app))
end
@impl Spandex.Tracer
def current_trace_id(opts \\ []) do
Spandex.current_trace_id(config(opts, @otp_app))
end
@impl Spandex.Tracer
def current_span_id(opts \\ []) do
Spandex.current_span_id(config(opts, @otp_app))
end
@impl Spandex.Tracer
def current_span(opts \\ []) do
Spandex.current_span(config(opts, @otp_app))
end
@impl Spandex.Tracer
def current_context(opts \\ []) do
Spandex.current_context(config(opts, @otp_app))
end
@impl Spandex.Tracer
def distributed_context(conn, opts \\ []) do
Spandex.distributed_context(conn, config(opts, @otp_app))
end
@impl Spandex.Tracer
def inject_context(headers, opts \\ []) do
opts
|> current_context()
|> case do
{:ok, span_context} ->
Spandex.inject_context(headers, span_context, config(opts, @otp_app))
_ ->
headers
end
end
defp merge_config(opts, otp_app) do
otp_app
|> Application.get_env(__MODULE__)
|> Kernel.||([])
|> Keyword.merge(opts || [])
|> Optimal.validate!(@opts)
|> Keyword.put(:trace_key, __MODULE__)
end
defp config(opts, otp_app) do
config = merge_config(opts, otp_app)
if config[:disabled?] do
:disabled
else
config
end
end
defp validate_update_config(opts, otp_app) do
env = Application.get_env(otp_app, __MODULE__)
if env[:disabled?] do
:disabled
else
schema = %{@opts | defaults: [], required: []}
# TODO: We may want to have some concept of "the quintessential tracer configs"
# So that we can take those here, instead of embedding that knowledge here.
opts
|> Optimal.validate!(schema)
|> Keyword.put(:trace_key, __MODULE__)
|> Keyword.put(:strategy, env[:strategy] || Spandex.Strategy.Pdict)
|> Keyword.put(:adapter, env[:adapter])
|> Keyword.put(:sender, env[:sender])
end
end
end
end
end