aprs.me/lib/aprsme/error_handler.ex

380 lines
11 KiB
Elixir

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