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..5fe7aefa --- /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 => inspect(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..04b7d887 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,13 @@ 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, &Map.merge(&1, builtin_attributes)) + end end diff --git a/apps/opentelemetry_api/test/open_telemetry_test.exs b/apps/opentelemetry_api/test/open_telemetry_test.exs index fefa9b06..f2d98879 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"] == "OpenTelemetryTest" + end end diff --git a/test/otel_tests.exs b/test/otel_tests.exs index 677bda51..2d9d40e0 100644 --- a/test/otel_tests.exs +++ b/test/otel_tests.exs @@ -36,14 +36,14 @@ 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 +56,31 @@ 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 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", 73} in :otel_attributes.map(span_attributes) end test "use Span to set attributes" do @@ -83,14 +90,14 @@ 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,16 +204,16 @@ 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( name: "span-2", @@ -238,17 +245,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 +255,20 @@ 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