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

Phoenix LiveView instrumentation #500

Merged
merged 2 commits into from
Feb 15, 2025
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Some common Elixir packages are auto-instrumented via [`telemetry`](https://gith

* [`Plug`](https://github.com/elixir-plug/plug): See [NewRelic.Telemetry.Plug](https://hexdocs.pm/new_relic_agent/NewRelic.Telemetry.Plug.html) for details.
* [`Phoenix`](https://github.com/phoenixframework/phoenix): See [NewRelic.Telemetry.Phoenix](https://hexdocs.pm/new_relic_agent/NewRelic.Telemetry.Phoenix.html) for details.
* [`Phoenix LiveView`](https://github.com/phoenixframework/phoenix_live_view): See [NewRelic.Telemetry.PhoenixLiveView](https://hexdocs.pm/new_relic_agent/NewRelic.Telemetry.PhoenixLiveView.html) for details.
* [`Ecto`](https://github.com/elixir-ecto/ecto): See [NewRelic.Telemetry.Ecto](https://hexdocs.pm/new_relic_agent/NewRelic.Telemetry.Ecto.html) for details.
* [`Redix`](https://github.com/whatyouhide/redix): See [NewRelic.Telemetry.Redix](https://hexdocs.pm/new_relic_agent/NewRelic.Telemetry.Redix.html) for details.
* [`Finch`](https://github.com/sneako/finch): See [NewRelic.Telemetry.Finch](https://hexdocs.pm/new_relic_agent/NewRelic.Telemetry.Finch.html) for details.
Expand Down
8 changes: 6 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import Config

if Mix.env() == :test, do: import_config("test.exs")
if File.exists?("config/secret.exs"), do: import_config("secret.exs")
if Mix.env() == :test do
import_config("test.exs")
else
if File.exists?("config/secret.exs"),
do: import_config("secret.exs")
end
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ defmodule AbsintheExampleTest do

operation =
Enum.find(spans, fn %{attributes: attr} ->
attr[:name] == "query:TestQuery"
attr[:name] == "Absinthe/Operation/query:TestQuery"
end)

one_resolver =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ defmodule PhxExampleWeb.BanditEndpoint do
same_site: "Lax"
]

socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
socket "/live",
Phoenix.LiveView.Socket,
websocket: [connect_info: [session: @session_options]],
longpoll: [connect_info: [session: @session_options]]

plug Plug.Static,
at: "/",
Expand Down
5 changes: 4 additions & 1 deletion examples/apps/phx_example/lib/phx_example_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ defmodule PhxExampleWeb.Endpoint do
same_site: "Lax"
]

socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
socket "/live",
Phoenix.LiveView.Socket,
websocket: [connect_info: [session: @session_options]],
longpoll: [connect_info: [session: @session_options]]

plug Plug.Static,
at: "/",
Expand Down
28 changes: 27 additions & 1 deletion examples/apps/phx_example/lib/phx_example_web/live/home_live.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
defmodule PhxExampleWeb.HomeLive do
use PhxExampleWeb, :live_view

@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:content, "stuff")

{:ok, socket}
end

def handle_event("click", _params, socket) do
socket =
socket
|> assign(:content, "clicked")

Process.send_after(self(), :after, 1_000)
{:noreply, socket}
end

def handle_info(:after, socket) do
socket =
socket
|> assign(:content, "after")

{:noreply, socket}
end

def render(assigns) do
~H"""
<div>
<h1>Home</h1>
<p>Some content</p>
<p><%= @content %></p>
<div phx-click="click">Click me</div>
</div>
"""
end
Expand Down
1 change: 1 addition & 0 deletions examples/apps/phx_example/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ defmodule PhxExample.MixProject do
{:phoenix_html, "~> 3.3"},
{:phoenix_view, "~> 2.0"},
{:phoenix_live_view, "~> 0.20"},
{:floki, ">= 0.30.0", only: :test},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:bandit, "~> 1.0"}
Expand Down
58 changes: 41 additions & 17 deletions examples/apps/phx_example/test/phx_example_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ defmodule PhxExampleTest do
TestHelper.simulate_agent_enabled()
end

setup do
TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle)
TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle)
TestHelper.restart_harvest_cycle(Collector.SpanEvent.HarvestCycle)
TestHelper.restart_harvest_cycle(Collector.ErrorTrace.HarvestCycle)
NewRelic.DistributedTrace.BackoffSampler.reset()
:ok
end

for server <- [:cowboy, :bandit] do
describe "Testing #{server}:" do
test "Phoenix metrics generated" do
TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle)
TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle)

