defmodule Opencensus.Trace do
@moduledoc """
Macros to help Elixir programmers use OpenCensus tracing.
"""
@doc """
Wrap the given block in a child span with the given label/name and optional attributes.
Sets `Logger.metadata/0` with `Opencensus.Logger.set_logger_metadata/0` after changing the span
context tracked in the process dictionary.
No attributes:
```elixir
with_child_span "child_span" do
:do_something
end
with_child_span "child_span", %{} do
:do_something
end
```
Custom attributes:
```elixir
with_child_span "child_span", [:module, :function, %{"custom_id" => "xxx"}] do
:do_something
end
```
Automatic insertion of the `module`, `file`, `line`, or `function`:
```elixir
with_child_span "child_span", [:module, :function, %{}] do
:do_something
end
```
Collapsing multiple attribute maps (last wins):
```elixir
with_child_span "child_span", [:function, %{"a" => "b", "c" => "d"}, %{"c" => "e"}] do
:do_something
end
```
"""
defmacro with_child_span(label, attributes \\ quote(do: %{}), do: block) do
line = __CALLER__.line
module = __CALLER__.module
file = __CALLER__.file
function = format_function(__CALLER__.function)
computed_attributes =
compute_attributes(attributes, %{
line: line,
module: module,
file: file,
function: function
})
quote do
parent_span_ctx = :ocp.current_span_ctx()
new_span_ctx =
:oc_trace.start_span(unquote(label), parent_span_ctx, %{
:attributes => unquote(computed_attributes)
})
_ = :ocp.with_span_ctx(new_span_ctx)
Opencensus.Logger.set_logger_metadata()
try do
unquote(block)
after
_ = :oc_trace.finish_span(new_span_ctx)
_ = :ocp.with_span_ctx(parent_span_ctx)
Opencensus.Logger.set_logger_metadata()
end
end
end
defp compute_attributes(attributes, default_attributes) when is_list(attributes) do
{atoms, custom_attributes} = Enum.split_with(attributes, &is_atom/1)
default_attributes = compute_default_attributes(atoms, default_attributes)
case Enum.split_with(custom_attributes, fn
## map ast
{:%{}, _, _} -> true
_ -> false
end) do
{[ca_map | ca_maps], []} ->
## custom attributes are literal maps, merge 'em
{:%{}, meta, custom_attributes} =
List.foldl(ca_maps, ca_map, fn {:%{}, _, new_pairs}, {:%{}, meta, old_pairs} ->
{:%{}, meta,
:maps.to_list(:maps.merge(:maps.from_list(old_pairs), :maps.from_list(new_pairs)))}
end)
{:%{}, meta,
:maps.to_list(:maps.merge(:maps.from_list(custom_attributes), default_attributes))}
{_ca_maps, _other_calls} ->
[f_ca | r_ca] = custom_attributes
quote do
unquote(
List.foldl(r_ca ++ [Macro.escape(default_attributes)], f_ca, fn ca, acc ->
quote do
Map.merge(unquote(acc), unquote(ca))
end
end)
)
end
end
end
defp compute_attributes(attributes, _default_attributes) do
attributes
end
defp compute_default_attributes(atoms, default_attributes) do
List.foldl(atoms, %{}, fn
:default, _acc ->
default_attributes
atom, acc ->
Map.put(acc, atom, Map.fetch!(default_attributes, atom))
end)
end
defp format_function(nil), do: nil
defp format_function({name, arity}), do: "#{name}/#{arity}"
@doc """
Drop-in replacement for `Task.async/1` that propagates the process' span context.
Does NOT start a new span for what's inside. Consider `with_child_span/3`.
"""
@spec async((() -> any())) :: Task.t()
def async(fun) when is_function(fun, 0) do
async(:erlang, :apply, [fun, []])
end
@doc """
Drop-in replacement for `Task.async/3` that propagates the process' span context.
Does NOT start a new span for what's inside. Consider `with_child_span/3`.
"""
@spec async(module(), atom(), [term()]) :: Task.t()
def async(module, function_name, args)
when is_atom(module) and is_atom(function_name) and is_list(args) do
original_span_ctx = :ocp.current_span_ctx()
wrapper = fn ->
:ocp.with_span_ctx(original_span_ctx)
apply(module, function_name, args)
end
Task.async(wrapper)
end
@doc """
Drop-in replacement for `Task.await/2`.
"""
@spec await(Task.t(), :infinity | pos_integer()) :: term()
defdelegate await(task, timeout \\ 5000), to: Task
end