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.
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 }
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 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() }
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>
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> <h2> Full Code </h2> <p>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.</p> <p>Happy coding!</p>
The above is the detailed content of Building Simple Real-Time System Monitor using Go, HTMX, and Web Socket. For more information, please follow other related articles on the PHP Chinese website!