{:ok, %{body: body}} = request("/phx/bar", unquote(server))
assert body =~ "Welcome to Phoenix"

Expand Down Expand Up @@ -48,9 +54,6 @@ defmodule PhxExampleTest do
end

test "Phoenix metrics generated for LiveView" do
TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle)
TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle)

{:ok, %{body: body}} = request("/phx/home", unquote(server))
assert body =~ "Some content"

Expand All @@ -71,23 +74,51 @@ defmodule PhxExampleTest do
end

test "Phoenix spans generated" do
TestHelper.restart_harvest_cycle(Collector.SpanEvent.HarvestCycle)
{:ok, %{body: body}} = request("/phx/home", unquote(server))
assert body =~ "Some content"

span_events = TestHelper.gather_harvest(Collector.SpanEvent.Harvester)

tx_span = TestHelper.find_span(span_events, "/Phoenix/PhxExampleWeb.HomeLive/index")
process_span = TestHelper.find_span(span_events, "Transaction Root Process")
mount_span = TestHelper.find_span(span_events, "PhxExampleWeb.HomeLive:index.mount")

assert process_span[:parentId] == tx_span[:guid]
assert mount_span[:"live_view.params"]
end

@endpoint PhxExampleWeb.Endpoint
test "Live View transaction and spans generated" do
import Phoenix.ConnTest
import Phoenix.LiveViewTest

conn =
Phoenix.ConnTest.build_conn()
|> Plug.Test.init_test_session([])

conn = get(conn, "/phx/home")
assert html_response(conn, 200) =~ "<p>Some content</p>"

{:ok, _view, _html} = live(conn)

span_events = TestHelper.gather_harvest(Collector.SpanEvent.Harvester)

tx_span =
TestHelper.find_span(span_events, "/Phoenix.LiveView/Live/PhxExampleWeb.HomeLive/index")

process_span = TestHelper.find_span(span_events, "Transaction Root Process")
mount_span = TestHelper.find_span(span_events, "PhxExampleWeb.HomeLive:index.mount")
render_span = TestHelper.find_span(span_events, "PhxExampleWeb.HomeLive:index.render")

assert tx_span[:"live_view.endpoint"] == "PhxExampleWeb.Endpoint"

assert process_span[:parentId] == tx_span[:guid]
assert mount_span[:parentId] == process_span[:guid]
assert render_span[:parentId] == process_span[:guid]
end

@tag :capture_log
test "Phoenix error" do
TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle)
TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle)

{:ok, %{body: body, status_code: 500}} = request("/phx/error", unquote(server))

assert body =~ "Oops, Internal Server Error"
Expand All @@ -111,9 +142,6 @@ defmodule PhxExampleTest do

@tag :capture_log
test "Phoenix LiveView error" do
TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle)
TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle)

{:ok, %{body: body, status_code: 500}} = request("/phx/live_error", unquote(server))

assert body =~ "Oops, Internal Server Error"
Expand All @@ -136,10 +164,6 @@ defmodule PhxExampleTest do
end

test "Phoenix route not found" do
TestHelper.restart_harvest_cycle(Collector.Metric.HarvestCycle)
TestHelper.restart_harvest_cycle(Collector.TransactionEvent.HarvestCycle)
TestHelper.restart_harvest_cycle(Collector.ErrorTrace.HarvestCycle)

{:ok, %{body: body, status_code: 404}} = request("/not_found", unquote(server))
assert body =~ "Not Found"

Expand Down
5 changes: 4 additions & 1 deletion examples/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ for config <- "../apps/*/config/config.exs" |> Path.expand(__DIR__) |> Path.wild
import_config config
end

