• Labs icon Lab
  • Core Tech
Labs

Guided: Build a System Monitor in Go

In this Guided Code Lab, you will build a real-time system monitor using the Go language. Your goal is to create a robust system monitor application that displays crucial system information like CPU usage, memory usage, and currently running processes. Throughout the lab, you will gain practical experience in utilizing Go Routines, Channels, and building a Text-based User Interface (TUI).

Labs

Path Info

Level
Clock icon Intermediate
Duration
Clock icon 1h 4m
Published
Clock icon Jan 18, 2024

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome!

    In this Code Lab lab, you will dive into the world of Go programming and create a real-time system monitor. You will be working with the main.go, systeminfo.go and tui.go files and writing the code required to build the real-time system monitor.

    Completing this lab will involve utilizing the following Go concepts:

    • Reading System Information: Learn how to access and read system information.
    • Go Routines: Understand how to use Go routines to read and display system information concurrently.
    • Channels: Learn about channels and how they can be used for communication between Go routines.
    • Terminal User Interface (TUI): Learn how to build a text-based user interface in the Terminal. Before you begin, let's review some key points:
    • Your task involves implementing code in the main.go , systeminfo.go and tui.go files.
    • To execute the program, type go build . | go run . in the Terminal and press Enter.
    • Use CTRL + C to stop the program execution.
    • If you encounter any challenges along the way, feel free to consult the solution directory. It contains solutions for each step. For example, the solution for Step 2 can be found in solution/Step2_XXX.go.
    • For your convenience, imports required at each step are commented at the top of file. You'll need to uncomment those based on the step you are currently working on.
    • For simplicity based upon the Step you are on there are comments for you to locate the changes required at that step.
    • The go.mod file in the project's root is the module definition file. It contains information about the required dependencies and their versions.
  2. Challenge

    Read System Information

    Read System Information Using gopsutil

    Let's get started!
    in this step, you'll learn about the gopsutil library and how to use the library to fetch the system information.

    gopsutil is a Go package that provides a set of functions to retrieve system and process metrics from various platforms. It serves as a convenient and cross-platform way to gather information about system resources such as CPU usage, memory usage, disk usage, and more.


    CPU Metrics

    You can retrieve information about CPU usage, including overall usage percentage and per-core usage.

    You are going to use the Percent function to get the CPU Utilization %:

    cpuUsage, err = cpu.Percent(time.Second, false)
    

    It returns cpuUsage and err :

    • cpuUsage: This is a variable that will store the CPU usage percentage. It is an array of Utilization per CPU.
    • err: This is a variable that will store any error that might occur during the function call.

    Here's the function signature for reference:

    func Percent(interval time.Duration, percpu bool) ([]float64, error)
    
    • interval : The amount of time over which the percentage is calculated.
    • percpu : If true, the function returns the percentage for each CPU core. If false, it returns an array with a average CPU percentage across all cores.

    *** #### Memory Metrics

    You can use the function below to get the memory detalis:

    memInfo, err = mem.VirtualMemory()
    
    • mem.VirtualMemory(): This function is part of the gopsutil library and returns a mem.VirtualMemoryStat structure, which contains information about virtual memory usage, such as total, available, used, and used percentage.
    • err := mem.VirtualMemory() : This function may return an error, so it's a good practice to check for errors. If there is an error, you can handle it accordingly.
    • memInfo.UsedPercent: After obtaining the virtual memory information, you can use the UsedPercent field of the mem.VirtualMemoryStat structure to get the percentage of used memory.

    Process

    Similar to CPU and Memory , you can also retrieve the running processes in the system using the process package of gopsutil.

    Let's see an example:

    processes, err = process.Processes()
    if err != nil {
        // Handle error
    }
    for _, p := range processes {
        name, err := p.Name()
        if err == nil {
            fmt.Println("Process:", name)
        }
    }
    
    • process.Processes(): This function returns a slice of *process.Process structures, each representing information about a running process. The information includes process ID (PID), parent process ID (PPID), executable name, and more.

    • err := process.Processes(): As with many functions in gopsutil, it's a good practice to check for errors. If there is an error, you can handle it accordingly.

    • for _, p := range processes { ... }: This loop iterates over the slice of processes obtained from process.Processes().

    • name, err := p.Name(): For each process, the Name() method of the process.Process structure is called to retrieve the name of the executable associated with the process. The Name() method may return an error, so it's checked.

    • if err == nil { fmt.Println("Process:", name) }: If the Name() method call is successful (i.e., no error occurred), the name of the process is printed to the console. This gives you a list of running processes on the system. Great job!

    You were able to successfully retrieve the CPU Usage, Memory Usage and Running Processes.

    For more details about gopsutils and the available functions and their parameters, you can refer to the official documentation on GitHub : gopsutil Documentation.

    Next let's learn about how to make the program concurrent using goroutines.

  3. Challenge

    Goroutines to Read Information Concurrently

    In this step, you will enhance the system monitor's concurrency by introducing Goroutines. Goroutines are lightweight threads in Go that enable concurrent execution.

    Goroutines are distinct from traditional threads in that they are managed by the Go runtime, which is responsible for their scheduling and execution. They are more lightweight than operating system threads, and many goroutines can run concurrently on a small number of operating system threads. This enables efficient parallelism and concurrency without the overhead associated with traditional threading models.

    In Go, you can create a new goroutine by using the go keyword followed by a function call. For example:

    func main() {
    	go worker() // Start a worker goroutine
    
    	// Keep the main goroutine running
    	select {}
    }
    
    func worker() {
    	for {
    		fmt.Println("Working...")
    		time.Sleep(time.Second)
    	}
    }
    
    

    In this example, the worker function runs in a separate goroutine, printing "Working..." every second. The select{} statement ensures that the main Goroutine continues running indefinitely, preventing the program from exiting immediately, thus allowing the worker goroutine to continue its work concurrently.

    Alternatively the above code could also be written using an anonymous function:

    func main() {
    	// Start the worker goroutine using an anonymous function
    	go func() {
    	 for {
    		 worker()
    		 time.Sleep(time.Second)
    	 }
    	}()
    
    	// Keep the main goroutine running
    	select {}
    }
    
    func worker() {
    	for {
    		fmt.Println("Working...")
    		// You may include additional logic here if needed
    	}
    

    Now that you've learned how goroutines work , update your program to continuously print CPU usage, memory usage, and running processes.

  4. Challenge

    Channels and TUI Interface

    In this step, you'll dive into the dynamic realm of concurrent programming with Go, focusing on the versatile use of channels. These channels serve as communication pathways between different parts of your system resource monitor, allowing seamless data exchange among goroutines. Understanding and effectively implementing channels is key to enhancing the responsiveness and efficiency of your monitor.

    Additionally, you'll explore the world of Text User Interfaces (TUI) to craft an engaging terminal-based user interface. Leveraging the tview package, you'll learn how to design and implement an interactive display for your system resource monitor.


    First let's understand about channels.

    Channels

    In Go, channels are a powerful and flexible mechanism for communication and synchronization between goroutines (concurrently executing functions). Channels facilitate the safe exchange of data between goroutines by providing a way for one goroutine to send data to another goroutine. They also help to coordinate the execution of goroutines.

    1. Channel Creation:

    Channels are created using the make function, and you specify the type of data that will be transmitted through the channel. For example:

    // Creates an unbuffered channel for transmitting integers
    ch := make(chan int) 
    

    2. Channel Direction:

    You can specify the direction of a channel, indicating whether it can only send, only receive, or both. The default is a bidirectional channel. Here is an example:

    // Send-only channel
    func sendData(ch chan<- int) {
        // code to send data to the channel
    }
    
    // Receive-only channel
    func receiveData(ch <-chan int) {
        // code to receive data from the channel
    }
    

    3. Channel Operations:

    • Sending Data (<-): Use the <- operator to send data into a channel:
    // Send the value 42 into the channel
    ch <- 42 
    
    • Receiving Data (<-): Use the <- operator to receive data from a channel:
    // Receive a value from the channel and store it in 'value'
    value := <-ch 
    

    4. Blocking

    • A send operation on an unbuffered channel will block until there is a corresponding receive operation, and vice versa.
    • Buffered channels allow a certain number of elements to be stored in the channel without a corresponding receiver. A send operation on a buffered channel will block only when the buffer is full.

    5. Closing Channels:

    You can close a channel using the close function. Once a channel is closed, no more values can be sent on it:

    close(ch)
    



    Here's a simple example that demonstrates the basic usage of channels in Go:
    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func sendData(ch chan<- int) {
    	for i := 0; i < 5; i++ {
    		ch <- i
    		time.Sleep(time.Millisecond * 500)
    	}
    	close(ch)
    }
    
    func main() {
    	ch := make(chan int)
    
    	go sendData(ch)
    
    	for {
    		value, ok := <-ch
    		if !ok {
    			// channel closed
    			break
    		}
    		fmt.Println("Received:", value)
    	}
    
    	fmt.Println("Main goroutine done")
    }
    
    

    In this example, the sendData goroutine sends values into the channel, and the main goroutine receives and prints them until the channel is closed.


    Now let's understand about Text User Interface (TUI)

    TUI Interface

    TUI stands for "Text-based User Interface." A Text-based User Interface is an interface that allows interaction with a program through text commands and displays information in a text format, typically in a terminal or console window.

    Go, being a versatile programming language, has libraries and frameworks that facilitate the creation of Text-based User Interfaces. One such popular library is tview, which is a feature-rich TUI library for Go. Tview provides a variety of components, such as tables, lists, forms, and more, to help developers build interactive and visually appealing terminal applications.

    Here's a simple example of creating a TUI application using tview in Go:

    package main
    
    import (
    	"github.com/rivo/tview"
    )
    
    func main() {
    	app := tview.NewApplication()
    
    	textView := tview.NewTextView().
    		SetText("Hello Learners!").
    		SetTextAlign(tview.AlignCenter)
    
    	if err := app.SetRoot(textView, true).Run(); err != nil {
    		panic(err)
    	}
    }
    

    This example creates a basic TUI application with a TextView component that displays the text "Hello Learners!" in the center of the Terminal window. The tview library simplifies the creation of such interfaces and provides functionalities to handle user input and update the display dynamically.


    Let's understand it step by step :

    • Create a new TUI application using tview.NewApplication():
    func main() {
        // Create a new TUI application
        app := tview.NewApplication()
    
    • The program creates a TextView component with some initial settings:
        textView := tview.NewTextView().
            SetText("Hello Learners!").
            SetTextAlign(tview.AlignCenter)        
    
    • SetText("Hello Learners!") : Sets the initial text content of the TextView.
    • SetTextAlign(tview.AlignCenter) : Aligns the text to the center within the TextView.

          if err := app.SetRoot(textView, true).Run(); err != nil {
            panic(err)
        }
    
    • The program sets the root component of the application to the created TextView. The second argument, true, indicates that the TextView should take up the entire screen. Finally, the Run() method starts the TUI application, and any potential errors are handled by panicking.
  5. Challenge

    Integrating Channels and Building a TUI Interface

    Now that you've understood about the Channels and TUI , let's look at a example containing both channels and TUI.

    This example creates a TUI application that initially shows a loading screen and then periodically updates the TUI with random numbers, providing a simple example of a dynamic console-based user interface:

    package main
    
    import (
    	"fmt"
    	"math/rand"
    	"time"
    
    	"github.com/gdamore/tcell/v2"
    	"github.com/rivo/tview"
    )
    
    func main() {
    	// Create a new TUI application
    	app := tview.NewApplication()
    
    	// Show the loading screen
    	showLoadingScreen(app)
    
    	// Channel for updating system information
    	updateCh := make(chan *tview.Flex, 1)
    
    	// Close the update channel when the application is stopped
    	defer close(updateCh)
    
    	// Periodically update TUI with system information
    	go func() {
    		for {
    			renderInfo(updateCh)
    			time.Sleep(2 * time.Second)
    		}
    	}()
    
    	// Goroutine to handle TUI updates
    	go func() {
    		for {
    			select {
    			case updatedFlex, ok := <-updateCh:
    				if !ok {
    					return
    				}
    				app.QueueUpdateDraw(func() {
    					app.SetRoot(updatedFlex, true)
    				})
    			}
    		}
    	}()
    
    	// Handle keyboard events to exit the application
    	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
    		if event.Key() == tcell.KeyEscape || event.Rune() == 'q' {
    			// Close the update channel to stop goroutines
    			close(updateCh) 
    			// Stop the TUI application
    			app.Stop()      
    		}
    		return event
    	})
    
    	// Keep the application running
    	select {}
    }
    
    func showLoadingScreen(app *tview.Application) {
    	// Create Flex layout for the main container
    	flex := tview.NewFlex().SetDirection(tview.FlexRow)
    	flex.SetBorder(true)
    
    	// Add TextViews to Flex container
    	infoTextView := tview.NewTextView().
    		SetText("Loading...").
    		SetTextAlign(tview.AlignCenter).
    		SetDynamicColors(true)
    
    	flex.AddItem(infoTextView, 1, 1, false)
    
    	// Add the Flex layout to the application
    	go func() {
    		if err := app.SetRoot(flex, true).Run(); err != nil {
    			panic(err)
    		}
    	}()
    }
    
    func renderInfo(updateCh chan<- *tview.Flex) {
    	randomInt := rand.Int()
    	randomFloat := rand.Float64()
    
    	// Create Flex layout for the updated container
    	flex := tview.NewFlex().SetDirection(tview.FlexRow)
    	flex.SetBorder(true)
    
    	title := tview.NewTextView().
    		SetText(" Sample Title ").
    		SetTextAlign(tview.AlignCenter)
    
    	// Create TextViews for updated CPU, Memory, and Processes information
    	box1 := createResourceBox(fmt.Sprintf("Box 1 Random Int   : %d ", randomInt))
    	box2 := createResourceBox(fmt.Sprintf("Box 2 Random Float : %.2f ", randomFloat))
    
    	// Add updated TextViews to Flex container
    	flex.AddItem(title, 1, 1, false).
    		AddItem(box1, 1, 1, false).
    		AddItem(box2, 0, 1, false)
    
    	// Update the channel with the updated Flex layout
    	updateCh <- flex
    }
    
    func createResourceBox(title string) *tview.Flex {
    	textView := tview.NewTextView().
    		SetText(fmt.Sprintf("%s\n\n", title)).
    		SetTextColor(tcell.Color107).
    		SetDynamicColors(true).
    		SetTextAlign(tview.AlignLeft)
    
    	flex := tview.NewFlex().SetDirection(tview.FlexColumn).
    		AddItem(textView, 0, 1, false)
    
    	return flex
    }
    

    Output

    The image shows a black background with a white border and green text. The title "Sample Title" is centered at the top. Below the title, two lines of text read: - "Box 1 Random Int: 227338160" - "Box 2 Random Float: 0.74"


    Let's understand it step by step :

    • A new TUI application is created using the tview.NewApplication() function.

    • The showLoadingScreen method is called to display a loading screen on the TUI.

    func showLoadingScreen(app *tview.Application) {
    	flex := tview.NewFlex().SetDirection(tview.FlexRow)
    	flex.SetBorder(true)
    	infoTextView := tview.NewTextView().
    		SetText("Loading...").
    		SetTextAlign(tview.AlignCenter).
    		SetDynamicColors(true)
    	flex.AddItem(infoTextView, 1, 1, false)
    
    	go func() {
    		if err := app.SetRoot(flex, true).Run(); err != nil {
    			panic(err)
    		}
    	}()
    }
    

    The showLoadingScreen method creates a flex layout for the main container, adds a text view with the message "Loading...", and sets it as the root of the application.

    • updateCh := make(chan *tview.Flex, 1): A channel named updateCh of type *tview.Flex is created. This channel is used for communication between goroutines to update the TUI.

    • defer close(updateCh): The defer statement ensures that the updateCh channel is closed when the main function exits.

    • Periodically update the TUI with the following code:
    go func() {
    		for {
    			renderInfo(updateCh)
    			time.Sleep(2 * time.Second)
    		}
    	}()
    

    A goroutine is launched to periodically call the renderInfo method every 2 seconds. renderInfo method generates random information and updates the TUI layout.

    func renderInfo(updateCh chan<- *tview.Flex) {
    	randomInt := rand.Int()
    	randomFloat := rand.Float64()
    	flex := tview.NewFlex().SetDirection(tview.FlexRow)
    	flex.SetBorder(true)
    	title := tview.NewTextView().
    		SetText(" Sample Title ").
    		SetTextAlign(tview.AlignCenter)
    	box1 := createResourceBox(fmt.Sprintf("Box 1 Random Int   : %d ", randomInt))
    	box2 := createResourceBox(fmt.Sprintf("Box 2 Random Float : %.2f ", randomFloat))
    	flex.AddItem(title, 1, 1, false).
    		AddItem(box1, 1, 1, false).
    		AddItem(box2, 0, 1, false)
    	updateCh <- flex
    }
    
    

    The renderInfo function generates random integers and floats, creates a new flex layout with text views displaying this information, and then updates the updateCh channel with the updated layout.

    Let's understand about the AddItem method.

    AddItem(box1, 1, 1, false).

    The first argument is the textView which has to be added to flex layout. The second argument is the fixedSize of the tview component.

    • 1 means it will occupy one row.
    • 0 means it is flexible and can expand to take up all available rows. Third argument is the proportion of textView in the layout. The last one being the focus.

    The renderInfo function also calls the createResouceBox function:

    func createResourceBox(title string) *tview.Flex {
    	textView := tview.NewTextView().
    		SetText(fmt.Sprintf("%s\n\n", title)).
    		SetTextColor(tcell.Color107).
    		SetDynamicColors(true).
    		SetTextAlign(tview.AlignLeft)
    
    	flex := tview.NewFlex().SetDirection(tview.FlexColumn).
    		AddItem(textView, 0, 1, false)
    
    	return flex
    }
    
    

    The createResourceBox function creates a flex layout containing a text view with the specified title. The layout is then returned.


    • Goroutine to handle TUI updates:
    	go func() {
    		for {
    			select {
    			case updatedFlex, ok := <-updateCh:
    				if !ok {
    					return
    				}
    				app.QueueUpdateDraw(func() {
    					app.SetRoot(updatedFlex, true)
    				})
    			}
    		}
    	}()
    

    Another goroutine is created to handle TUI updates. It listens for updates on the updateCh channel and uses the QueueUpdateDraw method to update the TUI layout with the received information.


    • Handle keyboard events:
    	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
    		if event.Key() == tcell.KeyEscape || event.Rune() == 'q' {
    			close(updateCh)
    			app.Stop()
    		}
    		return event
    	})
    
    

    The SetInputCapture method is used to set up a function that captures keyboard events. If the 'q' key or the 'Escape' key is pressed, it closes the updateCh channel gracefully and stops the TUI application. Great job ! You have succssfully learned and implemented goroutines, channels, and TUI.

    There are some more things you could try to improve in this System Monitor:

    1. Increase the number of displayed processes. Currently , for simplicy only first three processes are displayed, you can increase it in the function GetRunningProcess located in systeminfo.go file.

    2. Add more system information using other packages of gopsutil:

    • Disk - "github.com/shirou/gopsutil/v3/disk"
    • Network - "github.com/shirou/gopsutil/v3/net"

    3. Enhance the user interface using TUI library. You can make it look better by tweaking and adding more colors to the display.

    Happy learning!

Amar Sonwani is a software architect with more than twelve years of experience. He has worked extensively in the financial industry and has expertise in building scalable applications.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.