diff --git a/apps/deployex_web/lib/deployex_web/live/applications/index.ex b/apps/deployex_web/lib/deployex_web/live/applications/index.ex index 786eb3c4..262ef448 100644 --- a/apps/deployex_web/lib/deployex_web/live/applications/index.ex +++ b/apps/deployex_web/lib/deployex_web/live/applications/index.ex @@ -528,7 +528,8 @@ defmodule DeployexWeb.ApplicationsLive do {:metrics_new_data, source_node, metric_key, %Telemetry.Data{measurements: %{total: count, limit: limit}}}, %{assigns: %{metrics: metrics}} = socket - ) do + ) + when is_integer(limit) and limit > 0 do if source_node in metrics.monitored_nodes do current_percentage = trunc(count / limit * 100) @@ -551,7 +552,8 @@ defmodule DeployexWeb.ApplicationsLive do {:noreply, socket} end - def handle_info({:update_system_info, host_info}, %{assigns: %{metrics: metrics}} = socket) do + def handle_info({:update_system_info, host_info}, %{assigns: %{metrics: metrics}} = socket) + when is_integer(host_info.memory_total) and host_info.memory_total > 0 do # Sync ui_settings from cache to ensure NavMenu has latest state ui_settings = UiSettings.get() @@ -572,6 +574,12 @@ defmodule DeployexWeb.ApplicationsLive do |> assign(:ui_settings, ui_settings)} end + # NOTE: Ignore system info when memory_total is nil or 0 to avoid division by zero + def handle_info({:update_system_info, host_info}, socket) do + ui_settings = UiSettings.get() + {:noreply, socket |> assign(:host_info, host_info) |> assign(:ui_settings, ui_settings)} + end + def handle_info( {:monitoring_app_updated, source_node, monitoring_apps_data}, %{assigns: %{node: node, metrics: metrics}} = socket diff --git a/apps/deployex_web/test/deployex_web/live/applications/index_test.exs b/apps/deployex_web/test/deployex_web/live/applications/index_test.exs index 6e7b9f0f..fec4e0e9 100644 --- a/apps/deployex_web/test/deployex_web/live/applications/index_test.exs +++ b/apps/deployex_web/test/deployex_web/live/applications/index_test.exs @@ -5,6 +5,7 @@ defmodule DeployexWeb.Applications.IndexTest do import Mox alias DeployexWeb.Fixture.Status, as: FixtureStatus + alias Host.Info setup [ :set_mox_global, @@ -490,6 +491,98 @@ defmodule DeployexWeb.Applications.IndexTest do end end) + describe "division by zero guards" do + @tag :capture_log + test "GET /applications handles vm metric with limit 0 without crashing", %{conn: conn} do + topic = "test-topic" + + deployex = + FixtureStatus.deployex(%{ + node: Node.self(), + monitoring: add_metrics([:memory, :port]) + }) + + Deployer.StatusMock + |> expect(:monitoring, fn -> {:ok, [deployex, FixtureStatus.application()]} end) + |> expect(:subscribe, fn -> Phoenix.PubSub.subscribe(Deployer.PubSub, topic) end) + |> stub(:history_version_list, fn _name, _options -> FixtureStatus.versions() end) + + {:ok, view, _html} = live(conn, ~p"/applications") + + # Send a metric with limit 0 - should not crash the LiveView + zero_limit_data = %ObserverWeb.Telemetry.Data{ + timestamp: :rand.uniform(2_000_000_000_000), + value: 0, + unit: " kilobyte", + tags: %{}, + measurements: %{ + limit: 0, + total: 0 + } + } + + Phoenix.PubSub.broadcast( + Deployer.PubSub, + topic, + {:metrics_new_data, Node.self(), "vm.port.total", zero_limit_data} + ) + + # The LiveView should still be alive and renderable + assert Process.alive?(view.pid) + assert render(view) =~ "Resource Monitoring" + end + + @tag :capture_log + test "GET /applications handles nil memory_total without crashing", %{conn: conn} do + topic = "test-topic" + + deployex = + FixtureStatus.deployex(%{ + node: Node.self(), + monitoring: add_metrics([:memory]) + }) + + Deployer.StatusMock + |> expect(:monitoring, fn -> {:ok, [deployex, FixtureStatus.application()]} end) + |> expect(:subscribe, fn -> Phoenix.PubSub.subscribe(Deployer.PubSub, topic) end) + |> stub(:history_version_list, fn _name, _options -> FixtureStatus.versions() end) + + {:ok, view, _html} = live(conn, ~p"/applications") + + # Send system info with nil memory_total - should not crash the LiveView + send(view.pid, {:update_system_info, %Info{memory_total: nil, memory_free: nil}}) + + # The LiveView should still be alive and renderable + assert Process.alive?(view.pid) + assert render(view) =~ "Resource Monitoring" + end + + @tag :capture_log + test "GET /applications handles zero memory_total without crashing", %{conn: conn} do + topic = "test-topic" + + deployex = + FixtureStatus.deployex(%{ + node: Node.self(), + monitoring: add_metrics([:memory]) + }) + + Deployer.StatusMock + |> expect(:monitoring, fn -> {:ok, [deployex, FixtureStatus.application()]} end) + |> expect(:subscribe, fn -> Phoenix.PubSub.subscribe(Deployer.PubSub, topic) end) + |> stub(:history_version_list, fn _name, _options -> FixtureStatus.versions() end) + + {:ok, view, _html} = live(conn, ~p"/applications") + + # Send system info with 0 memory_total - should not crash the LiveView + send(view.pid, {:update_system_info, %Info{memory_total: 0, memory_free: 0}}) + + # The LiveView should still be alive and renderable + assert Process.alive?(view.pid) + assert render(view) =~ "Resource Monitoring" + end + end + defp add_metrics(metrics, enabled \\ true) do Enum.map(metrics, fn metric -> {metric,