defmodule AprsmeWeb.Plugs.HealthCheck do @moduledoc """ Health check plug that returns appropriate status based on application state. Used by Kubernetes liveness and readiness probes. """ import Plug.Conn alias Ecto.Adapters.SQL def init(opts), do: opts def call(%{request_path: "/health"} = conn, opts) do probe_type = opts[:probe_type] || :readiness case check_health(probe_type) do {:ok, message} -> conn |> put_resp_content_type("text/plain") |> send_resp(200, message) |> halt() {:error, message} -> conn |> put_resp_content_type("text/plain") |> send_resp(503, message) |> halt() end end def call(conn, _opts), do: conn defp check_health(:liveness) do # Liveness probe - only fails if the application is truly broken # Continue returning OK even during shutdown to prevent unnecessary restarts case basic_health_checks() do :ok -> {:ok, "OK"} {:error, reason} -> {:error, "Liveness check failed: #{reason}"} end end defp check_health(:readiness) do # Readiness probe - fails when shutting down to stop new traffic. health_status = Application.get_env(:aprsme, :health_status, :healthy) readiness_status(health_status, shutting_down?()) end defp readiness_status(:draining, _shutting_down?), do: {:error, "Application is draining connections"} defp readiness_status(_status, true), do: {:error, "Application is shutting down"} defp readiness_status(_status, false) do case full_health_checks() do :ok -> {:ok, "OK"} {:error, reason} -> {:error, "Readiness check failed: #{reason}"} end end # Basic checks for liveness # Check if the application is running defp basic_health_checks do _ = Application.get_env(:aprsme, :env) :ok rescue _ -> {:error, "Application not responding"} end defp full_health_checks do # Comprehensive checks for readiness with :ok <- check_database_connection() do check_pubsub() end end defp check_database_connection do case SQL.query(Aprsme.Repo, "SELECT 1", [], timeout: 1000) do {:ok, _} -> :ok _ -> {:error, "Database connection failed"} end rescue _ -> {:error, "Database check failed"} end defp check_pubsub do _ = Phoenix.PubSub.broadcast(Aprsme.PubSub, "health_check", :ping) :ok rescue _ -> {:error, "PubSub check failed"} end defp shutting_down? do # Check if ShutdownHandler process exists and is shutting down Aprsme.ShutdownHandler |> Process.whereis() |> ask_shutting_down?() end # No ShutdownHandler process → definitely not shutting down. defp ask_shutting_down?(nil), do: false defp ask_shutting_down?(pid) when is_pid(pid) do ask_if_alive(Process.alive?(pid), pid) end defp ask_if_alive(false, _pid), do: false defp ask_if_alive(true, pid) do timeout = Application.get_env(:aprsme, :health_check_call_timeout_ms, 5000) GenServer.call(pid, :shutting_down?, timeout) catch _kind, _reason -> false end end