Current section
Files
Jump to
Current section
Files
lib/mob_dev/observer.ex
defmodule DalaDev.Observer do
@moduledoc """
Remote node observer inspired by `:observer.start()`.
Collects comprehensive system information from remote nodes and provides
data structures suitable for LiveView rendering. This includes:
- System information (memory, CPU, uptime)
- Process list with detailed statistics
- ETS tables information
- Application controller state
- Loaded modules and memory
- Port information
- Node connectivity status
All data is collected via RPC calls to remote nodes, making it suitable
for monitoring dala Elixir nodes that don't have direct access to
`:observer`.
"""
# alias DalaDev.Network - currently unused
@type process_info :: %{
pid: String.t(),
name: String.t() | nil,
memory: integer(),
reductions: integer(),
message_queue_len: integer(),
current_function: String.t(),
status: atom(),
registered_name: String.t() | nil
}
@type system_info :: %{
memory: map(),
statistics: map(),
system_version: String.t(),
uptime_ms: integer(),
process_count: integer(),
ets_tables_count: integer()
}
@doc """
Get comprehensive system information from a node.
Returns all observable data from the specified node.
"""
@spec observe(node(), keyword()) :: {:ok, map()} | {:error, term()}
def observe(node \\ Node.self(), opts \\ []) do
timeout = Keyword.get(opts, :timeout, 10_000)
try do
data = %{
node: node,
timestamp: DateTime.utc_now(),
system: fetch_system_info(node, timeout),
processes: fetch_processes(node, timeout),
ets_tables: fetch_ets_tables(node, timeout),
applications: fetch_applications(node, timeout),
modules: fetch_modules_info(node, timeout),
ports: fetch_ports(node, timeout),
load: fetch_load(node, timeout)
}
{:ok, data}
rescue
e -> {:error, Exception.message(e)}
catch
:exit, reason -> {:error, "Node #{inspect(node)} unreachable: #{inspect(reason)}"}
end
end
@doc """
Get system-level information (memory, stats, version).
"""
@spec system_info(node(), integer()) :: system_info()
def system_info(node \\ Node.self(), timeout \\ 10_000) do
fetch_system_info(node, timeout)
end
@doc """
Get detailed process list with statistics.
"""
@spec process_list(node(), integer()) :: [process_info()]
def process_list(node \\ Node.self(), timeout \\ 10_000) do
fetch_processes(node, timeout)
end
@doc """
Get ETS tables information.
"""
@spec ets_tables(node(), integer()) :: [map()]
def ets_tables(node \\ Node.self(), timeout \\ 10_000) do
fetch_ets_tables(node, timeout)
end
# ── Private: Data Collection ─────────────────────
defp fetch_system_info(node, _timeout) do
call_remote(node, fn ->
memory = :erlang.memory() |> Enum.into(%{})
stats = :erlang.statistics(:runtime) |> elem(0)
%{
memory: %{
total: memory[:total] || 0,
processes: memory[:processes] || 0,
atom: memory[:atom] || 0,
binary: memory[:binary] || 0,
code: memory[:code] || 0,
ets: memory[:ets] || 0
},
statistics: %{
runtime: stats
},
system_version: to_string(:erlang.system_info(:system_version)),
uptime_ms: :erlang.statistics(:wall_clock) |> elem(0),
process_count: :erlang.system_info(:process_count),
ets_tables_count: :ets.all() |> length(),
wordsize: :erlang.system_info(:wordsize)
}
end)
end
defp fetch_processes(node, _timeout) do
call_remote(node, fn ->
:erlang.processes()
|> Enum.map(fn pid ->
try do
info =
Process.info(pid, [
:memory,
:reductions,
:message_queue_len,
:current_function,
:status,
:registered_name
])
%{
pid: inspect(pid),
name: format_process_name(pid, info[:registered_name]),
memory: info[:memory] || 0,
reductions: info[:reductions] || 0,
message_queue_len: info[:message_queue_len] || 0,
current_function: format_mfa(info[:current_function]),
status: info[:status],
registered_name: format_registered_name(info[:registered_name])
}
rescue
_ -> nil
end
end)
|> Enum.filter(&(&1 != nil))
|> Enum.sort_by(& &1.memory, &>=/2)
end)
end
defp fetch_ets_tables(node, _timeout) do
call_remote(node, fn ->
:ets.all()
|> Enum.map(fn tid ->
try do
info = :ets.info(tid)
%{
id: inspect(tid),
name: to_string(info[:name] || ""),
type: info[:type],
size: info[:size] || 0,
memory: info[:memory] || 0,
owner: inspect(info[:owner] || ""),
heir: inspect(info[:heir] || ""),
protection: info[:protection]
}
rescue
_ -> nil
end
end)
|> Enum.filter(&(&1 != nil))
|> Enum.sort_by(& &1.memory, &>=/2)
end)
end
defp fetch_applications(node, _timeout) do
call_remote(node, fn ->
:application.which_applications()
|> Enum.map(fn {name, desc, version} ->
%{
name: to_string(name),
description: to_string(desc),
version: to_string(version)
}
end)
end)
end
defp fetch_modules_info(node, _timeout) do
call_remote(node, fn ->
modules = :code.all_loaded()
total_memory =
modules
|> Enum.map(fn {_, beam_path} ->
if beam_path != :preloaded, do: file_size(beam_path), else: 0
end)
|> Enum.sum()
%{
count: length(modules),
total_memory: total_memory,
modules:
modules
|> Enum.take(100)
|> Enum.map(fn {mod, path} ->
%{
module: inspect(mod),
path: to_string(path)
}
end)
}
end)
end
defp fetch_ports(node, _timeout) do
call_remote(node, fn ->
:erlang.ports()
|> Enum.map(fn port ->
try do
info = Port.info(port)
%{
id: inspect(port),
name: to_string(info[:name] || ""),
os_pid: info[:os_pid],
connected: inspect(info[:connected] || ""),
input: info[:input] || 0,
output: info[:output] || 0
}
rescue
_ -> nil
end
end)
|> Enum.filter(&(&1 != nil))
end)
end
defp fetch_load(node, _timeout) do
call_remote(node, fn ->
%{
scheduler_usage:
try do
sample = apply(:scheduler, :sample, [])
apply(:scheduler, :usage, [sample])
rescue
_ -> []
end,
io: :erlang.statistics(:io)
}
end)
end
# ── Private: Helpers ─────────────────────────────
defp call_remote(node, fun) do
if node == Node.self() do
fun.()
else
case Node.connect(node) do
true ->
case :rpc.call(node, __MODULE__, :call_remote, [node, fun], 10_000) do
{:badrpc, reason} -> %{error: "RPC failed: #{inspect(reason)}"}
result -> result
end
false ->
%{error: "Cannot connect to node #{inspect(node)}"}
end
end
end
defp format_process_name(_pid, :undefined), do: nil
defp format_process_name(_pid, name), do: to_string(name)
defp format_registered_name(:undefined), do: nil
defp format_registered_name(name), do: to_string(name)
defp format_mfa({mod, fun, arity}) do
"#{inspect(mod)}.#{fun}/#{arity}"
end
defp format_mfa(_), do: "unknown"
defp file_size(path) when is_list(path) do
case File.stat(to_string(path)) do
{:ok, %File.Stat{size: size}} -> size
_ -> 0
end
end
defp file_size(_), do: 0
end