Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: setup code attributes in Elixir macros #808

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions apps/opentelemetry_api/lib/open_telemetry/attributes.ex
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 62 in apps/opentelemetry_api/lib/open_telemetry/attributes.ex

View check run for this annotation

Codecov / codecov/patch

apps/opentelemetry_api/lib/open_telemetry/attributes.ex#L62

Added line #L62 was not covered by tests
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
41 changes: 35 additions & 6 deletions apps/opentelemetry_api/lib/open_telemetry/tracer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@
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
Expand All @@ -37,12 +42,17 @@
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 =

Check warning on line 45 in apps/opentelemetry_api/lib/open_telemetry/tracer.ex

View check run for this annotation

Codecov / codecov/patch

apps/opentelemetry_api/lib/open_telemetry/tracer.ex#L45

Added line #L45 was not covered by tests
__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
Expand Down Expand Up @@ -70,11 +80,16 @@
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
Expand All @@ -88,12 +103,17 @@
See `start_span/2` and `end_span/0`.
"""
defmacro with_span(ctx, name, start_opts, do: block) do
attributes =

Check warning on line 106 in apps/opentelemetry_api/lib/open_telemetry/tracer.ex

View check run for this annotation

Codecov / codecov/patch

apps/opentelemetry_api/lib/open_telemetry/tracer.ex#L106

Added line #L106 was not covered by tests
__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
Expand Down Expand Up @@ -221,4 +241,13 @@
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this public but undocumented. Not sure if we want to document and make it part of the public API

start_opts
|> Map.new()
|> Map.update(:attributes, builtin_attributes, &Map.merge(&1, builtin_attributes))
end
end
9 changes: 9 additions & 0 deletions apps/opentelemetry_api/test/open_telemetry_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
78 changes: 42 additions & 36 deletions test/otel_tests.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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

Expand Down
Loading