From 7f028343c69d8a6cd57c13867c6996fc0e0c2d81 Mon Sep 17 00:00:00 2001 From: Blake Kostner Date: Wed, 4 Dec 2024 12:59:50 -0800 Subject: [PATCH] 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