From 3e70f0ba48a57a698baebb88b0fb3cbd0ea68f68 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 10 Mar 2026 14:46:45 -0500 Subject: [PATCH] fix: add detailed error logging for ARP upsert failures Previously just logged a count without any details, making debugging impossible. Now logs first 3 sample errors with IP, MAC, and validation errors to help identify root cause. Sample output: 290 ARP upsert failures for device X sample_errors: [ %{ip: nil, mac: "00:11:22:33:44:55", errors: %{ip_address: ["can't be blank"]}}, %{ip: "10.0.0.1", mac: nil, errors: %{mac_address: ["can't be blank"]}}, ... ] --- lib/towerops/snmp.ex | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/towerops/snmp.ex b/lib/towerops/snmp.ex index 89747471..ad7798d8 100644 --- a/lib/towerops/snmp.ex +++ b/lib/towerops/snmp.ex @@ -1858,6 +1858,26 @@ defmodule Towerops.Snmp do end) end + # Helper to format changeset errors for logging + defp format_changeset_errors(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) + end + + # Helper to collect error samples (first N errors for debugging) + defp maybe_add_error_sample(error_count, samples, attrs, changeset, max_samples \\ 3) do + if error_count < max_samples do + error_details = %{ + ip: Map.get(attrs, :ip_address), + mac: Map.get(attrs, :mac_address), + errors: format_changeset_errors(changeset) + } + + [error_details | samples] + else + samples + end + end + @doc """ Bulk upserts ARP entries for a device. Returns `{success_count, error_count}` tuple. @@ -1866,7 +1886,7 @@ defmodule Towerops.Snmp do interface_map = Map.new(interfaces, fn i -> {i.if_index, i.id} end) arp_entries - |> Enum.reduce({0, 0}, fn entry, {s, e} -> + |> Enum.reduce({0, 0, []}, fn entry, {s, e, error_samples} -> interface_id = Map.get(interface_map, entry.if_index) attrs = @@ -1875,18 +1895,25 @@ defmodule Towerops.Snmp do |> Map.put(:interface_id, interface_id) case upsert_arp_entry(attrs) do - {:ok, _} -> {s + 1, e} - {:error, _} -> {s, e + 1} + {:ok, _} -> + {s + 1, e, error_samples} + + {:error, changeset} -> + # Sample first 3 errors for debugging + sample = maybe_add_error_sample(e, error_samples, attrs, changeset) + {s, e + 1, sample} end end) - |> tap(fn {_s, errors} -> + |> tap(fn {_s, errors, error_samples} -> if errors > 0 do Logger.warning("#{errors} ARP upsert failures for device #{device_id}", device_id: device_id, - error_count: errors + error_count: errors, + sample_errors: Enum.reverse(error_samples) ) end end) + |> then(fn {s, e, _samples} -> {s, e} end) end # MAC Address queries