if File.exists?("config/secret.exs"), do: import_config("secret.exs")
if Mix.env() != :test do
if File.exists?(Path.expand("./secret.exs", __DIR__)),
do: import_config("secret.exs")
end
1 change: 1 addition & 0 deletions examples/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.14.0", "8710aa6de137a9c428fc86306b574777e9d545534b0259030a325948d43fc740", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "4f69e5df5bdf8b98d80797b1a88b1d24b13ad9f187a553241c2aa6cb7447a672"},
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
"exqlite": {:hex, :exqlite, "0.17.0", "865ab503debde7913ffa02b58838ab92885165978f4c88d8169ee8688c655d1e", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "719fa7986fed242839629a907d60f774000c1d2dc03ba6ba05fcd30579f2ab45"},
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
"hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"},
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
Expand Down
2 changes: 1 addition & 1 deletion lib/new_relic/aggregate/aggregate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ defmodule NewRelic.Aggregate do

defp averages(%{call_count: call_count} = values) do
values
|> Stream.reject(fn {key, _value} -> key == :call_count end)
|> Enum.reject(fn {key, _value} -> key == :call_count end)
|> Map.new(fn {key, value} -> {:"avg_#{key}", value / call_count} end)
end

Expand Down
5 changes: 5 additions & 0 deletions lib/new_relic/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ defmodule NewRelic.Config do

* `:plug_instrumentation_enabled`
* `:phoenix_instrumentation_enabled`
* `:phoenix_live_view_instrumentation_enabled`
* `:ecto_instrumentation_enabled`
* `:redix_instrumentation_enabled`
* `:oban_instrumentation_enabled`
Expand Down Expand Up @@ -184,6 +185,10 @@ defmodule NewRelic.Config do
get(:features, :phoenix_instrumentation)
end

def feature?(:phoenix_live_view_instrumentation) do
get(:features, :phoenix_live_view_instrumentation)
end

def feature?(:ecto_instrumentation) do
get(:features, :ecto_instrumentation)
end
Expand Down
5 changes: 5 additions & 0 deletions lib/new_relic/init.ex
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ defmodule NewRelic.Init do
"NEW_RELIC_PHOENIX_INSTRUMENTATION_ENABLED",
:phoenix_instrumentation_enabled
),
phoenix_live_view_instrumentation:
determine_feature(
"NEW_RELIC_PHOENIX_LIVE_VIEW_INSTRUMENTATION_ENABLED",
:phoenix_live_view_instrumentation_enabled
),
oban_instrumentation:
determine_feature(
"NEW_RELIC_OBAN_INSTRUMENTATION_ENABLED",
Expand Down
10 changes: 10 additions & 0 deletions lib/new_relic/metric/metric_data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,16 @@ defmodule NewRelic.Metric.MetricData do
max_call_time: duration_s
}

def transform({:function, function_name}, duration_s: duration_s),
do: %Metric{
name: join(["Function", function_name]),
call_count: 1,
total_call_time: duration_s,
total_exclusive_time: duration_s,
min_call_time: duration_s,
max_call_time: duration_s
}

def transform({:function, function_name},
duration_s: duration_s,
exclusive_time_s: exclusive_time_s
Expand Down
10 changes: 5 additions & 5 deletions lib/new_relic/telemetry/absinthe.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ defmodule NewRelic.Telemetry.Absinthe do

NewRelic.Tracer.Direct.start_span(
meta.id,
"Operation",
start_time: meas.system_time,
"Absinthe/Operation",
system_time: meas.system_time,
attributes: [
"absinthe.schema": inspect(meta.options[:schema]),
"absinthe.query": query
Expand Down Expand Up @@ -116,7 +116,7 @@ defmodule NewRelic.Telemetry.Absinthe do
NewRelic.Tracer.Direct.start_span(
meta.id,
{resolver_name, "#{inspect(resolution.schema)}.#{path}"},
start_time: meas.system_time,
system_time: meas.system_time,
attributes: [
"absinthe.field.path": path,
"absinthe.field.type": type,
Expand Down Expand Up @@ -160,11 +160,11 @@ defmodule NewRelic.Telemetry.Absinthe do
end

defp operation_span_name(%{type: type, name: name}) when is_binary(name) do
"#{to_string(type)}:#{name}"
"Absinthe/Operation/#{to_string(type)}:#{name}"
end

defp operation_span_name(%{type: type}) do
"#{to_string(type)}"
"Absinthe/Operation/#{to_string(type)}"
end

defp transaction_name(schema, operation) do
Expand Down
Loading
Loading