diff --git a/internal/gopsutil/common/common_windows.go b/internal/gopsutil/common/common_windows.go index 3b55c72008..32d4548411 100644 --- a/internal/gopsutil/common/common_windows.go +++ b/internal/gopsutil/common/common_windows.go @@ -1,9 +1,11 @@ +//go:build windows // +build windows package common import ( "context" + "fmt" "path/filepath" "strings" "syscall" @@ -70,6 +72,7 @@ var ( ProcNtWow64ReadVirtualMemory64 = ModNt.NewProc("NtWow64ReadVirtualMemory64") PdhOpenQuery = ModPdh.NewProc("PdhOpenQuery") + PdhAddEnglishCounterW = ModPdh.NewProc("PdhAddEnglishCounterW") PdhAddCounter = ModPdh.NewProc("PdhAddCounterW") PdhCollectQueryData = ModPdh.NewProc("PdhCollectQueryData") PdhGetFormattedCounterValue = ModPdh.NewProc("PdhGetFormattedCounterValue") @@ -130,6 +133,62 @@ func CreateCounter(query windows.Handle, pname, cname string) (*CounterInfo, err }, nil } +// GetCounterValue get counter value from handle +// adapted from https://github.com/mackerelio/mackerel-agent/ +func GetCounterValue(counter windows.Handle) (float64, error) { + var value PDH_FMT_COUNTERVALUE_DOUBLE + r, _, err := PdhGetFormattedCounterValue.Call(uintptr(counter), PDH_FMT_DOUBLE, uintptr(0), uintptr(unsafe.Pointer(&value))) + if r != 0 && r != PDH_INVALID_DATA { + return 0.0, err + } + return value.DoubleValue, nil +} + +type Win32PerformanceCounter struct { + PostName string + CounterName string + Query windows.Handle + Counter windows.Handle +} + +func NewWin32PerformanceCounter(postName, counterName string) (*Win32PerformanceCounter, error) { + query, err := CreateQuery() + if err != nil { + return nil, err + } + var counter = Win32PerformanceCounter{ + Query: query, + PostName: postName, + CounterName: counterName, + } + r, _, err := PdhAddEnglishCounterW.Call( + uintptr(counter.Query), + uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(counter.CounterName))), + 0, + uintptr(unsafe.Pointer(&counter.Counter)), + ) + if r != 0 { + return nil, err + } + return &counter, nil +} + +func (w *Win32PerformanceCounter) GetValue() (float64, error) { + r, _, err := PdhCollectQueryData.Call(uintptr(w.Query)) + if r != 0 && err != nil { + if r == PDH_NO_DATA { + return 0.0, fmt.Errorf("%w: this counter has not data", err) + } + return 0.0, err + } + + return GetCounterValue(w.Counter) +} + +func ProcessorQueueLengthCounter() (*Win32PerformanceCounter, error) { + return NewWin32PerformanceCounter("processor_queue_length", `\System\Processor Queue Length`) +} + // WMIQueryWithContext - wraps wmi.Query with a timed-out context to avoid hanging func WMIQueryWithContext(ctx context.Context, query string, dst interface{}, connectServerArgs ...interface{}) error { if _, ok := ctx.Deadline(); !ok { diff --git a/internal/gopsutil/load/load.go b/internal/gopsutil/load/load.go new file mode 100644 index 0000000000..b2cc654de5 --- /dev/null +++ b/internal/gopsutil/load/load.go @@ -0,0 +1,31 @@ +package load + +import ( + "encoding/json" +) + +//var invoke common.Invoker = common.Invoke{} + +type AvgStat struct { + Load1 float64 `json:"load1"` + Load5 float64 `json:"load5"` + Load15 float64 `json:"load15"` +} + +func (l AvgStat) String() string { + s, _ := json.Marshal(l) + return string(s) +} + +type MiscStat struct { + ProcsTotal int64 `json:"procsTotal"` + ProcsCreated int64 `json:"procsCreated"` + ProcsRunning int64 `json:"procsRunning"` + ProcsBlocked int64 `json:"procsBlocked"` + Ctxt int64 `json:"ctxt"` +} + +func (m MiscStat) String() string { + s, _ := json.Marshal(m) + return string(s) +} diff --git a/internal/gopsutil/load/load_bsd.go b/internal/gopsutil/load/load_bsd.go new file mode 100644 index 0000000000..9cb254a05f --- /dev/null +++ b/internal/gopsutil/load/load_bsd.go @@ -0,0 +1,80 @@ +// +build freebsd openbsd + +package load + +import ( + "context" + "os/exec" + "strings" + "unsafe" + + "golang.org/x/sys/unix" +) + +func Avg() (*AvgStat, error) { + return AvgWithContext(context.Background()) +} + +func AvgWithContext(ctx context.Context) (*AvgStat, error) { + // This SysctlRaw method borrowed from + // https://github.com/prometheus/node_exporter/blob/master/collector/loadavg_freebsd.go + type loadavg struct { + load [3]uint32 + scale int + } + b, err := unix.SysctlRaw("vm.loadavg") + if err != nil { + return nil, err + } + load := *(*loadavg)(unsafe.Pointer((&b[0]))) + scale := float64(load.scale) + ret := &AvgStat{ + Load1: float64(load.load[0]) / scale, + Load5: float64(load.load[1]) / scale, + Load15: float64(load.load[2]) / scale, + } + + return ret, nil +} + +type forkstat struct { + forks int + vforks int + __tforks int +} + +// Misc returns miscellaneous host-wide statistics. +// darwin use ps command to get process running/blocked count. +// Almost same as Darwin implementation, but state is different. +func Misc() (*MiscStat, error) { + return MiscWithContext(context.Background()) +} + +func MiscWithContext(ctx context.Context) (*MiscStat, error) { + bin, err := exec.LookPath("ps") + if err != nil { + return nil, err + } + out, err := invoke.CommandWithContext(ctx, bin, "axo", "state") + if err != nil { + return nil, err + } + lines := strings.Split(string(out), "\n") + + ret := MiscStat{} + for _, l := range lines { + if strings.Contains(l, "R") { + ret.ProcsRunning++ + } else if strings.Contains(l, "D") { + ret.ProcsBlocked++ + } + } + + f, err := getForkStat() + if err != nil { + return nil, err + } + ret.ProcsCreated = f.forks + + return &ret, nil +} diff --git a/internal/gopsutil/load/load_darwin.go b/internal/gopsutil/load/load_darwin.go new file mode 100644 index 0000000000..a205f2f8ae --- /dev/null +++ b/internal/gopsutil/load/load_darwin.go @@ -0,0 +1,71 @@ +// +build darwin + +package load + +import ( + "context" + "os/exec" + "strings" + "unsafe" + + "golang.org/x/sys/unix" +) + +func Avg() (*AvgStat, error) { + return AvgWithContext(context.Background()) +} + +func AvgWithContext(ctx context.Context) (*AvgStat, error) { + // This SysctlRaw method borrowed from + // https://github.com/prometheus/node_exporter/blob/master/collector/loadavg_freebsd.go + // this implementation is common with BSDs + type loadavg struct { + load [3]uint32 + scale int + } + b, err := unix.SysctlRaw("vm.loadavg") + if err != nil { + return nil, err + } + load := *(*loadavg)(unsafe.Pointer((&b[0]))) + scale := float64(load.scale) + ret := &AvgStat{ + Load1: float64(load.load[0]) / scale, + Load5: float64(load.load[1]) / scale, + Load15: float64(load.load[2]) / scale, + } + + return ret, nil +} + +// Misc returnes miscellaneous host-wide statistics. +// darwin use ps command to get process running/blocked count. +// Almost same as FreeBSD implementation, but state is different. +// U means 'Uninterruptible Sleep'. +func Misc() (*MiscStat, error) { + return MiscWithContext(context.Background()) +} + +func MiscWithContext(ctx context.Context) (*MiscStat, error) { + bin, err := exec.LookPath("ps") + if err != nil { + return nil, err + } + out, err := invoke.CommandWithContext(ctx, bin, "axo", "state") + if err != nil { + return nil, err + } + lines := strings.Split(string(out), "\n") + + ret := MiscStat{} + for _, l := range lines { + if strings.Contains(l, "R") { + ret.ProcsRunning++ + } else if strings.Contains(l, "U") { + // uninterruptible sleep == blocked + ret.ProcsBlocked++ + } + } + + return &ret, nil +} diff --git a/internal/gopsutil/load/load_fallback.go b/internal/gopsutil/load/load_fallback.go new file mode 100644 index 0000000000..4642b446fc --- /dev/null +++ b/internal/gopsutil/load/load_fallback.go @@ -0,0 +1,25 @@ +// +build !darwin,!linux,!freebsd,!openbsd,!windows,!solaris + +package load + +import ( + "context" + + "github.com/shirou/gopsutil/internal/common" +) + +func Avg() (*AvgStat, error) { + return AvgWithContext(context.Background()) +} + +func AvgWithContext(ctx context.Context) (*AvgStat, error) { + return nil, common.ErrNotImplementedError +} + +func Misc() (*MiscStat, error) { + return MiscWithContext(context.Background()) +} + +func MiscWithContext(ctx context.Context) (*MiscStat, error) { + return nil, common.ErrNotImplementedError +} diff --git a/internal/gopsutil/load/load_freebsd.go b/internal/gopsutil/load/load_freebsd.go new file mode 100644 index 0000000000..bee8f554d7 --- /dev/null +++ b/internal/gopsutil/load/load_freebsd.go @@ -0,0 +1,7 @@ +// +build freebsd + +package load + +func getForkStat() (forkstat, error) { + return forkstat{}, nil +} diff --git a/internal/gopsutil/load/load_linux.go b/internal/gopsutil/load/load_linux.go new file mode 100644 index 0000000000..8cdd84243d --- /dev/null +++ b/internal/gopsutil/load/load_linux.go @@ -0,0 +1,136 @@ +//go:build linux +// +build linux + +package load + +import ( + "context" + "io/ioutil" + "strconv" + "strings" + "syscall" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +func Avg() (*AvgStat, error) { + return AvgWithContext(context.Background()) +} + +func AvgWithContext(ctx context.Context) (*AvgStat, error) { + stat, err := fileAvgWithContext(ctx) + if err != nil { + stat, err = sysinfoAvgWithContext(ctx) + } + return stat, err +} + +func sysinfoAvgWithContext(ctx context.Context) (*AvgStat, error) { + var info syscall.Sysinfo_t + err := syscall.Sysinfo(&info) + if err != nil { + return nil, err + } + + const si_load_shift = 16 + return &AvgStat{ + Load1: float64(info.Loads[0]) / float64(1<0.00 MB +
+
+
One Minute Load Avg
+

