diff --git a/config/config.exs b/config/config.exs index 369af7c..436f766 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,6 +8,11 @@ if Mix.env() == :test do report_dir: "reports/exunit" config :opencensus, + process_contexts: [ + Opencensus.Unstable.ProcessContext.SeqTrace, + Opencensus.Unstable.ProcessContext.ProcessDictionary, + Opencensus.Unstable.ProcessContext.ProcessDictionaryWithRecovery + ], reporters: [{Opencensus.TestSupport.SpanCaptureReporter, []}], send_interval_ms: 100 end diff --git a/lib/opencensus/trace.ex b/lib/opencensus/trace.ex index f8cfd5f..947513d 100644 --- a/lib/opencensus/trace.ex +++ b/lib/opencensus/trace.ex @@ -45,6 +45,7 @@ defmodule Opencensus.Trace do end ``` """ + defmacro with_child_span(label, attributes \\ quote(do: %{}), do: block) do line = __CALLER__.line module = __CALLER__.module @@ -60,21 +61,23 @@ defmodule Opencensus.Trace do }) quote do - parent_span_ctx = :ocp.current_span_ctx() + previous_span_ctx = Opencensus.Unstable.current_span_ctx() + parent_span_ctx = Opencensus.Unstable.recover_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.Unstable.with_span_ctx(new_span_ctx) + ^new_span_ctx = Opencensus.Unstable.current_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.Unstable.with_span_ctx(previous_span_ctx) Opencensus.Logger.set_logger_metadata() end end diff --git a/lib/opencensus/unstable.ex b/lib/opencensus/unstable.ex new file mode 100644 index 0000000..252542e --- /dev/null +++ b/lib/opencensus/unstable.ex @@ -0,0 +1,158 @@ +defmodule Opencensus.Unstable do + @moduledoc """ + Experimental higher-level API built on proposed `ot_ctx` behaviour. + """ + + @doc """ + Get the current span context. + + Uses the first configured `process_context` only to ensure the value is safe to pass to + `with_span_ctx/1` and `with_span_ctx/2` after you've finished your work. + """ + @spec current_span_ctx() :: :opencensus.span_ctx() | :undefined + def current_span_ctx do + process_contexts() + |> hd + |> get_span_ctx_via() + end + + @doc """ + Recovers the span context. + + Uses all configured `process_context`. + Results MAY be used as the parent of a new span. + Results MUST NOT be passed to `with_span_ctx/1` or `with_span_ctx/2`. + """ + @spec recover_span_ctx() :: :opencensus.span_ctx() | :undefined + def recover_span_ctx do + process_contexts() + |> Enum.find_value(:undefined, &get_span_ctx_via/1) + end + + @doc """ + Sets the span context. Replaces `:ocp.with_span_ctx/1`. + + Uses all configured `process_context`. + Returns the previous value of `current_span_ctx/0`. + """ + @spec with_span_ctx(span_ctx :: :opencensus.span_ctx() | :undefined) :: + :opencensus.span_ctx() | :undefined + def with_span_ctx(span_ctx) do + return_span_ctx = current_span_ctx() + process_contexts() |> Enum.each(&put_span_ctx_via(&1, span_ctx)) + return_span_ctx + end + + defp get_span_ctx_via(module) do + apply(module, :get, [span_ctx_key()]) + |> case do + nil -> :undefined + span_ctx -> span_ctx + end + end + + defp put_span_ctx_via(module, value) do + apply(module, :with_value, [span_ctx_key(), value]) + end + + @spec span_ctx_key() :: atom() + defp span_ctx_key do + Application.get_env(:opencensus, :span_ctx_key, :oc_span_ctx_key) + end + + @spec process_contexts() :: list(module()) + defp process_contexts do + Application.get_env(:opencensus, :process_contexts, [ + Opencensus.Unstable.ProcessContext.SeqTrace, + Opencensus.Unstable.ProcessContext.ProcessDictionary, + Opencensus.Unstable.ProcessContext.ProcessDictionaryWithRecovery + ]) + end +end + +defmodule Opencensus.Unstable.ProcessContext do + @moduledoc "Abstraction over process-local storage." + + @doc "Get a value." + @callback get(key :: atom()) :: any() | nil + + @doc "Put a value." + @callback with_value(key :: atom, value :: any()) :: :ok +end + +defmodule Opencensus.Unstable.ProcessContext.SeqTrace do + @moduledoc """ + Process-local storage using `seq_trace`. + + Shares well with any other use that maintains a namespace in the second element of a 2-tuple + `{:shared_label, _map}`. Otherwise leaves the trace label alone to avoid disrupting the other + usage. + """ + + @behaviour Opencensus.Unstable.ProcessContext + + @doc "Get a value from the shared `seq_trace` label." + @impl Opencensus.Unstable.ProcessContext + def get(key) do + case :seq_trace.get_token(:label) do + {:label, {:shared_label, %{^key => value}}} -> + value + + _ -> + nil + end + end + + @doc "Put a value to the shared `seq_trace` label if safe." + @impl Opencensus.Unstable.ProcessContext + def with_value(key, value) do + case :seq_trace.get_token(:label) do + [] -> + :seq_trace.set_token(:label, {:shared_label, %{key => value}}) + + {:label, {:shared_label, map}} when is_map(map) -> + :seq_trace.set_token(:label, {:shared_label, Map.put(map, key, value)}) + + _ -> + nil + end + + :ok + end +end + +defmodule Opencensus.Unstable.ProcessContext.ProcessDictionary do + @moduledoc """ + Process-local storage using the process dictionary. + """ + + @behaviour Opencensus.Unstable.ProcessContext + + @doc "Get a value from the process dictionary." + @impl Opencensus.Unstable.ProcessContext + def get(key), do: Process.get(key) + + @impl Opencensus.Unstable.ProcessContext + def with_value(key, value) do + Process.put(key, value) + :ok + end +end + +defmodule Opencensus.Unstable.ProcessContext.ProcessDictionaryWithRecovery do + @moduledoc """ + Process-local storage using the process dictionary. + """ + + @behaviour Opencensus.Unstable.ProcessContext + + @doc "Get a value from the process dictionary." + @impl Opencensus.Unstable.ProcessContext + def get(key) do + [self() | Process.get(:"$callers", [])] + |> Enum.find_value(fn pid -> pid |> Process.info() |> get_in([:dictionary, key]) end) + end + + @impl Opencensus.Unstable.ProcessContext + defdelegate with_value(key, value), to: Opencensus.Unstable.ProcessContext.ProcessDictionary +end diff --git a/test/opencensus_test.exs b/test/opencensus_test.exs index e4b8c04..01fd336 100644 --- a/test/opencensus_test.exs +++ b/test/opencensus_test.exs @@ -11,12 +11,12 @@ defmodule OpencensusTest do on_exit(make_ref(), &detach/0) assert Logger.metadata() == [] - assert :ocp.current_span_ctx() == :undefined + assert Opencensus.Unstable.current_span_ctx() == :undefined with_child_span "child_span" do :do_something - assert :ocp.current_span_ctx() != :undefined + assert Opencensus.Unstable.current_span_ctx() != :undefined assert Logger.metadata() |> Keyword.keys() |> Enum.sort() == [ :span_id, diff --git a/test/opencensus_trace_async_test.exs b/test/opencensus_trace_async_test.exs index e4bdc0b..681e3ba 100644 --- a/test/opencensus_trace_async_test.exs +++ b/test/opencensus_trace_async_test.exs @@ -7,15 +7,15 @@ defmodule Opencensus.AsyncTest do alias Opencensus.Trace test "Trace.async/1" do - assert :ocp.current_span_ctx() == :undefined + assert Opencensus.Unstable.current_span_ctx() == :undefined {inner, outer} = Trace.with_child_span "outside" do - outer = :ocp.current_span_ctx() |> Span.load() + outer = Opencensus.Unstable.current_span_ctx() |> Span.load() Trace.async(fn -> Trace.with_child_span "inside" do - inner = :ocp.current_span_ctx() |> Span.load() + inner = Opencensus.Unstable.current_span_ctx() |> Span.load() {inner, outer} end end) @@ -30,19 +30,19 @@ defmodule Opencensus.AsyncTest do defmodule M do def f(outer) do Trace.with_child_span "inside" do - inner = :ocp.current_span_ctx() |> Span.load() + inner = Opencensus.Unstable.current_span_ctx() |> Span.load() {inner, outer} end end end test "Trace.async/3" do - assert :ocp.current_span_ctx() == :undefined + assert Opencensus.Unstable.current_span_ctx() == :undefined {inner, outer} = Trace.with_child_span "outside" do - outer = :ocp.current_span_ctx() |> Span.load() - Trace.async(M, :f, [outer]) |> Trace.await(10) + outer = Opencensus.Unstable.current_span_ctx() |> Span.load() + M |> Trace.async(:f, [outer]) |> Trace.await(10) end assert inner.trace_id == outer.trace_id