├── .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 | ![alt text](screenshot.png "screenshot of system monitor") 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 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 | 22 | 23 | 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 | 68 | 69 | 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 | 94 | 95 | 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 | 120 | 121 | 122 | 123 | 10 Most CPU Processes 124 |
    125 |
      126 | 127 |
    128 |
    129 |
    130 |
    131 | 132 | 133 | 134 | --------------------------------------------------------------------------------