diff --git a/lib/new_relic/transaction/reporter.ex b/lib/new_relic/transaction/reporter.ex index cceebaa2..d29b3840 100644 --- a/lib/new_relic/transaction/reporter.ex +++ b/lib/new_relic/transaction/reporter.ex @@ -18,7 +18,13 @@ defmodule NewRelic.Transaction.Reporter do def add_attributes(attrs) when is_list(attrs) do if tracking?(self()) do - AttrStore.add(__MODULE__, self(), NewRelic.Util.deep_flatten(attrs)) + AttrStore.add( + __MODULE__, + self(), + attrs + |> NewRelic.Util.deep_flatten() + |> NewRelic.Util.coerce_attributes() + ) end end @@ -28,20 +34,19 @@ defmodule NewRelic.Transaction.Reporter do end end - def set_transaction_name(custom_name) do + def set_transaction_name(custom_name) when is_binary(custom_name) do if tracking?(self()) do AttrStore.add(__MODULE__, self(), custom_name: custom_name) end end # Internal Agent API - # def start() do Transaction.Monitor.add(self()) AttrStore.track(__MODULE__, self()) - add_attributes( + AttrStore.add(__MODULE__, self(), pid: inspect(self()), start_time: System.system_time(), start_time_mono: System.monotonic_time() @@ -66,7 +71,7 @@ defmodule NewRelic.Transaction.Reporter do def fail(%{kind: kind, reason: reason, stack: stack} = error) do if tracking?(self()) do if NewRelic.Config.feature?(:error_collector) do - add_attributes( + AttrStore.add(__MODULE__, self(), error: true, transaction_error: {:error, error}, error_kind: kind, @@ -74,7 +79,7 @@ defmodule NewRelic.Transaction.Reporter do error_stack: inspect(stack) ) else - add_attributes(error: true) + AttrStore.add(__MODULE__, self(), error: true) end end end diff --git a/lib/new_relic/util.ex b/lib/new_relic/util.ex index 4b693a36..ec246737 100644 --- a/lib/new_relic/util.ex +++ b/lib/new_relic/util.ex @@ -41,6 +41,10 @@ defmodule NewRelic.Util do |> Enum.concat([{"#{key}.length", length(list_value)}]) end + def deep_flatten({key, %{__struct__: _} = value}) do + [{key, value}] + end + def deep_flatten({key, map_value}) when is_map(map_value) do map_value |> Enum.slice(0..9) @@ -52,6 +56,36 @@ defmodule NewRelic.Util do [{key, value}] end + def coerce_attributes(attrs) do + Enum.map(attrs, fn + {key, value} when is_number(value) when is_boolean(value) -> + {key, value} + + {key, value} when is_bitstring(value) -> + case String.valid?(value) do + true -> {key, value} + false -> bad_value(key, value) + end + + {key, value} when is_reference(value) when is_pid(value) when is_port(value) -> + {key, inspect(value)} + + {key, value} when is_atom(value) -> + {key, to_string(value)} + + {key, %struct{} = value} when struct in [Date, DateTime, Time, NaiveDateTime] -> + {key, struct.to_iso8601(value)} + + {key, value} -> + bad_value(key, value) + end) + end + + defp bad_value(key, value) do + NewRelic.log(:debug, "Bad attribute value: #{inspect(key)} => #{inspect(value)}") + {key, "[BAD_VALUE]"} + end + def elixir_environment() do build_info = System.build_info() diff --git a/test/transaction_test.exs b/test/transaction_test.exs index 461fec92..7836fbc1 100644 --- a/test/transaction_test.exs +++ b/test/transaction_test.exs @@ -37,6 +37,31 @@ defmodule TransactionTest do send_resp(conn, 200, "incr") end + get "/funky_attrs" do + NewRelic.add_attributes( + # allowed: + one: 1, + half: 0.5, + string: "A string", + bool: true, + atom: :atom, + pid: self(), + ref: make_ref(), + port: :erlang.list_to_port('#Port<0.4>'), + date: Date.utc_today(), + date_time: DateTime.utc_now(), + naive_date_time: NaiveDateTime.utc_now(), + time: Time.utc_now(), + # not allowed: + binary: "fooo" |> :zlib.gzip(), + struct: %NewRelic.Metric{}, + tuple: {:one, :two}, + function: fn -> :fun! end + ) + + send_resp(conn, 200, "funky_attrs") + end + get "/service" do NewRelic.set_transaction_name("/service") NewRelic.add_attributes(query: "query{}") @@ -134,6 +159,42 @@ defmodule TransactionTest do end) end + @bad "[BAD_VALUE]" + test "Attribute coercion" do + TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle) + + TestHelper.request(TestPlugApp, conn(:get, "/funky_attrs")) + + events = TestHelper.gather_harvest(Collector.TransactionEvent.Harvester) + + [_, event] = Enum.find(events, fn [_, event] -> event[:name] == "/Plug/GET//funky_attrs" end) + + # Basic values + assert event[:one] == 1 + assert event[:half] == 0.5 + assert event[:bool] == true + assert event[:string] == "A string" + assert event[:atom] == "atom" + + # Fancy values + assert event[:pid] =~ "#PID" + assert event[:ref] =~ "#Reference" + assert event[:port] =~ "#Port" + assert {:ok, _, _} = DateTime.from_iso8601(event[:date_time]) + assert {:ok, _} = NaiveDateTime.from_iso8601(event[:naive_date_time]) + assert {:ok, _} = Date.from_iso8601(event[:date]) + assert {:ok, _} = Time.from_iso8601(event[:time]) + + # Bad values + assert event[:binary] == @bad + assert event[:tuple] == @bad + assert event[:function] == @bad + assert event[:struct] == @bad + + # Make sure it can serialize to JSON + Jason.encode!(events) + end + test "Incrementing attribute counters" do TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle)