Add cluster-aware APRS-IS status checking
Both /status and /status.json endpoints now show cluster-wide APRS-IS connection status instead of just local node status. Features: - get_cluster_aprs_status() checks all nodes in cluster via RPC - Returns status from any connected node (usually the leader) - Shows cluster info: total nodes, connected nodes, leader node - Graceful fallback to local status in non-clustered mode - Enhanced status page UI with cluster information display This ensures status pages accurately reflect the true APRS-IS connection state across the entire cluster. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
37c9ac79ed
commit
0f26cd7e22
3 changed files with 122 additions and 3 deletions
|
|
@ -22,6 +22,21 @@ defmodule Aprsme.Cluster.LeaderElection do
|
||||||
GenServer.call(__MODULE__, :current_leader)
|
GenServer.call(__MODULE__, :current_leader)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets APRS-IS status from across the entire cluster.
|
||||||
|
Returns the status from whichever node has an active connection.
|
||||||
|
"""
|
||||||
|
def get_cluster_aprs_status do
|
||||||
|
cluster_enabled = Application.get_env(:aprsme, :cluster_enabled, false)
|
||||||
|
|
||||||
|
if cluster_enabled do
|
||||||
|
get_cluster_wide_status()
|
||||||
|
else
|
||||||
|
# Non-clustered mode - just return local status
|
||||||
|
Aprsme.Is.get_status()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(_opts) do
|
def init(_opts) do
|
||||||
Logger.info("Starting leader election process")
|
Logger.info("Starting leader election process")
|
||||||
|
|
@ -179,6 +194,67 @@ defmodule Aprsme.Cluster.LeaderElection do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_cluster_wide_status do
|
||||||
|
all_nodes = [node() | Node.list()]
|
||||||
|
|
||||||
|
# Check each node for APRS-IS connection status
|
||||||
|
connected_statuses =
|
||||||
|
all_nodes
|
||||||
|
|> Enum.map(&get_node_status/1)
|
||||||
|
|> Enum.filter(fn status -> status.connected end)
|
||||||
|
|
||||||
|
case connected_statuses do
|
||||||
|
[status | _] ->
|
||||||
|
# At least one node is connected - return its status
|
||||||
|
# Add cluster info to indicate this is cluster-wide status
|
||||||
|
Map.put(status, :cluster_info, %{
|
||||||
|
total_nodes: length(all_nodes),
|
||||||
|
connected_nodes: length(connected_statuses),
|
||||||
|
leader_node: get_leader_node_name()
|
||||||
|
})
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
# No nodes are connected - return local status but mark as cluster-wide
|
||||||
|
local_status = Aprsme.Is.get_status()
|
||||||
|
|
||||||
|
Map.put(local_status, :cluster_info, %{
|
||||||
|
total_nodes: length(all_nodes),
|
||||||
|
connected_nodes: 0,
|
||||||
|
leader_node: get_leader_node_name()
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_node_status(node_name) do
|
||||||
|
if node_name == node() do
|
||||||
|
# Local node - call directly
|
||||||
|
Aprsme.Is.get_status()
|
||||||
|
else
|
||||||
|
# Remote node - use RPC
|
||||||
|
case :rpc.call(node_name, Aprsme.Is, :get_status, [], 5000) do
|
||||||
|
{:badrpc, _reason} ->
|
||||||
|
# Node unreachable - return disconnected status
|
||||||
|
%{connected: false, server: "unreachable", port: 0}
|
||||||
|
|
||||||
|
status when is_map(status) ->
|
||||||
|
status
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
%{connected: false, server: "error", port: 0}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
_error ->
|
||||||
|
%{connected: false, server: "error", port: 0}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_leader_node_name do
|
||||||
|
case :global.whereis_name(@election_key) do
|
||||||
|
:undefined -> "none"
|
||||||
|
pid when is_pid(pid) -> pid |> node() |> to_string()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp notify_leadership_change(became_leader) do
|
defp notify_leadership_change(became_leader) do
|
||||||
Phoenix.PubSub.broadcast(
|
Phoenix.PubSub.broadcast(
|
||||||
Aprsme.PubSub,
|
Aprsme.PubSub,
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ defmodule AprsmeWeb.PageController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def status_json(conn, _params) do
|
def status_json(conn, _params) do
|
||||||
# Get APRS-IS connection status
|
# Get cluster-wide APRS-IS connection status
|
||||||
aprs_status = Aprsme.Is.get_status()
|
aprs_status = Aprsme.Cluster.LeaderElection.get_cluster_aprs_status()
|
||||||
|
|
||||||
# Get application version
|
# Get application version
|
||||||
version = :aprsme |> Application.spec(:vsn) |> List.to_string()
|
version = :aprsme |> Application.spec(:vsn) |> List.to_string()
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,49 @@ defmodule AprsmeWeb.StatusLive.Index do
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Cluster Information (if available) -->
|
||||||
|
<%= if Map.has_key?(@aprs_status, :cluster_info) do %>
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-semibold mb-4">{gettext("Cluster Status")}</h2>
|
||||||
|
<div class="card bg-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Total Nodes:")}</span>
|
||||||
|
<span class="text-sm font-mono">{@aprs_status.cluster_info.total_nodes}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Connected Nodes:")}</span>
|
||||||
|
<span class="text-sm font-mono">{@aprs_status.cluster_info.connected_nodes}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Leader Node:")}</span>
|
||||||
|
<span class="text-sm font-mono">{@aprs_status.cluster_info.leader_node}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-sm font-medium opacity-70 mr-2">{gettext("Cluster Mode:")}</span>
|
||||||
|
<div class="badge badge-success gap-1">
|
||||||
|
<div class="w-2 h-2 bg-current rounded-full"></div>
|
||||||
|
{gettext("Enabled")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs opacity-70 mt-2">
|
||||||
|
{gettext(
|
||||||
|
"Status is aggregated from all nodes in the cluster. Only the leader node maintains the APRS-IS connection."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -292,7 +335,7 @@ defmodule AprsmeWeb.StatusLive.Index do
|
||||||
# Private functions
|
# Private functions
|
||||||
|
|
||||||
defp get_aprs_status do
|
defp get_aprs_status do
|
||||||
Aprsme.Is.get_status()
|
Aprsme.Cluster.LeaderElection.get_cluster_aprs_status()
|
||||||
rescue
|
rescue
|
||||||
error ->
|
error ->
|
||||||
require Logger
|
require Logger
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue