defmodule Aprsme.ErrorHandler do @moduledoc """ Centralized error handling and recovery patterns for the application """ alias Aprsme.LogSanitizer alias Ecto.Query.CastError require Logger @doc """ Handle and log database errors with context """ def handle_database_error(error, context \\ %{}) do sanitized_context = LogSanitizer.sanitize_error_context(context) case error do %CastError{} = cast_error -> Logger.error("Database cast error - invalid data type", error_details: LogSanitizer.log_data( error_type: :cast_error, message: cast_error.message, context: sanitized_context ) ) {:error, :invalid_data} %Ecto.ConstraintError{} = constraint_error -> Logger.error("Database constraint violation", error_details: LogSanitizer.log_data( error_type: :constraint_violation, constraint: constraint_error.constraint, context: sanitized_context ) ) {:error, :constraint_violation} %Postgrex.Error{postgres: %{code: :unique_violation}} -> Logger.warning("Attempted to insert duplicate record", error_details: LogSanitizer.log_data( error_type: :duplicate_record, context: sanitized_context ) ) {:error, :duplicate_record} %DBConnection.ConnectionError{} -> Logger.error("Database connection error", error_details: LogSanitizer.log_data( error_type: :connection_error, context: sanitized_context ) ) {:error, :database_unavailable} %Ecto.QueryError{} = query_error -> Logger.error("Database query error", error_details: LogSanitizer.log_data( error_type: :query_error, message: query_error.message, context: sanitized_context ) ) {:error, :query_failed} error -> Logger.error("Unexpected database error", error_details: LogSanitizer.log_data( error_type: :unknown_database_error, error: LogSanitizer.sanitize(error), context: sanitized_context ) ) {:error, :database_error} end end @doc """ Handle network and external service errors """ def handle_network_error(error, service_name, context \\ %{}) do sanitized_context = LogSanitizer.sanitize_error_context(context) case error do %Req.TransportError{reason: :timeout} -> Logger.warning("Network request timeout", network_error: LogSanitizer.log_data( service: service_name, error_type: :timeout, context: sanitized_context ) ) {:error, :timeout} %Req.TransportError{reason: :econnrefused} -> Logger.error("Connection refused by service", network_error: LogSanitizer.log_data( service: service_name, error_type: :connection_refused, context: sanitized_context ) ) {:error, :service_unavailable} %Req.TransportError{reason: reason} -> Logger.error("Network transport error", network_error: LogSanitizer.log_data( service: service_name, error_type: :transport_error, reason: reason, context: sanitized_context ) ) {:error, :network_error} {:error, :circuit_open} -> Logger.warning("Circuit breaker open for service", network_error: LogSanitizer.log_data( service: service_name, error_type: :circuit_open, context: sanitized_context ) ) {:error, :service_unavailable} error -> Logger.error("Unexpected network error", network_error: LogSanitizer.log_data( service: service_name, error_type: :unknown_network_error, error: LogSanitizer.sanitize(error), context: sanitized_context ) ) {:error, :network_error} end end @doc """ Handle packet processing errors with recovery strategies """ def handle_packet_error(error, packet_data, context \\ %{}) do _sanitized_packet = LogSanitizer.sanitize_packet_data(packet_data) sanitized_context = LogSanitizer.sanitize_error_context(context) case error do %Jason.DecodeError{} -> Logger.warning("Invalid JSON in packet data", packet_error: LogSanitizer.log_data( error_type: :invalid_json, packet_sender: Map.get(packet_data, :sender, "unknown"), context: sanitized_context ) ) store_bad_packet(packet_data, "invalid_json") {:error, :invalid_json} %ArgumentError{message: message} -> if String.contains?(message, "invalid") do Logger.warning("Invalid packet format", packet_error: LogSanitizer.log_data( error_type: :invalid_format, packet_sender: Map.get(packet_data, :sender, "unknown"), message: message, context: sanitized_context ) ) store_bad_packet(packet_data, "invalid_format") {:error, :invalid_format} else Logger.error("Unexpected packet processing error", packet_error: LogSanitizer.log_data( error_type: :processing_error, packet_sender: Map.get(packet_data, :sender, "unknown"), error: LogSanitizer.sanitize(message), context: sanitized_context ) ) store_bad_packet(packet_data, "processing_error") {:error, :processing_failed} end error -> Logger.error("Unexpected packet processing error", packet_error: LogSanitizer.log_data( error_type: :processing_error, packet_sender: Map.get(packet_data, :sender, "unknown"), error: LogSanitizer.sanitize(error), context: sanitized_context ) ) store_bad_packet(packet_data, "processing_error") {:error, :processing_failed} end end @doc """ Handle GenServer and process errors """ def handle_process_error(error, process_name, context \\ %{}) do sanitized_context = LogSanitizer.sanitize_error_context(context) case error do :timeout -> Logger.warning("Process operation timeout", process_error: LogSanitizer.log_data( process: process_name, error_type: :timeout, context: sanitized_context ) ) {:error, :timeout} {:noproc, _} -> Logger.error("Process not found or died", process_error: LogSanitizer.log_data( process: process_name, error_type: :process_died, context: sanitized_context ) ) {:error, :process_unavailable} {:badrpc, reason} -> Logger.error("RPC call failed", process_error: LogSanitizer.log_data( process: process_name, error_type: :rpc_failed, reason: reason, context: sanitized_context ) ) {:error, :rpc_failed} error -> Logger.error("Unexpected process error", process_error: LogSanitizer.log_data( process: process_name, error_type: :unknown_process_error, error: LogSanitizer.sanitize(error), context: sanitized_context ) ) {:error, :process_error} end end @doc """ Execute function with comprehensive error handling and recovery """ def with_error_handling(fun, opts \\ []) when is_function(fun, 0) do context = Keyword.get(opts, :context, %{}) max_retries = Keyword.get(opts, :max_retries, 0) retry_delay = Keyword.get(opts, :retry_delay, 1000) do_with_retries(fun, max_retries, retry_delay, context, 0) end defp do_with_retries(fun, max_retries, retry_delay, context, attempt) do {:ok, fun.()} rescue error -> if attempt < max_retries do Logger.warning("Retrying operation after error", retry_info: LogSanitizer.log_data( attempt: attempt + 1, max_retries: max_retries, delay_ms: retry_delay, error: LogSanitizer.sanitize(error) ) ) Process.sleep(retry_delay) do_with_retries(fun, max_retries, retry_delay, context, attempt + 1) else categorize_and_handle_error(error, context) end catch kind, error -> Logger.error("Unexpected error in operation", error_details: LogSanitizer.log_data( kind: kind, error: LogSanitizer.sanitize(error), context: LogSanitizer.sanitize_error_context(context) ) ) {:error, :unexpected_error} end defp categorize_and_handle_error(error, context) do cond do database_error?(error) -> handle_database_error(error, context) network_error?(error) -> handle_network_error(error, :unknown_service, context) true -> Logger.error("Uncategorized error", error_details: LogSanitizer.log_data( error: LogSanitizer.sanitize(error), context: LogSanitizer.sanitize_error_context(context) ) ) {:error, :unknown_error} end end defp database_error?(%CastError{}), do: true defp database_error?(%Ecto.ConstraintError{}), do: true defp database_error?(%Postgrex.Error{}), do: true defp database_error?(%DBConnection.ConnectionError{}), do: true defp database_error?(%Ecto.QueryError{}), do: true defp database_error?(_), do: false defp network_error?(%Req.TransportError{}), do: true defp network_error?(%{reason: :circuit_open}), do: true defp network_error?(%{reason: :timeout}), do: true defp network_error?(%RuntimeError{message: "circuit_open"}), do: true defp network_error?(%RuntimeError{message: "timeout"}), do: true defp network_error?(_), do: false defp store_bad_packet(packet_data, reason) do # This would store the bad packet for later analysis # For now, just log it since BadPacket module may not exist Logger.warning("Storing bad packet for analysis", bad_packet: LogSanitizer.log_data( raw_packet: inspect(packet_data), error_reason: reason, received_at: DateTime.utc_now() ) ) :ok end end