# Building Simple Real-Time System Monitor using Go, HTMX, and Web Socket

I was finding a fun project to work with Go, HTMX, and Tailwwindcss and ended up built a simple real-time web based system monitor with the power of web socket. Here’s the result.

![screenshot of simple web based system monitor](https://cdn.hashnode.com/res/hashnode/image/upload/v1731297504702/1075ce7e-8290-4448-8f33-60801aaf9fe8.png align="center")

It shows system information, memories, disk, CPU, and running processes, updated automatically every 5 seconds.

I’ll break down the code little bit in this post.

# Stacks

* Go 1.23.2
    
* [Htmx](https://htmx.org/)
    
* [Tailwindcss](https://tailwindcss.com/)
    
* [Gopsutil](https://pkg.go.dev/github.com/shirou/gopsutil/v4)
    
* [Websocket](https://github.com/coder/websocket)
    
* [Htmx websocket extension](https://htmx.org/extensions/ws/)
    

# HTTP Server

```go
type HttpServer struct {
	subscriberMessageBuffer int
	Mux                     http.ServeMux
	subscribersMutex        sync.Mutex
	subscribers             map[*subscriber]struct{}
}

type subscriber struct {
	msgs chan []byte
}
```

It’s quite straightforward. `HttpServer` contains a `http.ServeMux` as http handler and `subscribers` for web socket broadcasting later. `subscriber` is simply has `msgs` channel for data update.

Since it only needs to serve a single HTML file, then it needs URL to show the page, and one URL for web socket connection.

```go
func NewHttpServer() *HttpServer {
	s := &HttpServer{
		subscriberMessageBuffer: 10,
		subscribers:             make(map[*subscriber]struct{}),
	}

	s.Mux.Handle("/", http.FileServer(http.Dir("./views")))
	s.Mux.HandleFunc("/ws", s.subscribeHandler)
	return s
}
```

# Web Socket Connection & Subscriber

Endpoint `/ws` will handling web socket connection and managing a subscriber. First it will initiate a new subscriber and added it to a map in the http server structure. `Lock` will be used to prevent race condition since we will use go routine later.

```go
func (s *HttpServer) subscribeHandler(w http.ResponseWriter, r *http.Request) {
	err := s.subscribe(r.Context(), w, r)
	if err != nil {
		fmt.Println(err)
		return
	}
}

func (s *HttpServer) addSubscriber(subscriber *subscriber) {
	s.subscribersMutex.Lock()
	s.subscribers[subscriber] = struct{}{}
	s.subscribersMutex.Unlock()
	fmt.Println("subscriber added", subscriber)
}
```

Web socket is starting accept a connection and via loop, we will detect an incoming channel `msgs` from subscriber and write it to web socket.

```go
func (s *HttpServer) subscribe(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
	var c *websocket.Conn
	subscriber := &subscriber{
		msgs: make(chan []byte, s.subscriberMessageBuffer),
	}

	s.addSubscriber(subscriber)

	c, err := websocket.Accept(w, r, nil)
	if err != nil {
		return err
	}

	defer c.CloseNow()

	ctx = c.CloseRead(ctx)
	for {
		select {
		case msg := <-subscriber.msgs:
			ctx, cancel := context.WithTimeout(ctx, time.Second)
			defer cancel()
			err := c.Write(ctx, websocket.MessageText, msg)
			if err != nil {
				return err
			}
		case <-ctx.Done():
			return ctx.Err()
		}
	}
}
```

# Auto Update

Auto update the system info data is handled by go routine. We will build a html response that will be sent via web socket and htmx will handle the update on the html side.

```go
func main() {
	fmt.Println("Starting system monitor")
	s := server.NewHttpServer()

	go func(s *server.HttpServer) {
		for {
			hostStat, _ := host.Info()
			timestamp := time.Now().Format("2006-01-02 15:04:05")
			html := `
			<span hx-swap-oob="innerHTML:#data-timestamp">` + timestamp + `</span>
			<span hx-swap-oob="innerHTML:#system-hostname">` + hostStat.Hostname + `</span>
			<span hx-swap-oob="innerHTML:#system-os">` + hostStat.OS + `</span>
			`
			s.Broadcast([]byte(html))
			time.Sleep(time.Second * 5)
		}
	}(s)
    // ...
}
```

Syntax `hx-swap-oob="innerHTML:#data-timestamp"` in htmx is tell us that swap a component inside `data-timestamp` id in our HTML. All swapping mechanism will be the same for other system information components.

All swappable `html` components will be sent via `Broadcast(msg)` method and later will be sent via channel every 5 seconds.

```go
func (s *HttpServer) Broadcast(msg []byte) {
	s.subscribersMutex.Lock()
	for subscriber := range s.subscribers {
		subscriber.msgs <- msg
	}
	s.subscribersMutex.Unlock()
}
```

# The HTMX View

It’s plain HTML file and for Tailwindcss I simple used CDN for that

```xml
<script src="https://cdn.tailwindcss.com"></script>
```

Same idea for HTMX and web socket extension for using CDN

```xml
<script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous"></script>
<script src="https://unpkg.com/htmx-ext-ws@2.0.1/ws.js"></script>
```

# How to connect to the web socket?

The system monitor page is expected to receives all the data by web socket so I can set it from the main div container. Specify `hx-ext=”ws”` to tell HTMX for using web socket extension and `ws-connect=”/ws”` to tell web socket to connect via `/ws` URL.

```xml
<body class="bg-gray-700 text-white">
    <div class="container mx-auto p-8" hx-ext="ws" ws-connect="/ws">
   <!-- all the data -->
    </div>
</body>
```

# Full Code

Here is the full version of the code [https://github.com/didikz/gosysmon-web](https://github.com/didikz/gosysmon-web) and you may want to play around with your own version.

Happy coding!
