Current section
Files
Jump to
Current section
Files
lib/dashboard.ex
defmodule ExESDBDashboard do
@moduledoc """
Composable cluster dashboard components that can be embedded
in any Phoenix application using ex_esdb_gater as a dependency.
## Usage
Add to your Phoenix router:
import ExESDBDashboard
scope "/admin" do
dashboard_routes()
end
Or mount individual components:
live "/cluster", ExESDBDashboard.ClusterLive
## Requirements
Your hosting application must have:
- `phoenix_live_view` in dependencies
- `ExESDBGater.System` started in your supervision tree
- Phoenix.PubSub configured (using the same pubsub server as ExESDBGater)
"""
@doc """
Returns the main cluster dashboard LiveView component.
"""
def cluster_live_component, do: ExESDBDashboard.ClusterLive
@doc """
Returns the cluster status component for embedding in other views.
"""
def cluster_status_component, do: ExESDBDashboard.ClusterStatus
@doc """
Adds dashboard routes to a Phoenix router.
## Example
scope "/admin" do
dashboard_routes()
end
"""
defmacro dashboard_routes do
quote do
import Phoenix.LiveView.Router
live "/", ExESDBDashboard.ClusterLive, :home
live "/cluster", ExESDBDashboard.ClusterLive, :cluster
end
end
@doc """
Gets comprehensive cluster data for dashboard display.
Returns a map containing:
- `:nodes` - List of connected nodes with their status
- `:stores` - List of available stores with statistics
- `:total_streams` - Total number of streams across all stores
- `:cluster_health` - Overall cluster health status
"""
def get_cluster_data do
%{
nodes: get_cluster_nodes(),
stores: get_cluster_stores(),
total_streams: get_total_streams(),
cluster_health: get_cluster_health(),
updated_at: DateTime.utc_now()
}
end
@doc """
Gets information about all cluster nodes.
"""
def get_cluster_nodes do
connected_nodes = Node.list()
nodes_info =
connected_nodes
|> Enum.map(fn node ->
%{
name: node,
status: :connected,
is_ex_esdb: is_ex_esdb_node?(node),
uptime: get_node_uptime(node),
last_seen: DateTime.utc_now()
}
end)
# Add self node
[%{
name: Node.self(),
status: :self,
is_ex_esdb: false, # This is the gater node
uptime: get_self_uptime(),
last_seen: DateTime.utc_now()
} | nodes_info]
end
@doc """
Gets information about cluster stores.
"""
def get_cluster_stores do
case ExESDBGater.API.list_stores() do
{:ok, stores} when is_list(stores) ->
stores
|> Enum.map(fn store_info ->
store_id = Map.get(store_info, :store_id) || Map.get(store_info, "store_id")
%{
id: store_id,
name: to_string(store_id),
stream_count: get_store_stream_count(store_id),
subscription_count: get_store_subscription_count(store_id),
status: :healthy,
nodes: get_store_nodes(store_id)
}
end)
{:error, reason} ->
require Logger
Logger.warning("Failed to get stores: #{inspect(reason)}")
[]
_ ->
[]
end
end
# Private helper functions
defp is_ex_esdb_node?(node) do
case :rpc.call(node, :application, :which_applications, [], 2_000) do
apps when is_list(apps) ->
Enum.any?(apps, fn {app, _, _} -> app == :ex_esdb end)
_ ->
false
end
end
defp get_node_uptime(node) do
case :rpc.call(node, :erlang, :statistics, [:wall_clock], 2_000) do
{uptime_ms, _} when is_integer(uptime_ms) ->
uptime_ms
_ ->
0
end
end
defp get_self_uptime do
{uptime_ms, _} = :erlang.statistics(:wall_clock)
uptime_ms
end
defp get_store_stream_count(store_id) do
case ExESDBGater.API.get_streams(store_id) do
{:ok, streams} when is_list(streams) -> length(streams)
_ -> 0
end
end
defp get_store_subscription_count(store_id) do
case ExESDBGater.API.get_subscriptions(store_id) do
{:ok, subscriptions} when is_list(subscriptions) -> length(subscriptions)
_ -> 0
end
end
defp get_store_nodes(store_id) do
ExESDBGater.API.gateway_worker_pids_for_store(store_id)
|> Enum.map(fn pid ->
case Process.info(pid, :registered_name) do
{:registered_name, name} -> extract_node_from_name(name)
_ -> node(pid)
end
end)
|> Enum.uniq()
end
defp extract_node_from_name({:gateway_worker, _store, node, _port}), do: node
defp extract_node_from_name(_), do: :unknown
defp get_total_streams do
get_cluster_stores()
|> Enum.reduce(0, fn store, acc -> acc + store.stream_count end)
end
defp get_cluster_health do
nodes = get_cluster_nodes()
stores = get_cluster_stores()
ex_esdb_nodes = Enum.count(nodes, & &1.is_ex_esdb)
healthy_stores = Enum.count(stores, & &1.status == :healthy)
cond do
ex_esdb_nodes == 0 -> :no_cluster
healthy_stores == 0 -> :no_stores
ex_esdb_nodes >= 3 and healthy_stores > 0 -> :healthy
ex_esdb_nodes >= 1 and healthy_stores > 0 -> :degraded
true -> :unhealthy
end
end
end