From ac0049b76d769c78794649db3da4d86c130741a2 Mon Sep 17 00:00:00 2001 From: Redish101 Date: Sat, 7 Sep 2024 16:27:36 +0800 Subject: [PATCH] feat: Migrate monitor middleware from Fiber v2 to v3 --- go.mod | 5 + go.sum | 11 ++ middleware/monitor/config.go | 132 +++++++++++++++++ middleware/monitor/index.go | 271 ++++++++++++++++++++++++++++++++++ middleware/monitor/monitor.go | 137 +++++++++++++++++ 5 files changed, 556 insertions(+) create mode 100644 middleware/monitor/config.go create mode 100644 middleware/monitor/index.go create mode 100644 middleware/monitor/monitor.go diff --git a/go.mod b/go.mod index dd88f5b1d9..56445cc6dc 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/google/uuid v1.6.0 github.com/mattn/go-colorable v0.1.13 github.com/mattn/go-isatty v0.0.20 + github.com/shirou/gopsutil v3.21.11+incompatible github.com/stretchr/testify v1.9.0 github.com/tinylib/msgp v1.1.8 github.com/valyala/bytebufferpool v1.0.0 @@ -16,10 +17,14 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect diff --git a/go.sum b/go.sum index 1c4e23e75d..c8597b36d1 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/gofiber/utils/v2 v2.0.0-beta.6 h1:ED62bOmpRXdgviPlfTmf0Q+AXzhaTUAFtdWjgx+XkYI= github.com/gofiber/utils/v2 v2.0.0-beta.6/go.mod h1:3Kz8Px3jInKFvqxDzDeoSygwEOO+3uyubTmUa6PqY+0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -17,10 +19,16 @@ github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8= @@ -28,6 +36,8 @@ github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRV github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -42,6 +52,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/middleware/monitor/config.go b/middleware/monitor/config.go new file mode 100644 index 0000000000..12390acbb0 --- /dev/null +++ b/middleware/monitor/config.go @@ -0,0 +1,132 @@ +package monitor + +import ( + "time" + + "github.com/gofiber/fiber/v3" +) + +// Config defines the config for middleware. +type Config struct { + // Metrics page title + // + // Optional. Default: "Fiber Monitor" + Title string + + // Refresh period + // + // Optional. Default: 3 seconds + Refresh time.Duration + + // Whether the service should expose only the monitoring API. + // + // Optional. Default: false + APIOnly bool + + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Custom HTML Code to Head Section(Before End) + // + // Optional. Default: empty + CustomHead string + + // FontURL for specify font resource path or URL . also you can use relative path + // + // Optional. Default: https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap + FontURL string + + // ChartJsURL for specify ChartJS library path or URL . also you can use relative path + // + // Optional. Default: https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js + ChartJsURL string // TODO: Rename to "ChartJSURL" in v3 + + index string +} + +var ConfigDefault = Config{ + Title: defaultTitle, + Refresh: defaultRefresh, + FontURL: defaultFontURL, + ChartJsURL: defaultChartJSURL, + CustomHead: defaultCustomHead, + APIOnly: false, + Next: nil, + index: newIndex(viewBag{ + defaultTitle, + defaultRefresh, + defaultFontURL, + defaultChartJSURL, + defaultCustomHead, + }), +} + +func configDefault(config ...Config) Config { + // Users can change ConfigDefault.Title/Refresh which then + // become incompatible with ConfigDefault.index + if ConfigDefault.Title != defaultTitle || + ConfigDefault.Refresh != defaultRefresh || + ConfigDefault.FontURL != defaultFontURL || + ConfigDefault.ChartJsURL != defaultChartJSURL || + ConfigDefault.CustomHead != defaultCustomHead { + if ConfigDefault.Refresh < minRefresh { + ConfigDefault.Refresh = minRefresh + } + // update default index with new default title/refresh + ConfigDefault.index = newIndex(viewBag{ + ConfigDefault.Title, + ConfigDefault.Refresh, + ConfigDefault.FontURL, + ConfigDefault.ChartJsURL, + ConfigDefault.CustomHead, + }) + } + + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Title == "" { + cfg.Title = ConfigDefault.Title + } + + if cfg.Refresh == 0 { + cfg.Refresh = ConfigDefault.Refresh + } + if cfg.FontURL == "" { + cfg.FontURL = defaultFontURL + } + + if cfg.ChartJsURL == "" { + cfg.ChartJsURL = defaultChartJSURL + } + if cfg.Refresh < minRefresh { + cfg.Refresh = minRefresh + } + + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + + if !cfg.APIOnly { + cfg.APIOnly = ConfigDefault.APIOnly + } + + // update cfg.index with custom title/refresh + cfg.index = newIndex(viewBag{ + title: cfg.Title, + refresh: cfg.Refresh, + fontURL: cfg.FontURL, + chartJSURL: cfg.ChartJsURL, + customHead: cfg.CustomHead, + }) + + return cfg +} diff --git a/middleware/monitor/index.go b/middleware/monitor/index.go new file mode 100644 index 0000000000..c873290c46 --- /dev/null +++ b/middleware/monitor/index.go @@ -0,0 +1,271 @@ +package monitor + +import ( + "strconv" + "strings" + "time" +) + +type viewBag struct { + title string + refresh time.Duration + fontURL string + chartJSURL string + customHead string +} + +// returns index with new title/refresh +func newIndex(dat viewBag) string { + timeout := dat.refresh.Milliseconds() - timeoutDiff + if timeout < timeoutDiff { + timeout = timeoutDiff + } + ts := strconv.FormatInt(timeout, 10) + replacer := strings.NewReplacer("$TITLE", dat.title, "$TIMEOUT", ts, + "$FONT_URL", dat.fontURL, "$CHART_JS_URL", dat.chartJSURL, "$CUSTOM_HEAD", dat.customHead, + ) + return replacer.Replace(indexHTML) +} + +const ( + defaultTitle = "Fiber Monitor" + + defaultRefresh = 3 * time.Second + timeoutDiff = 200 // timeout will be Refresh (in milliseconds) - timeoutDiff + minRefresh = timeoutDiff * time.Millisecond + defaultFontURL = `https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap` + defaultChartJSURL = `https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js` + defaultCustomHead = `` + + // parametrized by $TITLE and $TIMEOUT + indexHTML = ` + + + + + + + + $TITLE + + + +
+

