defmodule SystemMonitor do
@moduledoc """
Real-time system monitoring dashboard built with Raxol.
Provides comprehensive system metrics including CPU usage, memory consumption,
disk I/O, network traffic, and process monitoring in an interactive terminal UI.
## Features
* Real-time CPU, memory, disk, and network monitoring
* Process list with sorting and filtering
* Historical graphs and trending
* Interactive controls and navigation
* Customizable refresh rates and display options
* System information and diagnostics
## Usage
# Start with default settings
SystemMonitor.start()
# Start with custom refresh rate (in seconds)
SystemMonitor.start(refresh_rate: 2)
# Start in specific view mode
SystemMonitor.start(view: :processes)
## Key Bindings
* `q` - Quit application
* `r` - Refresh data
* `p` - Show processes view
* `s` - Show system overview
* `c` - Show CPU details
* `m` - Show memory details
* `d` - Show disk details
* `n` - Show network details
* `g` - Toggle graphs
* `h` - Show help
* `+/-` - Adjust refresh rate
"""
use Raxol.UI, framework: :universal
require Logger
# State structure
defstruct [
:current_view,
:current_metrics,
:refresh_rate,
:show_graphs,
:process_sort_by,
:process_filter,
:cpu_history,
:memory_history,
:network_history,
:last_update,
:help_visible
]
# Default configuration
@default_config %{
refresh_rate: 1,
show_graphs: true,
max_history: 60,
process_limit: 20
}
# Public API
@doc """
Start the system monitor.
"""
def start(opts \\ []) do
config = Map.merge(@default_config, Map.new(opts))
initial_state = %__MODULE__{
current_view: :overview,
current_metrics: collect_initial_metrics(),
refresh_rate: config.refresh_rate,
show_graphs: config.show_graphs,
process_sort_by: :memory,
process_filter: "",
cpu_history: :queue.new(),
memory_history: :queue.new(),
network_history: :queue.new(),
last_update: System.system_time(:second),
help_visible: false
}
case Raxol.UI.start_link(__MODULE__, initial_state) do
{:ok, pid} ->
Logger.info("System Monitor started")
start_update_timer(pid, config.refresh_rate)
run_monitor_loop(pid)
{:error, reason} ->
Logger.error("Failed to start monitor: #{inspect(reason)}")
{:error, reason}
end
end
# UI Implementation
@impl Raxol.UI
def render(assigns) do
~H"""
<%= render_header(assigns) %>
<%= render_main_content(assigns) %>
<%= render_footer(assigns) %>
<%= if @help_visible, do: render_help_overlay(assigns) %>
"""
end
defp render_header(assigns) do
~H"""
"""
end
defp render_main_content(assigns) do
case assigns.current_view do
:overview -> render_overview(assigns)
:cpu -> render_cpu_details(assigns)
:memory -> render_memory_details(assigns)
:disk -> render_disk_details(assigns)
:network -> render_network_details(assigns)
:processes -> render_processes(assigns)
:system -> render_system_info(assigns)
_ -> render_overview(assigns)
end
end
defp render_footer(assigns) do
~H"""
"""
end
defp render_overview(assigns) do
~H"""
CPU Usage
<%= @current_metrics.cpu.overall %>%
Cores: <%= @current_metrics.cpu.count %>
Load Avg: <%= Float.round(@current_metrics.system.load_average, 2) %>
Memory Usage
<%= @current_metrics.memory.percentage %>%
Used: <%= format_bytes(@current_metrics.memory.processes) %>
Total: <%= format_bytes(@current_metrics.memory.total) %>
Disk Usage
<%= for disk <- Enum.take(@current_metrics.disk.disks, 3) do %>
<%= disk.mount %>
<%= disk.percentage %>%
<% end %>
Network
<%= case List.first(@current_metrics.network.interfaces) do %>
<% nil -> %>
No interfaces
<% interface -> %>
↓ RX:
<%= format_bytes(interface.rx_speed) %>/s
↑ TX:
<%= format_bytes(interface.tx_speed) %>/s
<% end %>
Top Processes
<%= for {proc, index} <- Enum.with_index(Enum.take(@current_metrics.processes, 5)) do %>
<%= String.slice(proc.name, 0..15) %>
<%= format_bytes(proc.memory) %>
<% end %>
System Info
OS: <%= @current_metrics.system.os %>
Uptime: <%= format_duration(@current_metrics.system.uptime) %>
Processes: <%= length(@current_metrics.processes) %>
"""
end
defp render_cpu_details(assigns) do
~H"""
CPU Details
Overall: <%= @current_metrics.cpu.overall %>%
Cores: <%= @current_metrics.cpu.count %>
Load Average: <%= Float.round(@current_metrics.system.load_average, 2) %>
<%= if @show_graphs do %>
CPU History
[CPU History Chart - 60 data points]
<% end %>
Per-Core Usage:
<%= for {usage, core} <- @current_metrics.cpu.cores do %>
Core <%= core %>
<%= usage %>%
<% end %>
"""
end
defp render_memory_details(assigns) do
~H"""
Memory Details
Processes
<%= format_bytes(@current_metrics.memory.processes) %>
System
<%= format_bytes(@current_metrics.memory.system) %>
Atom
<%= format_bytes(@current_metrics.memory.atom) %>
<%= if @show_graphs do %>
Memory History (%)
[Memory History Chart - 60 data points]
<% end %>
"""
end
defp render_disk_details(assigns) do
~H"""
Disk I/O
Read Speed: <%= format_bytes(@current_metrics.disk.read_speed) %>/s
Write Speed: <%= format_bytes(@current_metrics.disk.write_speed) %>/s
Mounted Filesystems:
<%= for disk <- @current_metrics.disk.disks do %>
Used: <%= format_bytes(disk.used) %>
Total: <%= format_bytes(disk.total) %>
<% end %>
"""
end
defp render_network_details(assigns) do
~H"""
Network Interfaces
<%= if @show_graphs do %>
Network Traffic (bytes/s)
[Network Traffic Chart - 60 data points]
<% end %>
<%= for interface <- @current_metrics.network.interfaces do %>
↓ RX:
<%= format_bytes(interface.rx_speed) %>/s
↑ TX:
<%= format_bytes(interface.tx_speed) %>/s
Total:
<%= format_bytes(interface.rx_bytes + interface.tx_bytes) %>
<% end %>
"""
end
defp render_processes(assigns) do
filtered_processes =
@current_metrics.processes
|> Enum.sort_by(fn proc -> Map.get(proc, @process_sort_by, 0) end, :desc)
|> Enum.filter(fn proc ->
@process_filter == "" or
String.contains?(
String.downcase(proc.name),
String.downcase(@process_filter)
)
end)
|> Enum.take(20)
~H"""
Process List
Sort by: <%= @process_sort_by %>
Filter: <%= if @process_filter == "", do: "none", else: @process_filter %>
<%= for proc <- filtered_processes do %>
<%= proc.pid %>
<%= String.slice(proc.name, 0..20) %>
<%= format_bytes(proc.memory) %>
<%= format_number(proc.reductions) %>
<%= proc.status %>
<% end %>
"""
end
defp render_system_info(assigns) do
~H"""
System Information
Operating System
OS:
<%= @current_metrics.system.os %>
Uptime:
<%= format_duration(@current_metrics.system.uptime) %>
Load Avg:
<%= Float.round(@current_metrics.system.load_average, 2) %>
Runtime
Erlang:
<%= @current_metrics.system.erlang_version %>
Elixir:
<%= @current_metrics.system.elixir_version %>
Processors:
<%= @current_metrics.system.processors %>
Schedulers:
<%= @current_metrics.system.schedulers %>
"""
end
defp render_help_overlay(assigns) do
~H"""
System Monitor Help
Navigation
s - System overview
c - CPU details
m - Memory details
d - Disk details
n - Network details
p - Process list
Controls
r - Refresh data
g - Toggle graphs
+/- - Adjust refresh rate
q - Quit
h - Toggle help
"""
end
# Helper Functions
defp format_bytes(bytes) when is_integer(bytes) do
cond do
bytes < 1024 -> "#{bytes}B"
bytes < 1_048_576 -> "#{div(bytes, 1024)}KB"
bytes < 1_073_741_824 -> "#{div(bytes, 1_048_576)}MB"
true -> "#{Float.round(bytes / 1_073_741_824, 1)}GB"
end
end
defp format_bytes(_), do: "0B"
defp format_number(num) when is_integer(num) do
num |> Integer.to_string() |> add_commas()
end
defp format_number(_), do: "0"
defp add_commas(str) do
str
|> String.reverse()
|> String.replace(~r/(\d{3})/, "\\1,")
|> String.reverse()
|> String.trim_leading(",")
end
defp format_duration(seconds) when is_integer(seconds) do
days = div(seconds, 86400)
hours = div(rem(seconds, 86400), 3600)
mins = div(rem(seconds, 3600), 60)
cond do
days > 0 -> "#{days}d #{hours}h"
hours > 0 -> "#{hours}h #{mins}m"
true -> "#{mins}m"
end
end
defp format_duration(_), do: "0m"
defp format_time(timestamp) do
timestamp
|> DateTime.from_unix!()
|> Calendar.strftime("%H:%M:%S")
end
# Mock data functions
defp collect_initial_metrics do
%{
cpu: %{
overall: 45,
count: 8,
cores: Enum.map(0..7, fn i -> {15 + :rand.uniform(70), i} end)
},
memory: %{
total: 16_000_000_000,
processes: 8_000_000_000,
system: 2_000_000_000,
atom: 50_000_000,
binary: 100_000_000,
ets: 25_000_000,
percentage: 62
},
disk: %{
read_speed: 1_500_000,
write_speed: 800_000,
disks: [
%{
mount: "/",
used: 120_000_000_000,
total: 250_000_000_000,
percentage: 48
},
%{
mount: "/home",
used: 80_000_000_000,
total: 500_000_000_000,
percentage: 16
}
]
},
network: %{
interfaces: [
%{
name: "eth0",
rx_speed: 2_500_000,
tx_speed: 1_200_000,
rx_bytes: 1_500_000_000,
tx_bytes: 800_000_000
}
]
},
processes: generate_mock_processes(),
system: %{
os: "Linux 5.15.0",
erlang_version: "26.0",
elixir_version: "1.15.0",
processors: 8,
schedulers: 8,
uptime: 1_234_567,
load_average: 1.45
}
}
end
defp generate_mock_processes do
process_names = [
"beam.smp",
"systemd",
"kthreadd",
"rcu_gp",
"nginx",
"postgres",
"redis-server",
"chrome",
"firefox",
"code",
"iex",
"mix"
]
Enum.map(1..50, fn i ->
name = Enum.random(process_names)
%{
pid: 1000 + i,
name: "#{name}-#{i}",
memory: :rand.uniform(100_000_000),
reductions: :rand.uniform(1_000_000),
status: Enum.random([:running, :waiting, :suspended]),
function: "gen_server:loop/#{:rand.uniform(3)}"
}
end)
end
# Process loop functions
defp start_update_timer(_pid, refresh_rate) do
# In a real implementation, this would start a timer
:ok
end
defp run_monitor_loop(pid) do
receive do
{:stop} ->
Raxol.UI.stop(pid)
{:refresh} ->
Raxol.UI.refresh(pid)
run_monitor_loop(pid)
other ->
Logger.debug("Monitor loop received: #{inspect(other)}")
run_monitor_loop(pid)
end
end
end