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
syscall
package 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_t
type, which is a struct containing various system information, including uptime. - Output: A
string
that 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_t
is a struct provided by thesyscall
package 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 ofsysInfo
to the function, allowing the function to modify the original variable. err
would 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
float64
variable which holds the calculated CPU use percentage and anerror
when 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_t
is a structure in Go that’s defined in thesyscall
package. It’s used to hold information about a mounted filesystem. The data contained inStatfs_t
provides 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_t
structure:- 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
Bsize
gives you the total free space in the filesystem. - Bavail: The free blocks available to a non-superuser. This may be less than
Bfree
because 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.