From 7f028343c69d8a6cd57c13867c6996fc0e0c2d81 Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Wed, 4 Dec 2024 12:59:50 -0800 Subject: [PATCH 1/6] feat: setup code attributes in Elixir macros --- .../lib/open_telemetry/attributes.ex | 74 +++++++++++++++++++ .../lib/open_telemetry/tracer.ex | 45 +++++++++-- .../test/open_telemetry_test.exs | 9 +++ 3 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 apps/opentelemetry_api/lib/open_telemetry/attributes.ex diff --git a/apps/opentelemetry_api/lib/open_telemetry/attributes.ex b/apps/opentelemetry_api/lib/open_telemetry/attributes.ex new file mode 100644 index 00000000..df98553f --- /dev/null +++ b/apps/opentelemetry_api/lib/open_telemetry/attributes.ex @@ -0,0 +1,74 @@ +defmodule OpenTelemetry.Attributes do + @moduledoc """ + This module contains utility functions for span attributes. + + Elixir has built in variables like `__ENV__` and `__CALLER__` that can be used to generate + span attributes like `code.function`, `code.lineno`, and `code.namespace` either during runtime + or compile time. This module provides a function to generate these attributes from a `t:Macro.Env` + struct. + + For more information, view the [OpenTelemetry Semantic Conventions](OSC). + + [OSC]: https://opentelemetry.io/docs/specs/semconv/attributes-registry + """ + + @code_filepath :"code.filepath" + @code_function :"code.function" + @code_lineno :"code.lineno" + @code_namespace :"code.namespace" + + @doc """ + A function used to generate attributes from a `t:Macro.Env` struct. + + This function is used to generate span attributes like `code.function`, `code.lineno`, and + `code.namespace` from a `__CALLER__` variable during compile time or a `__ENV__` variable + run time. + + ## Usage + + # During run time + def my_function() do + OpenTelemetry.Attributes.from_macro_env(__ENV__) + end + + iex> my_function() + %{code_function: "my_function/0", ...} + + # During compile time in a macro + defmacro my_macro() do + attributes = + __CALLER__ + |> OpenTelemetry.Attributes.from_macro_env() + |> Macro.escape() + + quote do + unquote(attributes) + end + end + + def my_other_function() do + my_macro() + end + + iex> my_macro() + %{code_function: "my_other_function/0", ...} + + """ + @spec from_macro_env(Macro.Env.t()) :: OpenTelemetry.attributes_map() + def from_macro_env(%Macro.Env{} = env) do + function_arty = + case env.function do + {func_name, func_arity} -> "#{func_name}/#{func_arity}" + nil -> nil + end + + %{ + @code_function => function_arty, + @code_namespace => to_string(env.module), + @code_filepath => env.file, + @code_lineno => env.line + } + |> Enum.reject(fn {_k, v} -> is_nil(v) end) + |> Map.new() + end +end diff --git a/apps/opentelemetry_api/lib/open_telemetry/tracer.ex b/apps/opentelemetry_api/lib/open_telemetry/tracer.ex index e67ee33e..fd72ded5 100644 --- a/apps/opentelemetry_api/lib/open_telemetry/tracer.ex +++ b/apps/opentelemetry_api/lib/open_telemetry/tracer.ex @@ -22,11 +22,16 @@ defmodule OpenTelemetry.Tracer do The current active Span is used as the parent of the created Span. """ defmacro start_span(name, opts \\ quote(do: %{})) do - quote bind_quoted: [name: name, start_opts: opts] do + attributes = + __CALLER__ + |> OpenTelemetry.Attributes.from_macro_env() + |> Macro.escape() + + quote bind_quoted: [name: name, start_opts: opts, attributes: attributes] do :otel_tracer.start_span( :opentelemetry.get_application_tracer(__MODULE__), name, - Map.new(start_opts) + OpenTelemetry.Tracer.merge_start_opts(start_opts, attributes) ) end end @@ -37,12 +42,17 @@ defmodule OpenTelemetry.Tracer do The current active Span is used as the parent of the created Span. """ defmacro start_span(ctx, name, opts) do - quote bind_quoted: [ctx: ctx, name: name, start_opts: opts] do + attributes = + __CALLER__ + |> OpenTelemetry.Attributes.from_macro_env() + |> Macro.escape() + + quote bind_quoted: [ctx: ctx, name: name, start_opts: opts, attributes: attributes] do :otel_tracer.start_span( ctx, :opentelemetry.get_application_tracer(__MODULE__), name, - Map.new(start_opts) + OpenTelemetry.Tracer.merge_start_opts(start_opts, attributes) ) end end @@ -70,11 +80,16 @@ defmodule OpenTelemetry.Tracer do See `start_span/2` and `end_span/0`. """ defmacro with_span(name, start_opts \\ quote(do: %{}), do: block) do + attributes = + __CALLER__ + |> OpenTelemetry.Attributes.from_macro_env() + |> Macro.escape() + quote do :otel_tracer.with_span( :opentelemetry.get_application_tracer(__MODULE__), unquote(name), - Map.new(unquote(start_opts)), + OpenTelemetry.Tracer.merge_start_opts(unquote(start_opts), unquote(attributes)), fn _ -> unquote(block) end ) end @@ -88,12 +103,17 @@ defmodule OpenTelemetry.Tracer do See `start_span/2` and `end_span/0`. """ defmacro with_span(ctx, name, start_opts, do: block) do + attributes = + __CALLER__ + |> OpenTelemetry.Attributes.from_macro_env() + |> Macro.escape() + quote do :otel_tracer.with_span( unquote(ctx), :opentelemetry.get_application_tracer(__MODULE__), unquote(name), - Map.new(unquote(start_opts)), + OpenTelemetry.Tracer.merge_start_opts(unquote(start_opts), unquote(attributes)), fn _ -> unquote(block) end ) end @@ -221,4 +241,17 @@ defmodule OpenTelemetry.Tracer do def update_name(name) do :otel_span.update_name(:otel_tracer.current_span_ctx(), name) end + + @doc false + @spec merge_start_opts(OpenTelemetry.Span.start_opts(), OpenTelemetry.attributes_map()) :: + OpenTelemetry.Span.start_opts() + def merge_start_opts(start_opts, builtin_attributes) do + start_opts + |> Map.new() + |> Map.update(:attributes, builtin_attributes, fn specified_attributes -> + specified_attributes + |> Map.new(fn {k, v} -> {to_string(k), v} end) + |> Map.merge(builtin_attributes) + end) + end end diff --git a/apps/opentelemetry_api/test/open_telemetry_test.exs b/apps/opentelemetry_api/test/open_telemetry_test.exs index fefa9b06..a5de4c1b 100644 --- a/apps/opentelemetry_api/test/open_telemetry_test.exs +++ b/apps/opentelemetry_api/test/open_telemetry_test.exs @@ -144,4 +144,13 @@ defmodule OpenTelemetryTest do Ctx.detach(token) assert %{"a" => {"b", []}} = Baggage.get_all() end + + test "from_macro_env/1" do + attributes = OpenTelemetry.Attributes.from_macro_env(__ENV__) + + assert attributes[:"code.filepath"] =~ "open_telemetry_test.exs" + assert attributes[:"code.function"] =~ "from_macro_env/1" + assert attributes[:"code.lineno"] == 149 + assert attributes[:"code.namespace"] == "Elixir.OpenTelemetryTest" + end end From 1a4fff26f1083d8bb69d3be44a6ab26cc7cb4b22 Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Wed, 4 Dec 2024 18:51:05 -0800 Subject: [PATCH 2/6] fix otel tests --- .../lib/open_telemetry/tracer.ex | 6 +-- test/otel_tests.exs | 53 ++++++------------- 2 files changed, 18 insertions(+), 41 deletions(-) diff --git a/apps/opentelemetry_api/lib/open_telemetry/tracer.ex b/apps/opentelemetry_api/lib/open_telemetry/tracer.ex index fd72ded5..04b7d887 100644 --- a/apps/opentelemetry_api/lib/open_telemetry/tracer.ex +++ b/apps/opentelemetry_api/lib/open_telemetry/tracer.ex @@ -248,10 +248,6 @@ defmodule OpenTelemetry.Tracer do def merge_start_opts(start_opts, builtin_attributes) do start_opts |> Map.new() - |> Map.update(:attributes, builtin_attributes, fn specified_attributes -> - specified_attributes - |> Map.new(fn {k, v} -> {to_string(k), v} end) - |> Map.merge(builtin_attributes) - end) + |> Map.update(:attributes, builtin_attributes, &Map.merge(&1, builtin_attributes)) end end diff --git a/test/otel_tests.exs b/test/otel_tests.exs index 677bda51..4112b1e2 100644 --- a/test/otel_tests.exs +++ b/test/otel_tests.exs @@ -36,14 +36,13 @@ defmodule OtelTests do Tracer.set_attributes([{"attr-2", "value-2"}]) end - attributes = - :otel_attributes.new([{"attr-1", "value-1"}, {"attr-2", "value-2"}], 128, :infinity) - assert_receive {:span, span( name: "span-1", - attributes: ^attributes + attributes: span_attributes )} + assert {"attr-1", "value-1"} in :otel_attributes.map(span_attributes) + assert {"attr-2", "value-2"} in :otel_attributes.map(span_attributes) end test "use Tracer to start a Span as currently active with an explicit parent" do @@ -56,24 +55,16 @@ defmodule OtelTests do end span_ctx(span_id: parent_span_id) = Span.end_span(s1) - - attributes = :otel_attributes.new([], 128, :infinity) - - assert_receive {:span, - span( - name: "span-1", - attributes: ^attributes - )} - - attributes = - :otel_attributes.new([{"attr-1", "value-1"}, {"attr-2", "value-2"}], 128, :infinity) + assert_receive {:span, span(name: "span-1")} assert_receive {:span, span( name: "span-2", parent_span_id: ^parent_span_id, - attributes: ^attributes + attributes: span_attributes )} + assert {"attr-1", "value-1"} in :otel_attributes.map(span_attributes) + assert {"attr-2", "value-2"} in :otel_attributes.map(span_attributes) end test "use Span to set attributes" do @@ -83,14 +74,13 @@ defmodule OtelTests do assert span_ctx() = Span.end_span(s) - attributes = - :otel_attributes.new([{"attr-1", "value-1"}, {"attr-2", "value-2"}], 128, :infinity) - assert_receive {:span, span( name: "span-2", - attributes: ^attributes + attributes: span_attributes )} + assert {"attr-1", "value-1"} in :otel_attributes.map(span_attributes) + assert {"attr-2", "value-2"} in :otel_attributes.map(span_attributes) end test "create child Span in Task" do @@ -197,15 +187,14 @@ defmodule OtelTests do assert span_ctx() = Span.end_span(s2) assert span_ctx() = Span.end_span(s3) - attributes = - :otel_attributes.new([{"attr-1", "value-1"}, {"attr-2", "value-2"}], 128, :infinity) - assert_receive {:span, span( name: "span-1", parent_span_id: :undefined, - attributes: ^attributes + attributes: span_attributes )} + assert {"attr-1", "value-1"} in :otel_attributes.map(span_attributes) + assert {"attr-2", "value-2"} in :otel_attributes.map(span_attributes) assert_receive {:span, span( @@ -238,17 +227,6 @@ defmodule OtelTests do stacktrace = Exception.format_stacktrace(__STACKTRACE__) - attributes = - :otel_attributes.new( - [ - {:"exception.type", "Elixir.RuntimeError"}, - {:"exception.message", "my error message"}, - {:"exception.stacktrace", stacktrace} - ], - 128, - :infinity - ) - assert_receive {:span, span( name: "span-4", @@ -259,10 +237,13 @@ defmodule OtelTests do :infinity, 0, [ - {:event, _, :exception, ^attributes} + {:event, _, :exception, exception_attributes} ] } )} + assert {:"exception.type", "Elixir.RuntimeError"} in :otel_attributes.map(exception_attributes) + assert {:"exception.message", "my error message"} in :otel_attributes.map(exception_attributes) + assert {:"exception.stacktrace", stacktrace} in :otel_attributes.map(exception_attributes) end end From 3e34ac32c21e68784af83ddc4194a8ea016309cd Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Wed, 4 Dec 2024 18:56:56 -0800 Subject: [PATCH 3/6] add another tracer macro test --- test/otel_tests.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/otel_tests.exs b/test/otel_tests.exs index 4112b1e2..4a8d47ad 100644 --- a/test/otel_tests.exs +++ b/test/otel_tests.exs @@ -67,6 +67,16 @@ defmodule OtelTests do assert {"attr-2", "value-2"} in :otel_attributes.map(span_attributes) end + test "use Tracer includes code attributes" do + Tracer.with_span "span-1" do + :ok + end + + assert_receive {:span, span(name: "span-1", attributes: span_attributes)} + assert {:"code.function", "test use Tracer includes code attributes/1"} in :otel_attributes.map(span_attributes) + assert {:"code.lineno", 71} in :otel_attributes.map(span_attributes) + end + test "use Span to set attributes" do s = Tracer.start_span("span-2") Span.set_attribute(s, "attr-1", "value-1") From 699f6d7e53381abc1edf27e7b9d14ee79ac70c9d Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Wed, 4 Dec 2024 18:57:42 -0800 Subject: [PATCH 4/6] fix mix formatting on tests --- test/otel_tests.exs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/test/otel_tests.exs b/test/otel_tests.exs index 4a8d47ad..fed2fa47 100644 --- a/test/otel_tests.exs +++ b/test/otel_tests.exs @@ -41,6 +41,7 @@ defmodule OtelTests do name: "span-1", attributes: span_attributes )} + assert {"attr-1", "value-1"} in :otel_attributes.map(span_attributes) assert {"attr-2", "value-2"} in :otel_attributes.map(span_attributes) end @@ -63,6 +64,7 @@ defmodule OtelTests do parent_span_id: ^parent_span_id, attributes: span_attributes )} + assert {"attr-1", "value-1"} in :otel_attributes.map(span_attributes) assert {"attr-2", "value-2"} in :otel_attributes.map(span_attributes) end @@ -73,7 +75,11 @@ defmodule OtelTests do end assert_receive {:span, span(name: "span-1", attributes: span_attributes)} - assert {:"code.function", "test use Tracer includes code attributes/1"} in :otel_attributes.map(span_attributes) + + assert {:"code.function", "test use Tracer includes code attributes/1"} in :otel_attributes.map( + span_attributes + ) + assert {:"code.lineno", 71} in :otel_attributes.map(span_attributes) end @@ -89,6 +95,7 @@ defmodule OtelTests do name: "span-2", attributes: span_attributes )} + assert {"attr-1", "value-1"} in :otel_attributes.map(span_attributes) assert {"attr-2", "value-2"} in :otel_attributes.map(span_attributes) end @@ -203,6 +210,7 @@ defmodule OtelTests do parent_span_id: :undefined, attributes: span_attributes )} + assert {"attr-1", "value-1"} in :otel_attributes.map(span_attributes) assert {"attr-2", "value-2"} in :otel_attributes.map(span_attributes) @@ -251,8 +259,15 @@ defmodule OtelTests do ] } )} - assert {:"exception.type", "Elixir.RuntimeError"} in :otel_attributes.map(exception_attributes) - assert {:"exception.message", "my error message"} in :otel_attributes.map(exception_attributes) + + assert {:"exception.type", "Elixir.RuntimeError"} in :otel_attributes.map( + exception_attributes + ) + + assert {:"exception.message", "my error message"} in :otel_attributes.map( + exception_attributes + ) + assert {:"exception.stacktrace", stacktrace} in :otel_attributes.map(exception_attributes) end end From d755763ccba3adc35af940ed1620af722d78ad97 Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Wed, 4 Dec 2024 18:59:57 -0800 Subject: [PATCH 5/6] fix angry line number assert test --- test/otel_tests.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/otel_tests.exs b/test/otel_tests.exs index fed2fa47..2d9d40e0 100644 --- a/test/otel_tests.exs +++ b/test/otel_tests.exs @@ -80,7 +80,7 @@ defmodule OtelTests do span_attributes ) - assert {:"code.lineno", 71} in :otel_attributes.map(span_attributes) + assert {:"code.lineno", 73} in :otel_attributes.map(span_attributes) end test "use Span to set attributes" do From 543b8c0cac8f910e89caca13ba5de70502c97272 Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Wed, 4 Dec 2024 19:08:45 -0800 Subject: [PATCH 6/6] remove Elixir. prefix from namespace --- apps/opentelemetry_api/lib/open_telemetry/attributes.ex | 2 +- apps/opentelemetry_api/test/open_telemetry_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/opentelemetry_api/lib/open_telemetry/attributes.ex b/apps/opentelemetry_api/lib/open_telemetry/attributes.ex index df98553f..5fe7aefa 100644 --- a/apps/opentelemetry_api/lib/open_telemetry/attributes.ex +++ b/apps/opentelemetry_api/lib/open_telemetry/attributes.ex @@ -64,7 +64,7 @@ defmodule OpenTelemetry.Attributes do %{ @code_function => function_arty, - @code_namespace => to_string(env.module), + @code_namespace => inspect(env.module), @code_filepath => env.file, @code_lineno => env.line } diff --git a/apps/opentelemetry_api/test/open_telemetry_test.exs b/apps/opentelemetry_api/test/open_telemetry_test.exs index a5de4c1b..f2d98879 100644 --- a/apps/opentelemetry_api/test/open_telemetry_test.exs +++ b/apps/opentelemetry_api/test/open_telemetry_test.exs @@ -151,6 +151,6 @@ defmodule OpenTelemetryTest do assert attributes[:"code.filepath"] =~ "open_telemetry_test.exs" assert attributes[:"code.function"] =~ "from_macro_env/1" assert attributes[:"code.lineno"] == 149 - assert attributes[:"code.namespace"] == "Elixir.OpenTelemetryTest" + assert attributes[:"code.namespace"] == "OpenTelemetryTest" end end