├── .env.example
├── .gitignore
├── README.md
├── cmd
└── main.go
├── go.mod
├── go.sum
├── internal
└── server
│ └── http_server.go
├── pkg
└── util
│ ├── helper.go
│ └── helper_test.go
├── screenshot.png
└── views
└── index.html
/.env.example:
--------------------------------------------------------------------------------
1 | APP_ENV=local
2 | APP_PORT=8000
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Mac OS X files
2 | .DS_Store
3 |
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, build with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
18 | .glide/
19 |
20 | # Dependency directories (remove the comment below to include it)
21 | # vendor/
22 |
23 | # env variables
24 | .env
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GoSysMon
2 |
3 | Is a simple web based System Monitoring built with Go. It displays computer informations such are host, processes, memory, CPU, and disk. By using websocket, the data will automatically been refreshed within 5 seconds.
4 |
5 | 
6 |
7 | > Learning reference: [https://www.youtube.com/watch?v=fBDUn7b9plw&list=LL&index=3&ab_channel=sigfault](https://www.youtube.com/watch?v=fBDUn7b9plw&list=LL&index=3&ab_channel=sigfault)
8 |
9 | ## Stacks
10 |
11 | + Go 1.23.1
12 | + HTMX
13 | + Websocket HTMX Extension
14 | + Tailwindcss
15 | + [Gopsutil](https://pkg.go.dev/github.com/shirou/gopsutil/v4)
16 | + [Websocket](https://github.com/coder/websocket)
17 |
18 | ## Running The Application
19 |
20 | + Copy env file `cp .env.example .env` and adjust you desire port (default port is `8000`)
21 | + Install dependencies
22 |
23 | ```bash
24 | go mod tidy
25 | ```
26 |
27 | + Run the application locally
28 |
29 | ```bash
30 | go run ./cmd/main.go
31 | ```
32 |
33 | + Visit `localhost:8000` or using your desired port defined in `.env` file
34 |
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "os"
10 | "os/signal"
11 | "sort"
12 | "strconv"
13 | "syscall"
14 | "time"
15 |
16 | "github.com/didikz/gosysmon/internal/server"
17 | "github.com/didikz/gosysmon/pkg/util"
18 | "github.com/joho/godotenv"
19 | "github.com/shirou/gopsutil/v4/cpu"
20 | "github.com/shirou/gopsutil/v4/disk"
21 | "github.com/shirou/gopsutil/v4/host"
22 | "github.com/shirou/gopsutil/v4/mem"
23 | "github.com/shirou/gopsutil/v4/process"
24 | )
25 |
26 | func main() {
27 | fmt.Println("Starting system monitor")
28 | s := server.NewHttpServer()
29 |
30 | go func(s *server.HttpServer) {
31 | for {
32 | hostStat, _ := host.Info()
33 | vmStat, _ := mem.VirtualMemory()
34 |
35 | cpus, _ := cpu.Info()
36 | var cpuInfo string
37 | for _, cpu := range cpus {
38 | cpuInfo += `
39 |
40 | Manufacturer
41 | ` + cpu.VendorID + `
42 |
43 |
44 | Model
45 | ` + cpu.ModelName + `
46 |
47 |
48 | Family
49 | ` + cpu.Family + `
50 |
51 |
52 | Speed
53 | ` + fmt.Sprintf("%.2f MHz", cpu.Mhz) + `
54 |
55 |
56 | Cores
57 | ` + fmt.Sprintf("%d cores", cpu.Cores) + `
58 |
59 | `
60 | }
61 |
62 | partitionStats, _ := disk.Partitions(true)
63 | partitions, totalStorage, usedStorage, freeStorage := "", "", "", ""
64 |
65 | for _, partition := range partitionStats {
66 | diskUsage, _ := disk.Usage(partition.Mountpoint)
67 | if partitions == "" {
68 | partitions = fmt.Sprintf("%s (%s)", partition.Mountpoint, partition.Fstype)
69 | } else {
70 | partitions += fmt.Sprintf(", %s (%s)", partition.Mountpoint, partition.Fstype)
71 | }
72 |
73 | if diskUsage != nil {
74 | if totalStorage == "" {
75 | totalStorage = fmt.Sprintf("%s %dGB", partition.Mountpoint, util.BytesToGigabyte(diskUsage.Total))
76 | } else {
77 | totalStorage += fmt.Sprintf(", %s %dGB", partition.Mountpoint, util.BytesToGigabyte(diskUsage.Total))
78 | }
79 |
80 | if usedStorage == "" {
81 | usedStorage = fmt.Sprintf("%s %dGB (%.2f%%)", partition.Mountpoint, util.BytesToGigabyte(diskUsage.Used), diskUsage.UsedPercent)
82 | } else {
83 | usedStorage += fmt.Sprintf(", %s %dGB (%.2f%%)", partition.Mountpoint, util.BytesToGigabyte(diskUsage.Used), diskUsage.UsedPercent)
84 | }
85 |
86 | if freeStorage == "" {
87 | freeStorage = fmt.Sprintf("%s %dGB", partition.Mountpoint, util.BytesToGigabyte(diskUsage.Free))
88 | } else {
89 | freeStorage += fmt.Sprintf(", %s %dGB", partition.Mountpoint, util.BytesToGigabyte(diskUsage.Free))
90 | }
91 | }
92 | }
93 |
94 | processess, _ := process.Processes()
95 | sort.Slice(processess, func(i, j int) bool {
96 | p1, _ := processess[i].CPUPercent()
97 | p2, _ := processess[j].CPUPercent()
98 | return p1 > p2
99 | })
100 |
101 | processessRow := ""
102 | for i := 0; i < 10; i++ {
103 | n, _ := processess[i].Name()
104 | cp, _ := processess[i].CPUPercent()
105 | rowColor := ""
106 | if i%2 == 0 {
107 | rowColor = "bg-gray-500"
108 | }
109 | processessRow += fmt.Sprintf(`
110 |
111 | %s (PID %d)
112 | %.2f%% CPU
113 |
114 | `, rowColor, n, processess[i].Pid, cp)
115 | }
116 |
117 | timestamp := time.Now().Format("2006-01-02 15:04:05")
118 | html := `
119 | ` + timestamp + `
120 | ` + hostStat.Hostname + `
121 | ` + hostStat.OS + `
122 | ` + fmt.Sprintf("%s (%s)", hostStat.Platform, hostStat.PlatformFamily) + `
123 | ` + hostStat.PlatformVersion + `
124 | ` + fmt.Sprintf("%s (%s)", hostStat.KernelArch, hostStat.KernelVersion) + `
125 | ` + strconv.Itoa(int(hostStat.Procs)) + `
126 | ` + strconv.Itoa(int(util.BytesToGigabyte(vmStat.Total))) + `GB
127 | ` + strconv.Itoa(int(util.BytesToGigabyte(vmStat.Used))) + `GB (` + fmt.Sprintf("%.2f%%", vmStat.UsedPercent) + `)
128 | ` + strconv.Itoa(int(util.BytesToGigabyte(vmStat.Free))) + `GB
129 | ` + cpuInfo + `
130 | ` + partitions + `
131 | ` + totalStorage + `
132 | ` + usedStorage + `
133 | ` + freeStorage + `
134 | ` + processessRow + `
135 | `
136 | s.Broadcast([]byte(html))
137 | time.Sleep(time.Second * 5)
138 | }
139 | }(s)
140 |
141 | err := godotenv.Load()
142 | if err != nil {
143 | log.Fatal(err)
144 | }
145 |
146 | port := os.Getenv("APP_PORT")
147 | if port == "" {
148 | port = "8000"
149 | }
150 |
151 | server := &http.Server{
152 | Addr: fmt.Sprintf(":%s", port),
153 | Handler: &s.Mux,
154 | }
155 |
156 | stop := make(chan os.Signal, 1)
157 | signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
158 |
159 | go func() {
160 | if err = server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
161 | log.Fatal(err)
162 | }
163 | log.Println("Stopped serving new connections")
164 | }()
165 |
166 | <-stop
167 | log.Println("Shutting down gracefully...")
168 |
169 | shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
170 | defer shutdownRelease()
171 |
172 | if err := server.Shutdown(shutdownCtx); err != nil {
173 | log.Printf("server shutdown error: %v\n", err)
174 | }
175 |
176 | log.Println("Server stoppped")
177 | }
178 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/didikz/gosysmon
2 |
3 | go 1.23.2
4 |
5 | require (
6 | github.com/coder/websocket v1.8.12
7 | github.com/joho/godotenv v1.5.1
8 | github.com/shirou/gopsutil/v4 v4.24.10
9 | )
10 |
11 | require (
12 | github.com/ebitengine/purego v0.8.1 // indirect
13 | github.com/go-ole/go-ole v1.2.6 // indirect
14 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
15 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
16 | github.com/tklauser/go-sysconf v0.3.12 // indirect
17 | github.com/tklauser/numcpus v0.6.1 // indirect
18 | github.com/yusufpapurcu/wmi v1.2.4 // indirect
19 | golang.org/x/sys v0.26.0 // indirect
20 | )
21 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
2 | github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
6 | github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
7 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
8 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
9 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
10 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
11 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
12 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
13 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
14 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
15 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
18 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
19 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
20 | github.com/shirou/gopsutil/v4 v4.24.10 h1:7VOzPtfw/5YDU+jLEoBwXwxJbQetULywoSV4RYY7HkM=
21 | github.com/shirou/gopsutil/v4 v4.24.10/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
22 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
23 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
24 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
25 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
26 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
27 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
28 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
29 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
30 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
31 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
32 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
35 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
36 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
37 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
38 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
39 |
--------------------------------------------------------------------------------
/internal/server/http_server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "sync"
8 | "time"
9 |
10 | "github.com/coder/websocket"
11 | )
12 |
13 | type HttpServer struct {
14 | subscriberMessageBuffer int
15 | Mux http.ServeMux
16 | subscribersMutex sync.Mutex
17 | subscribers map[*subscriber]struct{}
18 | }
19 |
20 | type subscriber struct {
21 | msgs chan []byte
22 | }
23 |
24 | func NewHttpServer() *HttpServer {
25 | s := &HttpServer{
26 | subscriberMessageBuffer: 10,
27 | subscribers: make(map[*subscriber]struct{}),
28 | }
29 |
30 | s.Mux.Handle("/", http.FileServer(http.Dir("./views")))
31 | s.Mux.HandleFunc("/ws", s.subscribeHandler)
32 | return s
33 | }
34 |
35 | func (s *HttpServer) subscribeHandler(w http.ResponseWriter, r *http.Request) {
36 | err := s.subscribe(r.Context(), w, r)
37 | if err != nil {
38 | fmt.Println(err)
39 | return
40 | }
41 | }
42 |
43 | func (s *HttpServer) subscribe(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
44 | var c *websocket.Conn
45 | subscriber := &subscriber{
46 | msgs: make(chan []byte, s.subscriberMessageBuffer),
47 | }
48 |
49 | s.addSubscriber(subscriber)
50 |
51 | c, err := websocket.Accept(w, r, nil)
52 | if err != nil {
53 | return err
54 | }
55 |
56 | defer c.CloseNow()
57 |
58 | ctx = c.CloseRead(ctx)
59 | for {
60 | select {
61 | case msg := <-subscriber.msgs:
62 | ctx, cancel := context.WithTimeout(ctx, time.Second)
63 | defer cancel()
64 | err := c.Write(ctx, websocket.MessageText, msg)
65 | if err != nil {
66 | return err
67 | }
68 | case <-ctx.Done():
69 | return ctx.Err()
70 | }
71 | }
72 | }
73 |
74 | func (s *HttpServer) addSubscriber(subscriber *subscriber) {
75 | s.subscribersMutex.Lock()
76 | s.subscribers[subscriber] = struct{}{}
77 | s.subscribersMutex.Unlock()
78 | fmt.Println("subscriber added", subscriber)
79 | }
80 |
81 | func (s *HttpServer) Broadcast(msg []byte) {
82 | s.subscribersMutex.Lock()
83 | for subscriber := range s.subscribers {
84 | subscriber.msgs <- msg
85 | }
86 | s.subscribersMutex.Unlock()
87 | }
88 |
--------------------------------------------------------------------------------
/pkg/util/helper.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import "math"
4 |
5 | func BytesToGigabyte(b uint64) uint64 {
6 | return b / uint64(math.Pow(2, 30))
7 | }
8 |
--------------------------------------------------------------------------------
/pkg/util/helper_test.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "math"
5 | "testing"
6 | )
7 |
8 | func TestByteToGigabyteIsValid(t *testing.T) {
9 | got := BytesToGigabyte(uint64(math.Pow(2, 30) * 5))
10 | if got != uint64(5) {
11 | t.Errorf("BytesToGigabyte(%d) = %d; wants 5", uint64(math.Pow(2, 30)*5), got)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/didikz/gosysmon-web/3cb2ac162d0828f6a4fb4eba26ab3b362cba4d77/screenshot.png
--------------------------------------------------------------------------------
/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | GoSysMon - Go System Monitor
8 |
9 |
10 |
11 |
12 |
System Monitor
13 |
17 |
18 |
19 |
20 |
21 |
24 |
System
25 |
26 |
27 | -
28 | Hostname
29 |
30 |
31 | -
32 | Operating System
33 |
34 |
35 | -
36 | Platform
37 |
38 |
39 | -
40 | Version
41 |
42 |
43 | -
44 | Architecture
45 |
46 |
47 | -
48 | Running Processess
49 |
50 |
51 | -
52 | Total Memory
53 |
54 |
55 | -
56 | Used Memory
57 |
58 |
59 | -
60 | Free Memory
61 |
62 |
63 |
64 |
65 |
66 |
67 |
70 |
CPU
71 |
72 |
73 | -
74 | Manufacturer
75 |
76 |
77 | -
78 | Model
79 |
80 |
81 | -
82 | Family
83 |
84 |
85 | -
86 | Cores
87 |
88 |
89 |
90 |
91 |
92 |
93 |
96 |
Disk
97 |
98 |
99 | -
100 | Partitions
101 |
102 |
103 | -
104 | Total Storage
105 |
106 |
107 | -
108 | Used Storage
109 |
110 |
111 | -
112 | Free Storage
113 |
114 |
115 |
116 |
117 |
118 |
119 |
123 |
10 Most CPU Processes
124 |
125 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------