$TITLE

+
+
+
+
CPU Usage
+

0.00%

+
+
+ +
+
+
+
+
Memory Usage
+

0.00 MB

+
+
+ +
+
+
+
+
Response Time
+

0ms

+
+
+ +
+
+
+
+
Open Connections
+

0

+
+
+ +
+
+
+
+ + + +` +) diff --git a/middleware/monitor/monitor.go b/middleware/monitor/monitor.go new file mode 100644 index 0000000000..cbc545d7ef --- /dev/null +++ b/middleware/monitor/monitor.go @@ -0,0 +1,137 @@ +package monitor + +import ( + "os" + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v3" + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/load" + "github.com/shirou/gopsutil/mem" + "github.com/shirou/gopsutil/net" + "github.com/shirou/gopsutil/process" +) + +type stats struct { + PID statsPID `json:"pid"` + OS statsOS `json:"os"` +} + +type statsPID struct { + CPU float64 `json:"cpu"` + RAM uint64 `json:"ram"` + Conns int `json:"conns"` +} + +type statsOS struct { + CPU float64 `json:"cpu"` + RAM uint64 `json:"ram"` + TotalRAM uint64 `json:"total_ram"` + LoadAvg float64 `json:"load_avg"` + Conns int `json:"conns"` +} + +var ( + monitPIDCPU atomic.Value + monitPIDRAM atomic.Value + monitPIDConns atomic.Value + + monitOSCPU atomic.Value + monitOSRAM atomic.Value + monitOSTotalRAM atomic.Value + monitOSLoadAvg atomic.Value + monitOSConns atomic.Value +) + +var ( + mutex sync.RWMutex + once sync.Once + data = &stats{} +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Start routine to update statistics + once.Do(func() { + p, _ := process.NewProcess(int32(os.Getpid())) //nolint:errcheck // TODO: Handle error + numcpu := runtime.NumCPU() + updateStatistics(p, numcpu) + + go func() { + for { + time.Sleep(cfg.Refresh) + + updateStatistics(p, numcpu) + } + }() + }) + + // Return new handler + //nolint:errcheck // Ignore the type-assertion errors + return func(c fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(&c) { + return c.Next() + } + + if c.Method() != fiber.MethodGet { + return fiber.ErrMethodNotAllowed + } + if c.Get(fiber.HeaderAccept) == fiber.MIMEApplicationJSON || cfg.APIOnly { + mutex.Lock() + data.PID.CPU, _ = monitPIDCPU.Load().(float64) + data.PID.RAM, _ = monitPIDRAM.Load().(uint64) + data.PID.Conns, _ = monitPIDConns.Load().(int) + + data.OS.CPU, _ = monitOSCPU.Load().(float64) + data.OS.RAM, _ = monitOSRAM.Load().(uint64) + data.OS.TotalRAM, _ = monitOSTotalRAM.Load().(uint64) + data.OS.LoadAvg, _ = monitOSLoadAvg.Load().(float64) + data.OS.Conns, _ = monitOSConns.Load().(int) + mutex.Unlock() + return c.Status(fiber.StatusOK).JSON(data) + } + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).SendString(cfg.index) + } +} + +func updateStatistics(p *process.Process, numcpu int) { + pidCPU, err := p.Percent(0) + if err == nil { + monitPIDCPU.Store(pidCPU / float64(numcpu)) + } + + if osCPU, err := cpu.Percent(0, false); err == nil && len(osCPU) > 0 { + monitOSCPU.Store(osCPU[0]) + } + + if pidRAM, err := p.MemoryInfo(); err == nil && pidRAM != nil { + monitPIDRAM.Store(pidRAM.RSS) + } + + if osRAM, err := mem.VirtualMemory(); err == nil && osRAM != nil { + monitOSRAM.Store(osRAM.Used) + monitOSTotalRAM.Store(osRAM.Total) + } + + if loadAvg, err := load.Avg(); err == nil && loadAvg != nil { + monitOSLoadAvg.Store(loadAvg.Load1) + } + + pidConns, err := net.ConnectionsPid("tcp", p.Pid) + if err == nil { + monitPIDConns.Store(len(pidConns)) + } + + osConns, err := net.Connections("tcp") + if err == nil { + monitOSConns.Store(len(osConns)) + } +}