Introduction
Managing and observing system’s vital stats is crucial for optimizing performance and understanding usage patterns. In this post, I’ll be building a simple web application using Go that will display key system statistics such as CPU usage, RAM, disk space, and uptime.
Set Up Go Project
To begin with, let’s set up a new directory and initialize a Go module for our project. After creating a new directory, initialize a new module and create a main.go file:
mkdir go-proj && cd go-proj
go mod init go-proj
touch main.go
Base Go Code Structure
Now add the package name and some imports that we will need:
package main
import (
"fmt"
"html/template"
"net/http"
"os"
)
We define a structure named PageVariables to hold the formatted strings of system statistics to display on the web-page.
type PageVariables struct {
Uptime string
TotalRAM string
FreeRAM string
TotalDisk string
FreeDisk string
CPUUsage string
}
Interacting with System Stats using the syscall Package
Overview
- What: The
syscallpackage in Go provides a straightforward interface to the operating system’s low-level system call API. - Purpose: It’s used to execute system calls, which are requests made from user space (the Go program) to kernel space (the underlying operating system) to perform various low-level operations or retrieve information directly from the OS.
Gathering System Uptime
- Purpose: Get system uptime and convert it from seconds to a more readable format.
- Input:
sysInfo, asyscall.Sysinfo_ttype, which is a struct containing various system information, including uptime. - Output: A
stringthat provides the system’s uptime in a human-readable format (days, hours, minutes, and seconds).
func getUptime(sysInfo syscall.Sysinfo_t) string {
// Retrieve uptime in seconds
seconds := sysInfo.Uptime
days := seconds / (60 * 60 * 24)
hours := (seconds % (60 * 60 * 24)) / (60 * 60)
minutes := (seconds % (60 * 60)) / 60
remainingSeconds := seconds % 60
// `%d` is a placeholder for decimal numbers
uptimeString := fmt.Sprintf("%d days, %d hours, %d minutes, %d seconds", days, hours, minutes, remainingSeconds)
return uptimeString
}
Understanding syscall.Sysinfo_t
- What:
Sysinfo_tis a struct provided by thesyscallpackage that’s used to hold system information. - Fields: Some notable fields in this struct include:
Uptime: How long the system has been running, in seconds.Loads: 1, 5, and 15-minute load averages, which give a rough idea of system usage.Totalram: Total usable RAM (i.e., physical RAM minus a few reserved bits and the kernel binary code).Freeram: Amount of free RAM.Procs: Number of current processes.- … and many more, which provide various statistics and states of the system.
Usage
var sysInfo syscall.Sysinfo_t
err := syscall.Sysinfo(&sysInfo)
- The
&operator is used to pass the memory address ofsysInfoto the function, allowing the function to modify the original variable. errwould hold any error that occurred during the function call (e.g., if for some reason the system information could not be retrieved).
Getting CPU Usage
The current CPU status can be retrieved from the /proc/stat file:
data, err := os.ReadFile("/proc/stat")
More information: https://man7.org/linux/man-pages/man5/proc.5.html
- Purpose: Retrieve the current CPU status from system kernel.
- Input: Empty.
- Output: A
float64variable which holds the calculated CPU use percentage and anerrorwhen applicable.
func getCPUUsage() (float64, error) {
// Read the contents of /proc/stat.
data, err := os.ReadFile("/proc/stat")
// If an error occurs during reading the file, return 0 and the error.
if err != nil {
return 0, err
}
// Split the contents of the data into lines.
lines := strings.Split(string(data), "\n")
// Iterate through each line of data.
for _, line := range lines {
// Split each line into fields based on white space.
fields := strings.Fields(line)
// Check if the current line contains CPU information.
if fields[0] == "cpu" {
// Initialize a variable to keep track of total CPU time.
total := 0
// Iterate through each field (ignoring the first one) and convert them to integers,
// adding them to the `total`.
for _, v := range fields[1:] {
// Convert string to integer.
value, err := strconv.Atoi(v)
// If an error occurs during conversion, return 0 and the error.
if err != nil {
return 0, err
}
// Add the converted integer to total.
total += value
}
// Convert the 5th field, which is the idle time, to an integer.
idle, err := strconv.Atoi(fields[4])
// If an error occurs during conversion, return 0 and the error.
if err != nil {
return 0, err
}
// Calculate the CPU usage percentage and return it.
// The formula is: 100 * (1 - (idle time / total time))
return 100 * (1.0 - float64(idle)/float64(total)), nil
}
}
// If no line with "cpu" is found, return 0 and an error indicating so.
return 0, fmt.Errorf("cpu info not found")
}
Getting RAM usage
func getRAM(sysInfo syscall.Sysinfo_t) (uint64, uint64) {
totalRAM := sysInfo.Totalram / 1024 / 1024
freeRAM := sysInfo.Freeram / 1024 / 1024
return totalRAM, freeRAM
}
Getting disk space
func getDiskSpace(stat syscall.Statfs_t) (uint64, uint64) {
totalDisk := (stat.Blocks * uint64(stat.Bsize)) / 1024 / 1024
freeDisk := (stat.Bfree * uint64(stat.Bsize)) / 1024 / 1024
return totalDisk, freeDisk
}
Understanding syscall.Statfs_t
- What:
syscall.Statfs_tis a structure in Go that’s defined in thesyscallpackage. It’s used to hold information about a mounted filesystem. The data contained inStatfs_tprovides various pieces of information about the filesystem, such as its type, its block size, and space usage (in terms of blocks). - Fields: Here’s a breakdown of some of the fields in the
Statfs_tstructure:- Type: The type of filesystem (e.g., ext4, NTFS, etc.)
- Bsize: The preferred length of I/O requests for files on the filesystem. Typically, this indicates the block size.
- Blocks: The total data blocks in the filesystem. By multiplying this with
Bsize, you can determine the total size of the filesystem. - Bfree: The total free blocks in the filesystem. Multiplying this with
Bsizegives you the total free space in the filesystem. - Bavail: The free blocks available to a non-superuser. This may be less than
Bfreebecause some filesystems reserve a certain percentage of space that can only be used by the superuser. - Files: The total file nodes in the filesystem. A file node is a data structure on a filesystem on Linux or UNIX-like operating systems that stores all the information about a file excluding its name or its actual data.
- Ffree: The total free file nodes in the filesystem.
- Fsid: Filesystem ID (an identifying number).
- Namelen: The maximum length of a filename on this filesystem.
- Frsize: The fragment size, which may be smaller than the block size.
Building the Web Server with net/http
Creating the main function
func main() {
http.HandleFunc("/", handler)
fmt.Println("Starting Webserver at http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
Using the main handler
A function provided by the net/http package in Go. The http package provides functionalities to implement HTTP clients and servers in Go applications.
The HandleFunc function has the following signature:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
It takes two parameters:
pattern: A string that contains the URL pattern that you want your handler function to respond to.handler: A function that gets called when the URL pattern is matched.
func handler(w http.ResponseWriter, r *http.Request) {
var sysInfo syscall.Sysinfo_t
err := syscall.Sysinfo(&sysInfo)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var stat syscall.Statfs_t
err = syscall.Statfs("/", &stat)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
cpuUsage, err := getCPUUsage()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
uptime := getUptime(sysInfo)
totalRAM, freeRAM := getRAM(sysInfo)
totalDisk, freeDisk := getDiskSpace(stat)
pageVariables := PageVariables{
Uptime: fmt.Sprintf("%v", uptime),
TotalRAM: fmt.Sprintf("%v", totalRAM),
FreeRAM: fmt.Sprintf("%v", freeRAM),
TotalDisk: fmt.Sprintf("%v", totalDisk),
FreeDisk: fmt.Sprintf("%v", freeDisk),
CPUUsage: fmt.Sprintf("%.2f", cpuUsage),
}
tmpl, err := template.ParseFiles("index.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.Execute(w, pageVariables)
}
HTML Template for Displaying Data
Create a simplistic HTML template named index.html to elegantly display the fetched statistics.
<!DOCTYPE html>
<html>
<head>
<title>System Stats</title>
</head>
<body>
<h1>System Statistics</h1>
<p>Uptime: {{.Uptime}}</p>
<p>Total RAM: {{.TotalRAM}} MB</p>
<p>Free RAM: {{.FreeRAM}} MB</p>
<p>Total Disk Space: {{.TotalDisk}} MB</p>
<p>Free Disk Space: {{.FreeDisk}} MB</p>
<p>CPU Usage: {{.CPUUsage}}%</p>
</body>
</html>
Running the Project
Ensure Go is installed and your project setup is complete. Navigate to your project directory and execute:
go run main.go
The web server should be running, and we can navigate to http://localhost:8080 to visualize the system statistics.