Repository: newrelic/elixir_agent
Pull request #40 (2018-10-09 23:21:38)

Flatten attributes that are maps
This PR will flatten any attribute value that is a map so that it's component parts all show up as their own attributes.

In favor of #39 


Patch:
diff --git a/lib/new_relic/transaction/reporter.ex b/lib/new_relic/transaction/reporter.ex
index 87c862a..e22a7df 100644
--- a/lib/new_relic/transaction/reporter.ex
+++ b/lib/new_relic/transaction/reporter.ex
@@ -19,9 +19,9 @@ defmodule NewRelic.Transaction.Reporter do
 
   # Customer Exposed API
 
-  def add_attributes(attrs) do
+  def add_attributes(attrs) when is_list(attrs) do
     if tracking?(self()) do
-      AttrStore.add(__MODULE__, self(), attrs)
+      AttrStore.add(__MODULE__, self(), NewRelic.Util.deep_flatten(attrs))
     end
   end
 
@@ -62,7 +62,7 @@ defmodule NewRelic.Transaction.Reporter do
       add_attributes(
         end_time_mono: System.monotonic_time(),
         error: true,
-        transaction_error: error,
+        transaction_error: {:error, error},
         error_kind: error.kind,
         error_reason: inspect(error.reason),
         error_stack: inspect(error.stack)
@@ -85,7 +85,7 @@ defmodule NewRelic.Transaction.Reporter do
 
   def set_transaction_error(pid, error) do
     if tracking?(pid) do
-      AttrStore.add(__MODULE__, pid, transaction_error: error)
+      AttrStore.add(__MODULE__, pid, transaction_error: {:error, error})
     end
   end
 
@@ -381,7 +381,7 @@ defmodule NewRelic.Transaction.Reporter do
 
   defp report_transaction_error_event(_tx_attrs, nil), do: :ignore
 
-  defp report_transaction_error_event(tx_attrs, error) do
+  defp report_transaction_error_event(tx_attrs, {:error, error}) do
     attributes = Map.drop(tx_attrs, [:error, :error_kind, :error_reason, :error_stack])
 
     {exception_type, exception_reason, exception_stacktrace} =
diff --git a/lib/new_relic/util.ex b/lib/new_relic/util.ex
index 3afb083..62ad7d6 100644
--- a/lib/new_relic/util.ex
+++ b/lib/new_relic/util.ex
@@ -16,6 +16,18 @@ defmodule NewRelic.Util do
   def time_to_ms({megasec, sec, microsec}),
     do: (megasec * 1_000_000 + sec) * 1_000 + round(microsec / 1_000)
 
+  def deep_flatten(attrs) when is_list(attrs) do
+    Enum.flat_map(attrs, &deep_flatten/1)
+  end
+
+  def deep_flatten({key, value}) when is_map(value) do
+    Enum.flat_map(value, fn {k, v} -> deep_flatten({"#{key}.#{k}", v}) end)
+  end
+
+  def deep_flatten({key, value}) do
+    [{key, value}]
+  end
+
   def elixir_environment() do
     build_info = System.build_info()
 


Test patch:
diff --git a/test/transaction_test.exs b/test/transaction_test.exs
index e9cd4f4..dbb3802 100644
--- a/test/transaction_test.exs
+++ b/test/transaction_test.exs
@@ -45,6 +45,11 @@ defmodule TransactionTest do
       send_resp(conn, 200, "service")
     end
 
+    get "/map" do
+      NewRelic.add_attributes(plain: "attr", deep: %{foo: %{bar: "baz", baz: "bar"}})
+      send_resp(conn, 200, "map")
+    end
+
     get "/sequential/:order" do
       NewRelic.set_transaction_name("/sequential")
       NewRelic.add_attributes(order: order)
@@ -223,4 +228,17 @@ defmodule TransactionTest do
                event[:rabbit] == "hole" && event[:status] == 200
            end)
   end
+
+  test "Flatten the keys of a nested map into a list of individual attributes" do
+    TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle)
+
+    TestHelper.request(TestPlugApp, conn(:get, "/map"))
+
+    events = TestHelper.gather_harvest(Collector.TransactionEvent.Harvester)
+
+    assert Enum.find(events, fn [_, event] ->
+             event[:path] == "/map" && event[:plain] == "attr" && event["deep.foo.bar"] == "baz" &&
+               event["deep.foo.baz"] == "bar"
+           end)
+  end
 end
diff --git a/test/util_test.exs b/test/util_test.exs
index 6c91d68..7e24f2d 100644
--- a/test/util_test.exs
+++ b/test/util_test.exs
@@ -24,6 +24,14 @@ defmodule UtilTest do
     refute json =~ ":node"
   end
 
+  test "flatten deeply nested map attributes" do
+    flattened =
+      NewRelic.Util.deep_flatten(not_nested: "value", nested: %{foo: %{bar: %{baz: "qux"}}})
+
+    assert {"nested.foo.bar.baz", "qux"} in flattened
+    assert {:not_nested, "value"} in flattened
+  end
+
   test "Truncates unicode strings correctly" do
     %{key: truncated} =
       NewRelic.Util.Event.process_event(%{key: String.duplicate("a", 4094) <> "é"})