diff --git a/lib/towerops/alerts.ex b/lib/towerops/alerts.ex index 7e1b939b..ccbb88b4 100644 --- a/lib/towerops/alerts.ex +++ b/lib/towerops/alerts.ex @@ -36,7 +36,29 @@ defmodule Towerops.Alerts do end end rescue - _ -> :ok + # Only rescue specific expected errors, log unexpected ones + error in [Ecto.QueryError, DBConnection.ConnectionError] -> + require Logger + + Logger.warning( + "Failed to compute GAIIA impact for alert #{alert.id} due to database error: #{inspect(error)}", + alert_id: alert.id, + device_id: device_id + ) + + :ok + + error -> + require Logger + + Logger.error( + "Unexpected error computing GAIIA impact for alert #{alert.id}: #{inspect(error)}", + alert_id: alert.id, + device_id: device_id, + error: error + ) + + :ok end defp maybe_compute_gaiia_impact(_alert), do: :ok diff --git a/lib/towerops/alerts/alert.ex b/lib/towerops/alerts/alert.ex index 788c2fc1..ad62edc9 100644 --- a/lib/towerops/alerts/alert.ex +++ b/lib/towerops/alerts/alert.ex @@ -2,7 +2,7 @@ defmodule Towerops.Alerts.Alert do @moduledoc """ Alert schema for device monitoring alerts. - Tracks device_up, device_down, and sensor threshold alerts with + Tracks device_up, device_down, and monitoring check alerts with acknowledgement status and auto-resolution. """ use Ecto.Schema @@ -12,11 +12,14 @@ defmodule Towerops.Alerts.Alert do alias Ecto.Association.NotLoaded alias Towerops.Accounts.User alias Towerops.Devices.Device + alias Towerops.Monitoring.Check @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "alerts" do - field :alert_type, Ecto.Enum, values: [:device_down, :device_up] + # Alert type: device_down, device_up for device alerts + # For check alerts, uses string like "check_http", "check_ping", etc. + field :alert_type, :string field :triggered_at, :utc_datetime field :acknowledged_at, :utc_datetime field :resolved_at, :utc_datetime @@ -24,7 +27,14 @@ defmodule Towerops.Alerts.Alert do field :message, :string field :gaiia_impact, :map + # Check-related fields (added in ExtendAlertsForChecks migration) + field :severity, :integer + field :notification_sent, :boolean, default: false + field :notification_sent_at, :utc_datetime + field :output, :string + belongs_to :device, Device + belongs_to :check, Check belongs_to :acknowledged_by, User timestamps(type: :utc_datetime) @@ -32,15 +42,21 @@ defmodule Towerops.Alerts.Alert do @type t :: %__MODULE__{ id: Ecto.UUID.t(), - alert_type: :device_down | :device_up, + alert_type: String.t(), triggered_at: DateTime.t(), acknowledged_at: DateTime.t() | nil, resolved_at: DateTime.t() | nil, email_sent_at: DateTime.t() | nil, message: String.t() | nil, gaiia_impact: map() | nil, - device_id: Ecto.UUID.t(), - device: NotLoaded.t() | Device.t(), + severity: integer() | nil, + notification_sent: boolean(), + notification_sent_at: DateTime.t() | nil, + output: String.t() | nil, + device_id: Ecto.UUID.t() | nil, + device: NotLoaded.t() | Device.t() | nil, + check_id: Ecto.UUID.t() | nil, + check: NotLoaded.t() | Check.t() | nil, acknowledged_by_id: Ecto.UUID.t() | nil, acknowledged_by: NotLoaded.t() | User.t() | nil, inserted_at: DateTime.t(), @@ -52,6 +68,7 @@ defmodule Towerops.Alerts.Alert do alert |> cast(attrs, [ :device_id, + :check_id, :alert_type, :triggered_at, :acknowledged_at, @@ -59,10 +76,29 @@ defmodule Towerops.Alerts.Alert do :resolved_at, :email_sent_at, :message, - :gaiia_impact + :gaiia_impact, + :severity, + :notification_sent, + :notification_sent_at, + :output ]) - |> validate_required([:device_id, :alert_type, :triggered_at]) + |> validate_required([:alert_type, :triggered_at]) + |> validate_inclusion(:severity, [1, 2]) + |> validate_device_or_check() |> foreign_key_constraint(:device_id) + |> foreign_key_constraint(:check_id) |> foreign_key_constraint(:acknowledged_by_id) end + + # Ensure either device_id or check_id is present (but not required for flexibility) + defp validate_device_or_check(changeset) do + device_id = get_field(changeset, :device_id) + check_id = get_field(changeset, :check_id) + + if is_nil(device_id) and is_nil(check_id) do + add_error(changeset, :base, "either device_id or check_id must be present") + else + changeset + end + end end diff --git a/lib/towerops/devices.ex b/lib/towerops/devices.ex index 3e432eb7..d9d51b68 100644 --- a/lib/towerops/devices.ex +++ b/lib/towerops/devices.ex @@ -245,7 +245,21 @@ defmodule Towerops.Devices do defp get_org_default_agent(device) do # Use direct organization relationship instead of site.organization - device.organization.default_agent_token_id + # Guard against NotLoaded to prevent crashes if association isn't preloaded + case device.organization do + %Ecto.Association.NotLoaded{} -> + require Logger + + Logger.error( + "Organization not preloaded for device #{device.id} in get_org_default_agent", + device_id: device.id + ) + + nil + + organization -> + organization.default_agent_token_id + end end defp get_global_default_agent, do: Towerops.Settings.get_global_default_cloud_poller() diff --git a/lib/towerops/snmp/profiles/base.ex b/lib/towerops/snmp/profiles/base.ex index dd37ebd3..8170ee9c 100644 --- a/lib/towerops/snmp/profiles/base.ex +++ b/lib/towerops/snmp/profiles/base.ex @@ -172,14 +172,28 @@ defmodule Towerops.Snmp.Profiles.Base do case Client.get_multiple(client_opts, oids) do {:ok, values} -> - system_info = - @system_oids - |> Map.keys() - |> Enum.zip(values) - |> Map.new() - |> parse_system_info() + keys = Map.keys(@system_oids) - {:ok, system_info} + # Validate we got the expected number of values + if length(values) == length(keys) do + system_info = + keys + |> Enum.zip(values) + |> Map.new() + |> parse_system_info() + + {:ok, system_info} + else + Logger.error( + "SNMP value count mismatch: expected #{length(keys)}, got #{length(values)}", + expected: length(keys), + actual: length(values), + keys: keys, + values: values + ) + + {:error, :value_count_mismatch} + end {:error, reason} = error -> Logger.error("Failed to discover system info: #{inspect(reason)}") diff --git a/lib/towerops/workers/device_monitor_worker.ex b/lib/towerops/workers/device_monitor_worker.ex index 01b0b41e..758aefc4 100644 --- a/lib/towerops/workers/device_monitor_worker.ex +++ b/lib/towerops/workers/device_monitor_worker.ex @@ -202,6 +202,7 @@ defmodule Towerops.Workers.DeviceMonitorWorker do if Alerts.has_active_alert?(device.id, :device_down) do :ok else + # Single maintenance check (removed duplicate from create_device_down_alert) if Maintenance.device_in_maintenance?(device.id) do Logger.info("Device #{device.id} (#{device.name}) is down but in maintenance window — suppressing alert") :ok @@ -212,30 +213,50 @@ defmodule Towerops.Workers.DeviceMonitorWorker do end defp create_device_down_alert(device, now) do - if Maintenance.device_in_maintenance?(device.id) do - require Logger + alert_message = get_down_alert_message(device) - Logger.info("Device #{device.id} (#{device.name}) alert suppressed by maintenance window (safety net)") - :ok - else - alert_message = get_down_alert_message(device) + # Use case instead of pattern match to handle errors gracefully + case Alerts.create_alert(%{ + device_id: device.id, + alert_type: :device_down, + triggered_at: now, + message: alert_message + }) do + {:ok, alert} -> + # Wrap notification in try-catch to prevent silent failures + Task.start(fn -> + try do + Notifier.notify_trigger(alert, device) + rescue + error -> + require Logger - {:ok, alert} = - Alerts.create_alert(%{ - device_id: device.id, - alert_type: :device_down, - triggered_at: now, - message: alert_message - }) + Logger.error( + "Failed to send notification for device down alert: #{inspect(error)}", + alert_id: alert.id, + device_id: device.id, + error: error + ) + end + end) - Task.start(fn -> Notifier.notify_trigger(alert, device) end) + _ = + Phoenix.PubSub.broadcast( + Towerops.PubSub, + "alerts:org:#{device.organization_id}:new", + {:new_alert, device.id, :device_down} + ) - _ = - Phoenix.PubSub.broadcast( - Towerops.PubSub, - "alerts:org:#{device.organization_id}:new", - {:new_alert, device.id, :device_down} + :ok + + {:error, reason} -> + require Logger + + Logger.error("Failed to create device down alert for #{device.name}: #{inspect(reason)}", + device_id: device.id ) + + {:error, reason} end end @@ -259,25 +280,52 @@ defmodule Towerops.Workers.DeviceMonitorWorker do defp handle_equipment_up(device, now) do recovery_message = get_recovery_message(device) - {:ok, alert} = - Alerts.create_alert(%{ - device_id: device.id, - alert_type: :device_up, - triggered_at: now, - resolved_at: now, - message: recovery_message - }) + # Use case instead of pattern match to handle errors gracefully + case Alerts.create_alert(%{ + device_id: device.id, + alert_type: :device_up, + triggered_at: now, + resolved_at: now, + message: recovery_message + }) do + {:ok, alert} -> + # Wrap notification in try-catch to prevent silent failures + Task.start(fn -> + try do + Notifier.notify_trigger(alert, device) + rescue + error -> + require Logger - Task.start(fn -> Notifier.notify_trigger(alert, device) end) + Logger.error( + "Failed to send notification for device up alert: #{inspect(error)}", + alert_id: alert.id, + device_id: device.id, + error: error + ) + end + end) - resolve_down_alert(device) + resolve_down_alert(device) - _ = - Phoenix.PubSub.broadcast( - Towerops.PubSub, - "alerts:org:#{device.organization_id}:resolved", - {:alert_resolved, device.id, :device_down} - ) + _ = + Phoenix.PubSub.broadcast( + Towerops.PubSub, + "alerts:org:#{device.organization_id}:resolved", + {:alert_resolved, device.id, :device_down} + ) + + :ok + + {:error, reason} -> + require Logger + + Logger.error("Failed to create device up alert for #{device.name}: #{inspect(reason)}", + device_id: device.id + ) + + {:error, reason} + end end defp get_recovery_message(device) do diff --git a/lib/towerops/workers/device_poller_worker.ex b/lib/towerops/workers/device_poller_worker.ex index e05a4970..1e23961c 100644 --- a/lib/towerops/workers/device_poller_worker.ex +++ b/lib/towerops/workers/device_poller_worker.ex @@ -210,8 +210,21 @@ defmodule Towerops.Workers.DevicePollerWorker do ] # Wait for all tasks with timeout, log failures - tasks - |> Task.yield_many(30_000) + # Explicitly pair results with names to handle length mismatches + results = Task.yield_many(tasks, 30_000) + + # Validate we got results for all tasks + if length(results) != length(task_names) do + Logger.error( + "Task result count mismatch for #{device.name}: expected #{length(task_names)}, got #{length(results)}", + device_id: device.id, + expected: length(task_names), + actual: length(results) + ) + end + + # Process results with explicit pairing to handle any length mismatch + results |> Enum.zip(task_names) |> Enum.each(fn {{task, result}, name} -> case result do @@ -1041,11 +1054,15 @@ defmodule Towerops.Workers.DevicePollerWorker do end defp build_oper_status_event(interface, current_attrs, device_id, timestamp) do + # Handle nil status values from SNMP failures + old_status = interface.if_oper_status || "unknown" + new_status = current_attrs.if_oper_status || "unknown" + message = if interface.if_oper_status do - "Interface #{interface.if_name} changed from #{String.upcase(interface.if_oper_status)} to #{String.upcase(current_attrs.if_oper_status)}" + "Interface #{interface.if_name} changed from #{String.upcase(old_status)} to #{String.upcase(new_status)}" else - "Interface #{interface.if_name} is now #{String.upcase(current_attrs.if_oper_status)}" + "Interface #{interface.if_name} is now #{String.upcase(new_status)}" end %{ diff --git a/lib/towerops_web/channels/agent_channel.ex b/lib/towerops_web/channels/agent_channel.ex index 3cba17ad..b71f88be 100644 --- a/lib/towerops_web/channels/agent_channel.ex +++ b/lib/towerops_web/channels/agent_channel.ex @@ -731,9 +731,10 @@ defmodule ToweropsWeb.AgentChannel do defp needs_discovery?(device) do # Need discovery if no SNMP device or last discovery was >24 hours ago + # Clamp diff to 0 to handle clock skew (future timestamps) is_nil(device.snmp_device) or is_nil(device.last_discovery_at) or - DateTime.diff(DateTime.utc_now(), device.last_discovery_at, :hour) > 24 + max(DateTime.diff(DateTime.utc_now(), device.last_discovery_at, :hour), 0) > 24 end defp mikrotik_device?(device) do diff --git a/lib/towerops_web/live/activity_feed_live.ex b/lib/towerops_web/live/activity_feed_live.ex index 746363b4..bcac858d 100644 --- a/lib/towerops_web/live/activity_feed_live.ex +++ b/lib/towerops_web/live/activity_feed_live.ex @@ -15,18 +15,20 @@ defmodule ToweropsWeb.ActivityFeedLive do def mount(_params, _session, socket) do organization = socket.assigns.current_scope.organization - if connected?(socket) do - # Subscribe to all relevant PubSub topics for real-time updates - Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:org:#{organization.id}:new") - Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:org:#{organization.id}:resolved") - Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{organization.id}") - Phoenix.PubSub.subscribe(Towerops.PubSub, "config:org:#{organization.id}") - Phoenix.PubSub.subscribe(Towerops.PubSub, "sync:org:#{organization.id}") - Phoenix.PubSub.subscribe(Towerops.PubSub, "device_events:org:#{organization.id}") + timer_ref = + if connected?(socket) do + # Subscribe to all relevant PubSub topics for real-time updates + Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:org:#{organization.id}:new") + Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:org:#{organization.id}:resolved") + Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{organization.id}") + Phoenix.PubSub.subscribe(Towerops.PubSub, "config:org:#{organization.id}") + Phoenix.PubSub.subscribe(Towerops.PubSub, "sync:org:#{organization.id}") + Phoenix.PubSub.subscribe(Towerops.PubSub, "device_events:org:#{organization.id}") - # Timer for relative timestamp refresh - :timer.send_interval(@tick_interval_ms, self(), :tick) - end + # Timer for relative timestamp refresh + {:ok, ref} = :timer.send_interval(@tick_interval_ms, self(), :tick) + ref + end {:ok, socket @@ -36,6 +38,7 @@ defmodule ToweropsWeb.ActivityFeedLive do |> assign(:has_more, true) |> assign(:search, "") |> assign(:now, DateTime.utc_now()) + |> assign(:timer_ref, timer_ref) |> load_activity() |> load_counts()} end @@ -138,6 +141,15 @@ defmodule ToweropsWeb.ActivityFeedLive do {:noreply, socket} end + @impl true + def terminate(_reason, socket) do + if timer_ref = socket.assigns[:timer_ref] do + :timer.cancel(timer_ref) + end + + :ok + end + # --- Helpers --- defp filter_options do diff --git a/lib/towerops_web/live/admin/agent_live/index.ex b/lib/towerops_web/live/admin/agent_live/index.ex index 669bea7b..0498b3db 100644 --- a/lib/towerops_web/live/admin/agent_live/index.ex +++ b/lib/towerops_web/live/admin/agent_live/index.ex @@ -8,11 +8,13 @@ defmodule ToweropsWeb.Admin.AgentLive.Index do @impl true def mount(_params, _session, socket) do - if connected?(socket) do - Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health") - Phoenix.PubSub.subscribe(Towerops.PubSub, "admin:agents") - :timer.send_interval(1000, :tick) - end + timer_ref = + if connected?(socket) do + Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health") + Phoenix.PubSub.subscribe(Towerops.PubSub, "admin:agents") + {:ok, ref} = :timer.send_interval(1000, :tick) + ref + end agent_tokens = Agents.list_all_agent_tokens() {cloud_pollers, org_agents} = Enum.split_with(agent_tokens, & &1.is_cloud_poller) @@ -27,7 +29,8 @@ defmodule ToweropsWeb.Admin.AgentLive.Index do |> assign(:has_cloud_pollers, cloud_pollers != []) |> assign(:has_org_agents, org_agents != []) |> assign(:device_counts, device_counts) - |> assign(:now, DateTime.utc_now())} + |> assign(:now, DateTime.utc_now()) + |> assign(:timer_ref, timer_ref)} end @impl true @@ -104,4 +107,13 @@ defmodule ToweropsWeb.Admin.AgentLive.Index do {t.id, %{direct: direct, total: total}} end) end + + @impl true + def terminate(_reason, socket) do + if timer_ref = socket.assigns[:timer_ref] do + :timer.cancel(timer_ref) + end + + :ok + end end diff --git a/lib/towerops_web/live/agent_live/index.ex b/lib/towerops_web/live/agent_live/index.ex index c5bb4b3a..14d48cb8 100644 --- a/lib/towerops_web/live/agent_live/index.ex +++ b/lib/towerops_web/live/agent_live/index.ex @@ -19,10 +19,12 @@ defmodule ToweropsWeb.AgentLive.Index do agent_tokens = Agents.list_organization_agent_tokens(organization.id) # Subscribe to agent health updates for real-time status changes - if connected?(socket) do - _ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health") - :timer.send_interval(1000, :tick) - end + timer_ref = + if connected?(socket) do + _ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agents:health") + {:ok, ref} = :timer.send_interval(1000, :tick) + ref + end # If superadmin (including when impersonating), also load cloud pollers and global default {cloud_pollers, global_default_cloud_poller_id} = @@ -72,6 +74,7 @@ defmodule ToweropsWeb.AgentLive.Index do |> assign(:new_token, nil) |> assign(:show_token_modal, false) |> assign(:now, DateTime.utc_now()) + |> assign(:timer_ref, timer_ref) |> assign(:agent_form, to_form(%{"name" => "", "is_cloud_poller" => false}))} end @@ -468,4 +471,13 @@ defmodule ToweropsWeb.AgentLive.Index do |> assign(:agent_health_stats, agent_health_stats) |> assign(:offline_agents, offline_agents) end + + @impl true + def terminate(_reason, socket) do + if timer_ref = socket.assigns[:timer_ref] do + :timer.cancel(timer_ref) + end + + :ok + end end diff --git a/lib/towerops_web/live/device_live/form.ex b/lib/towerops_web/live/device_live/form.ex index fd18cc8f..78b81184 100644 --- a/lib/towerops_web/live/device_live/form.ex +++ b/lib/towerops_web/live/device_live/form.ex @@ -669,6 +669,11 @@ defmodule ToweropsWeb.DeviceLive.Form do "LiveView received credential test result: test_id=#{result.test_id} success=#{result.success} system_description=#{inspect(result.system_description)} error=#{inspect(result.error_message)}" ) + # Unsubscribe from this specific test topic since we got the result + if test_id = socket.assigns[:test_id] do + Phoenix.PubSub.unsubscribe(Towerops.PubSub, "credential_test:#{test_id}") + end + test_result = if result.success do %{ @@ -684,9 +689,13 @@ defmodule ToweropsWeb.DeviceLive.Form do Logger.info("Setting snmp_test_result assign: #{inspect(test_result)}") - {:noreply, assign(socket, :snmp_test_result, test_result)} + {:noreply, socket |> assign(:snmp_test_result, test_result) |> assign(:test_id, nil)} end + # Catch-all for unexpected messages + @impl true + def handle_info(_msg, socket), do: {:noreply, socket} + # Determine effective agent ID considering inheritance chain defp determine_effective_agent_id(device_params, assigns) do # Check device-level agent @@ -839,4 +848,14 @@ defmodule ToweropsWeb.DeviceLive.Form do # 100.64.0.0/10 (RFC6598 CGNAT) defp non_routable_ipv4_range?(100, b) when b >= 64 and b <= 127, do: true defp non_routable_ipv4_range?(_a, _b), do: false + + @impl true + def terminate(_reason, socket) do + # Unsubscribe from credential test topic if we're still subscribed + if test_id = socket.assigns[:test_id] do + Phoenix.PubSub.unsubscribe(Towerops.PubSub, "credential_test:#{test_id}") + end + + :ok + end end diff --git a/lib/towerops_web/live/device_live/index.ex b/lib/towerops_web/live/device_live/index.ex index bc6e17a7..3a6cb5cb 100644 --- a/lib/towerops_web/live/device_live/index.ex +++ b/lib/towerops_web/live/device_live/index.ex @@ -77,8 +77,8 @@ defmodule ToweropsWeb.DeviceLive.Index do socket |> assign(:active_tab, "discovered") - # Use temporary assign - will be cleared after render - |> assign_new(:discovered_devices, fn -> discovered_devices end) + # Use regular assign to prevent state bleeding between tab switches + |> assign(:discovered_devices, discovered_devices) |> assign(:pagination, %{ page: page, per_page: per_page, diff --git a/lib/towerops_web/live/graph_live/show.ex b/lib/towerops_web/live/graph_live/show.ex index a294c78f..9a621236 100644 --- a/lib/towerops_web/live/graph_live/show.ex +++ b/lib/towerops_web/live/graph_live/show.ex @@ -790,6 +790,10 @@ defmodule ToweropsWeb.GraphLive.Show do end end + # Catch-all for unexpected messages + @impl true + def handle_info(_msg, socket), do: {:noreply, socket} + # Perform SNMP polling and update buffer defp perform_live_poll(socket) do device_id = socket.assigns.device_id diff --git a/lib/towerops_web/live/mobile_qr_live.ex b/lib/towerops_web/live/mobile_qr_live.ex index e34b7820..266ca80b 100644 --- a/lib/towerops_web/live/mobile_qr_live.ex +++ b/lib/towerops_web/live/mobile_qr_live.ex @@ -17,7 +17,7 @@ defmodule ToweropsWeb.MobileQRLive do {:ok, qr_token} = MobileSessions.create_qr_login_token(user.id) # Start polling to check if token has been completed - _ = + timer_ref = if connected?(socket) do schedule_check() end @@ -27,6 +27,7 @@ defmodule ToweropsWeb.MobileQRLive do |> assign(:qr_token, qr_token) |> assign(:completed, false) |> assign(:mobile_session, nil) + |> assign(:timer_ref, timer_ref) {:ok, socket} end @@ -43,35 +44,57 @@ defmodule ToweropsWeb.MobileQRLive do user = socket.assigns.current_scope.user {:ok, qr_token} = MobileSessions.create_qr_login_token(user.id) + timer_ref = schedule_check() + socket = socket |> assign(:qr_token, qr_token) + |> assign(:timer_ref, timer_ref) |> put_flash(:info, t("QR code expired, generated a new one")) - schedule_check() {:noreply, socket} else # Still valid, check again - schedule_check() - {:noreply, socket} + timer_ref = schedule_check() + {:noreply, assign(socket, :timer_ref, timer_ref)} end mobile_session -> - # Token was completed! + # Token was completed! Cancel timer since we're done + cancel_timer(socket.assigns.timer_ref) + socket = socket |> assign(:completed, true) |> assign(:mobile_session, mobile_session) + |> assign(:timer_ref, nil) |> put_flash(:info, t("Mobile device authenticated successfully!")) {:noreply, socket} end end + # Catch-all for unexpected messages + @impl true + def handle_info(_msg, socket), do: {:noreply, socket} + + @impl true + def terminate(_reason, socket) do + cancel_timer(socket.assigns[:timer_ref]) + :ok + end + defp schedule_check do Process.send_after(self(), :check_completion, 2000) end + defp cancel_timer(nil), do: :ok + + defp cancel_timer(timer_ref) when is_reference(timer_ref) do + Process.cancel_timer(timer_ref) + :ok + end + # Generate QR code data URL using qrcode.show API (public service) defp qr_code_url(token) do # Base URL for the mobile app to handle the token diff --git a/lib/towerops_web/plugs/filter_noisy_logs.ex b/lib/towerops_web/plugs/filter_noisy_logs.ex index 41b71ee4..39756aa3 100644 --- a/lib/towerops_web/plugs/filter_noisy_logs.ex +++ b/lib/towerops_web/plugs/filter_noisy_logs.ex @@ -7,22 +7,29 @@ defmodule ToweropsWeb.Plugs.FilterNoisyLogs do Currently filters: - GET /health (Kubernetes health checks) + - GET /health/time (Kubernetes health checks with time sync verification) - HEAD / (external uptime monitors) """ import Plug.Conn + require Logger + def init(opts), do: opts def call(conn, _opts) do if should_filter?(conn) do # Setting phoenix_log to false prevents Phoenix.Logger from logging this request - put_private(conn, :phoenix_log, false) + # Also disable telemetry logging + conn + |> put_private(:phoenix_log, false) + |> put_private(:plug_skip_telemetry, true) else conn end end defp should_filter?(%{method: "GET", path_info: ["health"]}), do: true + defp should_filter?(%{method: "GET", path_info: ["health", "time"]}), do: true defp should_filter?(%{method: "HEAD", path_info: []}), do: true defp should_filter?(_), do: false end diff --git a/priv/repo/migrations/20260305144327_fix_equipment_indexes_after_rename.exs b/priv/repo/migrations/20260305144327_fix_equipment_indexes_after_rename.exs new file mode 100644 index 00000000..c7890f4e --- /dev/null +++ b/priv/repo/migrations/20260305144327_fix_equipment_indexes_after_rename.exs @@ -0,0 +1,27 @@ +defmodule Towerops.Repo.Migrations.FixEquipmentIndexesAfterRename do + use Ecto.Migration + + def up do + # The 20260115011502_add_alerts_equipment_indexes migration created indexes + # on the equipment table with equipment-prefixed names. + # The 20260117190134_rename_equipment_to_devices migration renamed the table + # but didn't rename these specific indexes. + + # Rename equipment indexes to device indexes + execute "ALTER INDEX IF EXISTS idx_equipment_snmp_poll RENAME TO idx_devices_snmp_poll" + + execute "ALTER INDEX IF EXISTS idx_equipment_snmp_monitoring_site RENAME TO idx_devices_snmp_monitoring_site" + + # The alerts index references device_id now (column was renamed), but name still says equipment + execute "ALTER INDEX IF EXISTS idx_alerts_equipment_type_unresolved RENAME TO idx_alerts_device_type_unresolved" + end + + def down do + # Reverse the renames if rolling back + execute "ALTER INDEX IF EXISTS idx_devices_snmp_poll RENAME TO idx_equipment_snmp_poll" + + execute "ALTER INDEX IF EXISTS idx_devices_snmp_monitoring_site RENAME TO idx_equipment_snmp_monitoring_site" + + execute "ALTER INDEX IF EXISTS idx_alerts_device_type_unresolved RENAME TO idx_alerts_equipment_type_unresolved" + end +end