0.00

+
+
+ +
+
+
Response Time
@@ -175,20 +185,23 @@

0

const cpuMetric = document.querySelector('#cpuMetric'); const ramMetric = document.querySelector('#ramMetric'); + const loadavgMetric = document.querySelector('#loadavgMetric'); const rtimeMetric = document.querySelector('#rtimeMetric'); const connsMetric = document.querySelector('#connsMetric'); const cpuChartCtx = document.querySelector('#cpuChart').getContext('2d'); const ramChartCtx = document.querySelector('#ramChart').getContext('2d'); + const loadavgChartCtx = document.querySelector('#loadavgChart').getContext('2d'); const rtimeChartCtx = document.querySelector('#rtimeChart').getContext('2d'); const connsChartCtx = document.querySelector('#connsChart').getContext('2d'); const cpuChart = createChart(cpuChartCtx); const ramChart = createChart(ramChartCtx); + const loadavgChart = createChart(loadavgChartCtx); const rtimeChart = createChart(rtimeChartCtx); const connsChart = createChart(connsChartCtx); - const charts = [cpuChart, ramChart, rtimeChart, connsChart]; + const charts = [cpuChart, ramChart, loadavgChart, rtimeChart, connsChart]; function createChart(ctx) { return new Chart(ctx, { @@ -231,9 +244,11 @@

0

function update(json, rtime) { cpu = json.pid.cpu.toFixed(1); cpuOS = json.os.cpu.toFixed(1); + loadavg = json.os.load_avg.toFixed(1) cpuMetric.innerHTML = cpu + '% ' + cpuOS + '%'; ramMetric.innerHTML = formatBytes(json.pid.ram) + ' / ' + formatBytes(json.os.ram) + ' / ' + formatBytes(json.os.total_ram) + ''; + loadavgMetric.innerHTML = loadavg; rtimeMetric.innerHTML = rtime + 'ms client'; connsMetric.innerHTML = json.pid.conns + ' ' + json.os.conns + ''; @@ -241,6 +256,7 @@

0

ramChart.data.datasets[2].data.push((json.os.total_ram / 1e6).toFixed(2)); ramChart.data.datasets[1].data.push((json.os.ram / 1e6).toFixed(2)); ramChart.data.datasets[0].data.push((json.pid.ram / 1e6).toFixed(2)); + loadavgChart.data.datasets[0].data.push(loadavg); rtimeChart.data.datasets[0].data.push(rtime); connsChart.data.datasets[0].data.push(json.pid.conns); diff --git a/middleware/monitor/monitor.go b/middleware/monitor/monitor.go index 727ef4e95d..c687294bf9 100644 --- a/middleware/monitor/monitor.go +++ b/middleware/monitor/monitor.go @@ -8,6 +8,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/internal/gopsutil/cpu" + "github.com/gofiber/fiber/v2/internal/gopsutil/load" "github.com/gofiber/fiber/v2/internal/gopsutil/mem" "github.com/gofiber/fiber/v2/internal/gopsutil/net" "github.com/gofiber/fiber/v2/internal/gopsutil/process" @@ -27,6 +28,7 @@ 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"` } @@ -38,6 +40,7 @@ var ( monitOsCpu atomic.Value monitOsRam atomic.Value monitOsTotalRam atomic.Value + monitOsLoadAvg atomic.Value monitOsConns atomic.Value ) @@ -86,6 +89,7 @@ func New(config ...Config) fiber.Handler { 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) @@ -95,7 +99,6 @@ func New(config ...Config) fiber.Handler { } } - func updateStatistics(p *process.Process) { pidCpu, _ := p.CPUPercent() monitPidCpu.Store(pidCpu / 10) @@ -113,6 +116,10 @@ func updateStatistics(p *process.Process) { monitOsTotalRam.Store(osMem.Total) } + if loadAvg, _ := load.Avg(); loadAvg != nil { + monitOsLoadAvg.Store(loadAvg.Load1) + } + pidConns, _ := net.ConnectionsPid("tcp", p.Pid) monitPidConns.Store(len(pidConns))