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"""
🖥️ System Monitor - <%= @current_view |> to_string() |> String.capitalize() %>
Refresh: <%= @refresh_rate %>s | Last Update: <%= format_time(@last_update) %> | Graphs: <%= if @show_graphs, do: "ON", else: "OFF" %>
""" 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 %>
<%= disk.mount %> <%= disk.percentage %>%
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 %>
<%= interface.name %> Active
↓ 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 %>
PID Name Memory Reductions Status
<%= 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