October 28, 2024
Building a simple Remote System Monitor
Building a simple Remote System Monitor
ReSysTor

As a developer, you'd often find yourself opening the system monitor to see how the system is managing processes. In my case, I had a separate physical device running heavy processes (my mini server because I can't afford AWS -_-) and needed to constantly check on it. Being experimental and curious, I tried building a simple system monitor that could be remotely accessed without having to set up industry-standard monitoring tools.
Features
By simple I mean dead simple, it needs to be able to:
- Read the resources I check on i.e CPU and RAM usage
- Be accessed remotely
Dead simple. No kidding.
The stack
- Nextjs with Typescript for the fancy web UI
- Go (Go lang) for the CLI
- LocalTunnel to expose it on the internet
The plan

The frontend
- Fetch the config
useEffect(() => {
(async () => {
const res = await fetch(`http://localhost:8080/config`, {
cache: "no-cache",
});
const config = (await res.json()) as ServerConfig;
setConfig(config);
})();
}, []);
The config contains the server's name and the interval, how long until it refreshes.
- Now that we have the config, we need to fetch the initial data and keep re-fetching after each interval we got from the config
const getData = async () => {
const res = await fetch(`http://localhost:8080`);
const data = await res.json();
setMontitorData(data);
};
useEffect(() => {
getData();
const ms = parseInt(interval) * 1000;
const intervalFn = setInterval(() => getData(), ms);
return () => clearInterval(intervalFn);
}, []);
The data returns the following
interface MonitorData {
filesystems: Filesystem[];
processes: Process[];
resources: Resources;
}
interface Filesystem {
path: string;
disk_type: string;
device: string;
total: number;
free: number;
used: number;
used_percentage: number;
}
interface Process {
name: string;
username: string;
pid: number;
memory_usage: number;
cpu_usage: number;
}
interface Resources {
local_ip: string;
uptime: Uptime;
battery_stats: BatteryStats;
memory_stats: MemoryStats;
cpu_stats: CpuStats;
user_meta: UserMeta;
}
interface Uptime {
days: number;
hours: number;
minutes: number;
}
interface BatteryStats {
charging_state: string;
current_power: number;
}
interface MemoryStats {
total: number;
used: number;
free: number;
used_percentage: number;
}
interface CpuStats {
model: string;
cores: number;
usages: number[];
}
interface UserMeta {
name: string;
}
The CLI
We need to ship the UI together with final binary, luckily, Go makes it simple (as it does with all others)
//go:embed all:frontend/out
var assets embed.FS
Now when the program runs, we have access to files we can extract to host
if err := fs.WalkDir(assets, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
outPath := filepath.Join(tempDir, path)
if d.IsDir() {
return os.MkdirAll(outPath, 0755)
}
data, err := assets.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(outPath, data, 0644)
}); err != nil {
log.Fatal(err)
}
Let’s serve it up locally using, well, serve at the location of the web files.
cmd := exec.Command("npx", "--yes", "serve", "-s", fmt.Sprintf("%v/frontend/out", currentDir))
We listen to status updates from the serve program at an interval of 5 seconds.
If its ready, fire up Localtunnel and listen on the running local port, 3000
for range time.Tick(time.Second * 5) {
if strings.Contains(stdBuffer.String(), "Accepting") {
log.Println("Getting tunnel url")
cmd := exec.Command("npx", "--yes", "localtunnel", "--port", "3000")
go cmd.Run()
break
}
}
Now we can get our data using Gopsutil and battery. We import them.
import (
"github.com/distatus/battery"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/mem"
"github.com/shirou/gopsutil/process"
)
Getting the system resources
func (m *Monitor) GetSystemResources() (data.SystemResources, error) {
stats := data.SystemResources{}
memoryStats, err := mem.VirtualMemory()
if err != nil {
return stats, err
}
cpuInfo, err := cpu.Info()
if err != nil {
return stats, err
}
cpuUsages, err := cpu.Percent(0, true)
if err != nil {
return stats, err
}
upTime, err := utils.GetUptime()
if err != nil {
upTime = data.UpTime{}
} else {
stats.UpTime = upTime
}
stats.MemoryStats.Total = memoryStats.Total
stats.MemoryStats.Free = memoryStats.Free
stats.MemoryStats.Used = memoryStats.Used
stats.MemoryStats.UsedPercentage = memoryStats.UsedPercent
stats.CPUStats = data.CPUStats{
Model: cpuInfo[0].ModelName,
Cores: len(cpuInfo),
Usages: cpuUsages,
}
batteries, err := battery.GetAll()
if err != nil {
stats.BatteryStats.CurrentPower = 0
stats.BatteryStats.ChargingState = "Unknown"
} else if len(batteries) > 0 {
batteryStats := batteries[0]
stats.BatteryStats.CurrentPower = int(math.Round(batteryStats.Current / batteryStats.Full * 100))
stats.BatteryStats.ChargingState = batteryStats.State.String()
} else {
stats.BatteryStats.CurrentPower = 100
stats.BatteryStats.ChargingState = "Full"
}
ip, err := utils.GetLocalIp()
if err != nil {
return stats, err
}
stats.LocalIP = ip
return stats, nil
}
Getting system processes
func (m *Monitor) GetSystemProcesses() ([]data.Process, error) {
stats := []data.Process{}
allProcesses, err := process.Processes()
if err != nil {
return stats, err
}
for _, currentProcess := range allProcesses {
name, _ := currentProcess.Name()
cpuUsage, _ := currentProcess.CPUPercent()
memory_usage, _ := currentProcess.MemoryPercent()
username, _ := currentProcess.Username()
pid := currentProcess.Pid
process := data.Process{
Name: name,
CPUUsage: cpuUsage,
MemoryUsage: float64(memory_usage),
Pid: pid,
Username: username,
}
stats = append(stats, process)
}
return stats, nil
}func (m *Monitor) GetSystemProcesses() ([]data.Process, error) {
stats := []data.Process{}
allProcesses, err := process.Processes()
if err != nil {
return stats, err
}
for _, currentProcess := range allProcesses {
name, _ := currentProcess.Name()
cpuUsage, _ := currentProcess.CPUPercent()
memory_usage, _ := currentProcess.MemoryPercent()
username, _ := currentProcess.Username()
pid := currentProcess.Pid
process := data.Process{
Name: name,
CPUUsage: cpuUsage,
MemoryUsage: float64(memory_usage),
Pid: pid,
Username: username,
}
stats = append(stats, process)
}
return stats, nil
}
Getting filesystems
func (m *Monitor) GetFileSystems() ([]data.DiskStats, error) {
stats := []data.DiskStats{}
diskPartitions, err := disk.Partitions(false)
if err != nil {
return []data.DiskStats{}, err
}
for _, diskPartition := range diskPartitions {
if !strings.Contains(diskPartition.Device, "loop") {
diskStats, _ := disk.Usage(diskPartition.Mountpoint)
stats = append(stats, data.DiskStats{
Path: "/",
DiskType: diskStats.Fstype,
Device: diskPartition.Device,
Total: diskStats.Total,
Free: diskStats.Free,
Used: diskStats.Used,
UsedPercentage: diskStats.UsedPercent,
})
}
}
return stats, nil
}
Putting all together using the Go HTTP router
func (s *Server) getStats(w http.ResponseWriter, r *http.Request) {
resources, err := s.Monitor.GetSystemResources()
if err != nil {
http.Error(w, "Failed to System stats", http.StatusInternalServerError)
return
}
processes, err := s.Monitor.GetSystemProcesses()
if err != nil {
http.Error(w, "Failed to System stats", http.StatusInternalServerError)
return
}
fileSystems, err := s.Monitor.GetFileSystems()
if err != nil {
http.Error(w, "Failed to System stats", http.StatusInternalServerError)
return
}
json, err := json.Marshal(map[string]interface{}{
"resources": resources,
"processes": processes,
"filesystems": fileSystems,
})
if err != nil {
http.Error(w, "Failed to System stats", http.StatusInternalServerError)
return
}
w.Write(json)
}
That’s all folks! A very simple Remote System Monitor (ReSysTor).
You may also like
Other posts
A couple of related reads, picked from the rest of the blog.
June 5, 2026
Trying to Find All Ghanaians on WhatsApp, Politely.
One boolean. Twelve prefixes. The whole country.
May 12, 2026
Owbird's Work Formula
I kept substituting physics equations until I accidentally reinvented suffering. Here's the science.