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.
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
- Tailwindcss
- Gopsutil
- Websocket
- Htmx websocket extension
HTTP Server
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.
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.
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.
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.
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.
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
<script src="https://cdn.tailwindcss.com"></script>
Same idea for HTMX and web socket extension for using CDN.
<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.
<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 and you may want to play around with your own version.
Happy coding!
Top comments (2)
Excellent article! It’s a great demonstration of how Go, HTMX, and WebSockets can work seamlessly together for real-time updates. The explanation is clear, and the use of htmx-ext-ws for dynamic updates is particularly clever. This is a solid foundation for anyone looking to build lightweight, real-time web apps. Great work! 🚀
Thanks!