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

Feature/add expand all maps opt #131

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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ config :open_telemetry_decorator, attr_joiner: "."
```

Thanks to @benregn for the examples and inspiration for these two options!

### Changing the default behavior for expanding nested maps
By default, nested attributes need to be explicitly selected in the `with_span/3` macro (See additional examples to see this in action). However if you want the default behavior across your application to always expand variables that are maps/structs, you can specify the following config
```elixir
config :open_telemetry_decorator, expand_maps: true
```

<!-- MDOC -->

### Additional Examples
Expand Down Expand Up @@ -176,6 +183,20 @@ defmodule MyApp.Worker do
end
```

Grab all nested map/struct properties:

```elixir
defmodule MyApp.Worker do
use OpenTelemetryDecorator

@decorate with_span("my_app.worker.do_work", include: [:arg1, :arg2], expand_maps: true)
def do_work(arg1, arg2) do
total = some_calculation(arg1.count, arg2.count)
{:ok, total}
end
end
```

```elixir
defmodule MyApp.Worker do
use OpenTelemetryDecorator
Expand Down
1 change: 1 addition & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Config

config :open_telemetry_decorator, attr_prefix: ""
config :open_telemetry_decorator, attr_joiner: "."
config :open_telemetry_decorator, expand_maps: false

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
Expand Down
7 changes: 5 additions & 2 deletions lib/open_telemetry_decorator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ defmodule OpenTelemetryDecorator do
alias OpenTelemetryDecorator.Attributes
alias OpenTelemetryDecorator.Validator

@default_expand_maps Application.compile_env(:open_telemetry_decorator, :expand_maps, false)

def trace(span_name, opts \\ [], body, context), do: with_span(span_name, opts, body, context)

@doc """
Expand All @@ -40,6 +42,7 @@ defmodule OpenTelemetryDecorator do
"""
def with_span(span_name, opts \\ [], body, context) do
include = Keyword.get(opts, :include, [])
expand_maps = Keyword.get(opts, :expand_maps, @default_expand_maps)
Validator.validate_args(span_name, include)

quote location: :keep do
Expand All @@ -51,7 +54,7 @@ defmodule OpenTelemetryDecorator do

input_params =
Kernel.binding()
|> Attributes.get(unquote(include))
|> Attributes.get(unquote(include), unquote(expand_maps))
|> Keyword.delete(:result)

Attributes.set(input_params)
Expand All @@ -62,7 +65,7 @@ defmodule OpenTelemetryDecorator do
attrs =
Kernel.binding()
|> Keyword.put(:result, result)
|> Attributes.get(unquote(include))
|> Attributes.get(unquote(include), unquote(expand_maps))
|> Keyword.merge(input_params)
|> Enum.map(fn {k, v} -> {Atom.to_string(k), v} end)

Expand Down
24 changes: 20 additions & 4 deletions lib/open_telemetry_decorator/attributes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,40 @@ defmodule OpenTelemetryDecorator.Attributes do
set(Tracer.current_span_ctx(), attributes)
end

def get(all_attributes, requested_attributes) do
def get(all_attributes, requested_attributes, expand_maps \\ false) do
Enum.reduce(requested_attributes, [], fn requested_attribute, taken_attributes ->
case get_attribute(all_attributes, requested_attribute) do
case get_attribute(all_attributes, requested_attribute, expand_maps) do
attributes when is_list(attributes) -> taken_attributes ++ attributes
{name, value} -> Keyword.put(taken_attributes, name, value)
_ -> taken_attributes
end
end)
end

defp get_attribute(attributes, [attribute_name | nested_keys]) do
defp get_attribute(attributes, attribute_name, true) do
requested_attribute = recursive_get_in(attributes, List.wrap(attribute_name))

if is_map(requested_attribute) do
requested_attribute
|> as_map()
|> Map.keys()
|> Enum.map(&get_attribute(attributes, List.wrap(attribute_name) ++ [&1], true))
|> Enum.reject(&(&1 == nil))
|> List.flatten()
else
get_attribute(attributes, attribute_name, false)
end
end

defp get_attribute(attributes, [attribute_name | nested_keys], false) do
requested_obj = attributes |> Keyword.get(attribute_name) |> as_map()

if value = recursive_get_in(requested_obj, nested_keys) do
{derived_name([attribute_name | nested_keys]), to_otlp_value(value)}
end
end

defp get_attribute(attributes, attribute_name) do
defp get_attribute(attributes, attribute_name, false) do
if value = Keyword.get(attributes, attribute_name) do
{derived_name(attribute_name), to_otlp_value(value)}
end
Expand Down
21 changes: 21 additions & 0 deletions test/open_telemetry_decorator/attributes_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,33 @@ defmodule OpenTelemetryDecorator.AttributesTest do
assert Attributes.get([obj: %{id: 1}], [[:obj, :id]]) == [obj_id: 1]
end

test "handles expanding map" do
attributes = Attributes.get([obj: %{id: 1, foo: 2}], [:obj], true)
assert {:obj_id, 1} in attributes
assert {:obj_foo, 2} in attributes

attributes =
Attributes.get(
[obj: %{id: 1, foo: 2, foop: %{id: 3}}, obj_b: %{id: 4}],
[:obj, :obj_b],
true
)

assert {:obj_id, 1} in attributes
assert {:obj_foo, 2} in attributes
assert {:obj_foop_id, 3} in attributes
assert {:obj_b_id, 4} in attributes
end

test "handles nested structs" do
one_level = %SomeStruct{beep: "boop"}
assert Attributes.get([obj: one_level], [[:obj, :beep]]) == [obj_beep: "boop"]

two_levels = %SomeStruct{failed: %SomeStruct{count: 3}}
assert Attributes.get([obj: two_levels], [[:obj, :failed, :count]]) == [obj_failed_count: 3]

# Use expand_all_maps functionality
assert Attributes.get([obj: two_levels], [:obj], true) == [obj_failed_count: 3]
end

test "handles invalid requests for nested structs" do
Expand Down
20 changes: 20 additions & 0 deletions test/open_telemetry_decorator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ defmodule OpenTelemetryDecoratorTest do
end
end

@decorate with_span("Example.find_expand", include: [:user], expand_maps: true)
def find_expand(id) do
_even = rem(id, 2) == 0
user = %{id: id, name: "my user"}

case id do
1 ->
{:ok, user}

error ->
{:error, error}
end
end

@decorate with_span("Example.parse_params", include: [[:params, "id"]])
def parse_params(params) do
%{"id" => id} = params
Expand Down Expand Up @@ -106,6 +120,12 @@ defmodule OpenTelemetryDecoratorTest do
assert %{"user_name" => "my user"} = get_span_attributes(attrs)
end

test "handles nested attributes when expand_maps is set" do
Example.find_expand(1)
assert_receive {:span, span(name: "Example.find_expand", attributes: attrs)}
assert %{"user_id" => 1, "user_name" => "my user"} = get_span_attributes(attrs)
end

test "handles maps with string keys" do
Example.parse_params(%{"id" => 12})
assert_receive {:span, span(name: "Example.parse_params", attributes: attrs)}
Expand Down