aprs.me/lib/aprsme_web/plugs/health_check.ex
Graham McIntire ee02769d38
refactor: pattern-match in HealthCheck, EncodingUtils, and StatusLive helpers
- HealthCheck.check_health(:readiness): collapse the 3-branch cond into
  readiness_status/2 dispatched on (health_status, shutting_down?).
- HealthCheck.shutting_down?: the whereis + alive? + try/catch nest
  becomes a small pipeline of function heads (ask_shutting_down?/1 +
  ask_if_alive/2), with the catch isolated to the single place it fires.
- EncodingUtils.sanitize_string_fields and sanitize_nested_map shared a
  three-branch atom-or-string key lookup. Extract update_existing_key/3
  + first_present_key/3 + nil_aware/1 helpers; the original functions
  become two-liners.
- StatusLive.Index.format_uptime: splits the cond into
  format_uptime_parts/4 clauses with numeric guards.
- StatusLive.Index.calculate_health_score: struct-shape pattern match
  on %{connected: false} / %{uptime_seconds: s} guards replaces the cond.
- StatusLive.Index.format_time_ago: pipe the diff through
  format_seconds_ago/1 with four clauses.

Adds tests for the four public StatusLive.Index helpers
(format_uptime, format_time_ago, get_health_description, format_number).
Coverage 67.29 → 67.43%.
2026-04-23 14:02:50 -05:00

112 lines
3 KiB
Elixir

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
require Logger
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
GenServer.call(pid, :shutting_down?, 5000)
catch
_kind, _reason -> false
end
end