GO
Monitoring System Stats with Go: A Simple Web Application pt.1

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, a syscall.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 the syscall 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 of sysInfo 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 an error 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 the syscall package. It’s used to hold information about a mounted filesystem. The data contained in Statfs_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.