feat: init pc-monitor project
- Client: Go-based Windows hardware monitoring (CPU, GPU, memory, disk, network, power) - Server: Go + Gin + SQLite backend with REST API - Frontend: Vue 3 + Element Plus dashboard - Docker deployment support - Windows service installation script
This commit is contained in:
61
client/collector/collector.go
Normal file
61
client/collector/collector.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
||||
CPU *CPUInfo `json:"cpu"`
|
||||
Memory *MemoryInfo `json:"memory"`
|
||||
GPU *GPUInfo `json:"gpu"`
|
||||
Disks []DiskInfo `json:"disks"`
|
||||
Net []NetInterface `json:"network"`
|
||||
Power *PowerInfo `json:"power"`
|
||||
}
|
||||
|
||||
func CollectAll() (*Metrics, error) {
|
||||
metrics := &Metrics{
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// Collect CPU
|
||||
cpuInfo, err := CollectCPU()
|
||||
if err == nil {
|
||||
metrics.CPU = cpuInfo
|
||||
}
|
||||
|
||||
// Collect Memory
|
||||
memInfo, err := CollectMemory()
|
||||
if err == nil {
|
||||
metrics.Memory = memInfo
|
||||
}
|
||||
|
||||
// Collect GPU
|
||||
gpuInfo, err := CollectGPU()
|
||||
if err == nil {
|
||||
metrics.GPU = gpuInfo
|
||||
}
|
||||
|
||||
// Collect Disks
|
||||
disks, err := CollectDisks()
|
||||
if err == nil {
|
||||
metrics.Disks = disks
|
||||
}
|
||||
|
||||
// Collect Network
|
||||
nets, err := CollectNetwork()
|
||||
if err == nil {
|
||||
metrics.Net = nets
|
||||
}
|
||||
|
||||
// Collect Power
|
||||
powerInfo, err := CollectPower()
|
||||
if err == nil {
|
||||
metrics.Power = powerInfo
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
41
client/collector/cpu.go
Normal file
41
client/collector/cpu.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
)
|
||||
|
||||
type CPUInfo struct {
|
||||
Usage float64 `json:"usage"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
CoreUsage []float64 `json:"core_usage"`
|
||||
}
|
||||
|
||||
func CollectCPU() (*CPUInfo, error) {
|
||||
info := &CPUInfo{}
|
||||
|
||||
// Get overall CPU usage
|
||||
percent, err := cpu.Percent(0, false)
|
||||
if err == nil && len(percent) > 0 {
|
||||
info.Usage = percent[0]
|
||||
}
|
||||
|
||||
// Get per-core CPU usage
|
||||
perCPU, err := cpu.Percent(0, true)
|
||||
if err == nil {
|
||||
info.CoreUsage = perCPU
|
||||
}
|
||||
|
||||
// Get CPU temperature
|
||||
temps, err := host.SensorsTemperatures()
|
||||
if err == nil {
|
||||
for _, temp := range temps {
|
||||
if temp.Temperature > 0 {
|
||||
info.Temperature = temp.Temperature
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
38
client/collector/disk.go
Normal file
38
client/collector/disk.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
)
|
||||
|
||||
type DiskInfo struct {
|
||||
MountPoint string `json:"mount_point"`
|
||||
Total uint64 `json:"total"`
|
||||
Used uint64 `json:"used"`
|
||||
Usage float64 `json:"usage"`
|
||||
FileSystem string `json:"file_system"`
|
||||
}
|
||||
|
||||
func CollectDisks() ([]DiskInfo, error) {
|
||||
partitions, err := disk.Partitions(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var disks []DiskInfo
|
||||
for _, p := range partitions {
|
||||
usage, err := disk.Usage(p.Mountpoint)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
disks = append(disks, DiskInfo{
|
||||
MountPoint: p.Mountpoint,
|
||||
Total: usage.Total,
|
||||
Used: usage.Used,
|
||||
Usage: usage.UsedPercent,
|
||||
FileSystem: p.Fstype,
|
||||
})
|
||||
}
|
||||
|
||||
return disks, nil
|
||||
}
|
||||
80
client/collector/gpu.go
Normal file
80
client/collector/gpu.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GPUInfo struct {
|
||||
Usage float64 `json:"usage"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
MemoryTotal uint64 `json:"memory_total"`
|
||||
MemoryUsed uint64 `json:"memory_used"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type nvidiaSMIOutput struct {
|
||||
GPUs []struct {
|
||||
Name string `json:"name"`
|
||||
Utilization struct {
|
||||
GPU string `json:"gpu"`
|
||||
} `json:"utilization"`
|
||||
Temperature struct {
|
||||
GPU string `json:"gpu_temp"`
|
||||
} `json:"temperature"`
|
||||
Memory struct {
|
||||
Total string `json:"total"`
|
||||
Used string `json:"used"`
|
||||
} `json:"fb_memory_usage"`
|
||||
} `json:"gpus"`
|
||||
}
|
||||
|
||||
func CollectGPU() (*GPUInfo, error) {
|
||||
// Try NVIDIA GPU first
|
||||
info, err := collectNvidiaGPU()
|
||||
if err == nil && info != nil {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Return empty GPU info if no GPU detected
|
||||
return &GPUInfo{
|
||||
Name: "No GPU detected",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func collectNvidiaGPU() (*GPUInfo, error) {
|
||||
cmd := exec.Command("nvidia-smi", "--query-gpu=name,utilization.gpu,temperature.gpu,memory.total,memory.used", "--format=csv,noheader,nounits")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
if len(lines) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fields := strings.Split(lines[0], ",")
|
||||
if len(fields) < 5 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
info := &GPUInfo{
|
||||
Name: strings.TrimSpace(fields[0]),
|
||||
}
|
||||
|
||||
// Parse values (ignore errors, use defaults)
|
||||
var usage, temp, memTotal, memUsed float64
|
||||
json.Unmarshal([]byte(strings.TrimSpace(fields[1])), &usage)
|
||||
json.Unmarshal([]byte(strings.TrimSpace(fields[2])), &temp)
|
||||
json.Unmarshal([]byte(strings.TrimSpace(fields[3])), &memTotal)
|
||||
json.Unmarshal([]byte(strings.TrimSpace(fields[4])), &memUsed)
|
||||
|
||||
info.Usage = usage
|
||||
info.Temperature = temp
|
||||
info.MemoryTotal = uint64(memTotal) * 1024 * 1024 // Convert MB to bytes
|
||||
info.MemoryUsed = uint64(memUsed) * 1024 * 1024
|
||||
|
||||
return info, nil
|
||||
}
|
||||
24
client/collector/memory.go
Normal file
24
client/collector/memory.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
)
|
||||
|
||||
type MemoryInfo struct {
|
||||
Total uint64 `json:"total"`
|
||||
Used uint64 `json:"used"`
|
||||
Usage float64 `json:"usage"`
|
||||
}
|
||||
|
||||
func CollectMemory() (*MemoryInfo, error) {
|
||||
vmStat, err := mem.VirtualMemory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MemoryInfo{
|
||||
Total: vmStat.Total,
|
||||
Used: vmStat.Used,
|
||||
Usage: vmStat.UsedPercent,
|
||||
}, nil
|
||||
}
|
||||
68
client/collector/network.go
Normal file
68
client/collector/network.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
)
|
||||
|
||||
type NetInterface struct {
|
||||
Name string `json:"name"`
|
||||
MACAddress string `json:"mac_address"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
BytesSent uint64 `json:"bytes_sent"`
|
||||
BytesRecv uint64 `json:"bytes_recv"`
|
||||
Speed uint64 `json:"speed"`
|
||||
IsUp bool `json:"is_up"`
|
||||
}
|
||||
|
||||
func CollectNetwork() ([]NetInterface, error) {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counters, err := net.IOCounters(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counterMap := make(map[string]net.IOCountersStat)
|
||||
for _, c := range counters {
|
||||
counterMap[c.Name] = c
|
||||
}
|
||||
|
||||
var nets []NetInterface
|
||||
for _, iface := range interfaces {
|
||||
if iface.Name == "lo" || iface.Name == "Loopback Pseudo-Interface 1" {
|
||||
continue
|
||||
}
|
||||
|
||||
ni := NetInterface{
|
||||
Name: iface.Name,
|
||||
MACAddress: iface.HardwareAddr,
|
||||
IsUp: false,
|
||||
}
|
||||
|
||||
for _, addr := range iface.Addrs {
|
||||
if addr.Addr != "" && addr.Addr != "0.0.0.0" {
|
||||
ni.IPAddress = addr.Addr
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, flag := range iface.Flags {
|
||||
if flag == "up" {
|
||||
ni.IsUp = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if c, ok := counterMap[iface.Name]; ok {
|
||||
ni.BytesSent = c.BytesSent
|
||||
ni.BytesRecv = c.BytesRecv
|
||||
}
|
||||
|
||||
nets = append(nets, ni)
|
||||
}
|
||||
|
||||
return nets, nil
|
||||
}
|
||||
47
client/collector/power.go
Normal file
47
client/collector/power.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
)
|
||||
|
||||
type PowerInfo struct {
|
||||
Status string `json:"status"`
|
||||
BatteryLevel int `json:"battery_level"`
|
||||
PowerSource string `json:"power_source"`
|
||||
}
|
||||
|
||||
func CollectPower() (*PowerInfo, error) {
|
||||
info := &PowerInfo{
|
||||
Status: "no_battery",
|
||||
BatteryLevel: 0,
|
||||
PowerSource: "ac",
|
||||
}
|
||||
|
||||
// Try to get battery info
|
||||
battery, err := host.SensorsBattery()
|
||||
if err != nil {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
if battery == nil {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
info.BatteryLevel = int(battery.Percent)
|
||||
|
||||
if battery.PowerSupply {
|
||||
info.PowerSource = "ac"
|
||||
} else {
|
||||
info.PowerSource = "battery"
|
||||
}
|
||||
|
||||
if battery.Charging {
|
||||
info.Status = "charging"
|
||||
} else if battery.Percent >= 100 {
|
||||
info.Status = "full"
|
||||
} else {
|
||||
info.Status = "discharging"
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
Reference in New Issue
Block a user