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:
672
2026-05-17 01:29:44 +08:00
commit 0e8c9f7bff
49 changed files with 3291 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
package handler
import (
"net/http"
"pc-monitor-server/model"
"pc-monitor-server/service"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type AlertHandler struct {
service *service.AlertService
}
func NewAlertHandler(service *service.AlertService) *AlertHandler {
return &AlertHandler{service: service}
}
type CreateRuleRequest struct {
DeviceID string `json:"device_id" binding:"required"`
Metric string `json:"metric" binding:"required"`
Operator string `json:"operator" binding:"required"`
Threshold float64 `json:"threshold" binding:"required"`
Duration int `json:"duration"`
}
func (h *AlertHandler) CreateRule(c *gin.Context) {
var req CreateRuleRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
rule := &model.AlertRule{
ID: uuid.New().String(),
DeviceID: req.DeviceID,
Metric: req.Metric,
Operator: req.Operator,
Threshold: req.Threshold,
Duration: req.Duration,
CreatedAt: time.Now(),
}
if err := h.service.CreateRule(rule); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"rule": rule})
}
func (h *AlertHandler) GetRulesByDevice(c *gin.Context) {
deviceID := c.Param("id")
rules, err := h.service.GetRulesByDevice(deviceID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"rules": rules})
}
func (h *AlertHandler) DeleteRule(c *gin.Context) {
id := c.Param("id")
if err := h.service.DeleteRule(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Rule deleted"})
}
func (h *AlertHandler) GetActiveAlerts(c *gin.Context) {
alerts, err := h.service.GetActiveAlerts()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"alerts": alerts})
}
func (h *AlertHandler) ResolveAlert(c *gin.Context) {
id := c.Param("id")
if err := h.service.ResolveAlert(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Alert resolved"})
}

View File

@@ -0,0 +1,75 @@
package handler
import (
"net/http"
"pc-monitor-server/service"
"github.com/gin-gonic/gin"
)
type DeviceHandler struct {
service *service.DeviceService
}
func NewDeviceHandler(service *service.DeviceService) *DeviceHandler {
return &DeviceHandler{service: service}
}
type RegisterRequest struct {
Hostname string `json:"hostname" binding:"required"`
OS string `json:"os" binding:"required"`
IP string `json:"ip" binding:"required"`
}
func (h *DeviceHandler) Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
device, err := h.service.Register(req.Hostname, req.OS, req.IP)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"device": device})
}
func (h *DeviceHandler) GetAll(c *gin.Context) {
devices, err := h.service.GetAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"devices": devices})
}
func (h *DeviceHandler) GetByID(c *gin.Context) {
id := c.Param("id")
device, err := h.service.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Device not found"})
return
}
c.JSON(http.StatusOK, gin.H{"device": device})
}
func (h *DeviceHandler) Delete(c *gin.Context) {
id := c.Param("id")
if err := h.service.Delete(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Device deleted"})
}
func (h *DeviceHandler) Heartbeat(c *gin.Context) {
id := c.Param("id")
if err := h.service.Heartbeat(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "OK"})
}

View File

@@ -0,0 +1,80 @@
package handler
import (
"net/http"
"pc-monitor-server/model"
"pc-monitor-server/service"
"time"
"github.com/gin-gonic/gin"
)
type MetricsHandler struct {
service *service.MetricsService
}
func NewMetricsHandler(service *service.MetricsService) *MetricsHandler {
return &MetricsHandler{service: service}
}
type ReportRequest struct {
DeviceID string `json:"device_id" binding:"required"`
model.Metrics
}
func (h *MetricsHandler) Report(c *gin.Context) {
var req ReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
metrics := req.Metrics
metrics.DeviceID = req.DeviceID
metrics.Timestamp = time.Now()
if err := h.service.Save(&metrics); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Metrics saved"})
}
func (h *MetricsHandler) GetLatest(c *gin.Context) {
deviceID := c.Param("id")
metrics, err := h.service.GetLatest(deviceID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "No metrics found"})
return
}
c.JSON(http.StatusOK, gin.H{"metrics": metrics})
}
func (h *MetricsHandler) GetHistory(c *gin.Context) {
deviceID := c.Param("id")
startStr := c.Query("start")
endStr := c.Query("end")
start := time.Now().Add(-1 * time.Hour)
end := time.Now()
if startStr != "" {
if t, err := time.Parse(time.RFC3339, startStr); err == nil {
start = t
}
}
if endStr != "" {
if t, err := time.Parse(time.RFC3339, endStr); err == nil {
end = t
}
}
metrics, err := h.service.GetHistory(deviceID, start, end, 1000)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"metrics": metrics})
}

57
server/api/router.go Normal file
View File

@@ -0,0 +1,57 @@
package api
import (
"pc-monitor-server/api/handler"
"pc-monitor-server/service"
"github.com/gin-gonic/gin"
)
func SetupRouter(deviceService *service.DeviceService, metricsService *service.MetricsService, alertService *service.AlertService) *gin.Engine {
r := gin.Default()
r.Use(corsMiddleware())
api := r.Group("/api/v1")
{
deviceHandler := handler.NewDeviceHandler(deviceService)
metricsHandler := handler.NewMetricsHandler(metricsService)
alertHandler := handler.NewAlertHandler(alertService)
// Device endpoints
api.POST("/register", deviceHandler.Register)
api.GET("/devices", deviceHandler.GetAll)
api.GET("/devices/:id", deviceHandler.GetByID)
api.DELETE("/devices/:id", deviceHandler.Delete)
api.POST("/devices/:id/heartbeat", deviceHandler.Heartbeat)
// Metrics endpoints
api.POST("/report", metricsHandler.Report)
api.GET("/devices/:id/metrics/latest", metricsHandler.GetLatest)
api.GET("/devices/:id/metrics/history", metricsHandler.GetHistory)
// Alert endpoints
api.GET("/alerts", alertHandler.GetActiveAlerts)
api.POST("/alerts/rules", alertHandler.CreateRule)
api.GET("/devices/:id/alerts/rules", alertHandler.GetRulesByDevice)
api.DELETE("/alerts/rules/:id", alertHandler.DeleteRule)
api.POST("/alerts/:id/resolve", alertHandler.ResolveAlert)
}
return r
}
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}

9
server/config.yaml Normal file
View File

@@ -0,0 +1,9 @@
server:
addr: ":8080"
database:
path: "./data/monitor.db"
retention_days: 30
auth:
admin_password: "admin123"

51
server/config/config.go Normal file
View File

@@ -0,0 +1,51 @@
package config
import (
"os"
"time"
"gopkg.in/yaml.v3"
)
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
Auth AuthConfig `yaml:"auth"`
}
type ServerConfig struct {
Addr string `yaml:"addr"`
}
type DatabaseConfig struct {
Path string `yaml:"path"`
RetentionDays int `yaml:"retention_days"`
CleanupInterval time.Duration `yaml:"cleanup_interval"`
}
type AuthConfig struct {
AdminPassword string `yaml:"admin_password"`
}
func Load() *Config {
cfg := &Config{
Server: ServerConfig{
Addr: ":8080",
},
Database: DatabaseConfig{
Path: "./data/monitor.db",
RetentionDays: 30,
CleanupInterval: 24 * time.Hour,
},
Auth: AuthConfig{
AdminPassword: "admin123",
},
}
data, err := os.ReadFile("config.yaml")
if err == nil {
yaml.Unmarshal(data, cfg)
}
return cfg
}

9
server/go.mod Normal file
View File

@@ -0,0 +1,9 @@
module pc-monitor-server
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/mattn/go-sqlite3 v1.14.22
gopkg.in/yaml.v3 v3.0.1
)

52
server/main.go Normal file
View File

@@ -0,0 +1,52 @@
package main
import (
"embed"
"io/fs"
"log"
"net/http"
"pc-monitor-server/api"
"pc-monitor-server/config"
"pc-monitor-server/repository"
"pc-monitor-server/service"
"github.com/gin-gonic/gin"
)
//go:embed web/dist
var webFS embed.FS
func main() {
cfg := config.Load()
db, err := repository.NewDB(cfg.Database.Path)
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
defer db.Close()
deviceRepo := repository.NewDeviceRepository(db)
metricsRepo := repository.NewMetricsRepository(db)
alertRepo := repository.NewAlertRepository(db)
deviceService := service.NewDeviceService(deviceRepo)
metricsService := service.NewMetricsService(metricsRepo)
alertService := service.NewAlertService(alertRepo, metricsRepo)
router := api.SetupRouter(deviceService, metricsService, alertService)
// Serve embedded frontend
distFS, err := fs.Sub(webFS, "web/dist")
if err != nil {
log.Fatalf("Failed to load embedded frontend: %v", err)
}
router.StaticFS("/assets", http.FS(distFS))
router.NoRoute(func(c *gin.Context) {
c.FileFromFS("/", http.FS(distFS))
})
log.Printf("Server starting on %s", cfg.Server.Addr)
if err := router.Run(cfg.Server.Addr); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}

25
server/model/alert.go Normal file
View File

@@ -0,0 +1,25 @@
package model
import "time"
type AlertRule struct {
ID string `json:"id"`
DeviceID string `json:"device_id"`
Metric string `json:"metric"`
Operator string `json:"operator"`
Threshold float64 `json:"threshold"`
Duration int `json:"duration"`
CreatedAt time.Time `json:"created_at"`
}
type Alert struct {
ID string `json:"id"`
DeviceID string `json:"device_id"`
RuleID string `json:"rule_id"`
Metric string `json:"metric"`
Value float64 `json:"value"`
Message string `json:"message"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
}

13
server/model/device.go Normal file
View File

@@ -0,0 +1,13 @@
package model
import "time"
type Device struct {
ID string `json:"id"`
Hostname string `json:"hostname"`
OS string `json:"os"`
IP string `json:"ip"`
RegisteredAt time.Time `json:"registered_at"`
LastReportAt time.Time `json:"last_report_at"`
Status string `json:"status"`
}

54
server/model/metrics.go Normal file
View File

@@ -0,0 +1,54 @@
package model
import "time"
type Metrics struct {
DeviceID string `json:"device_id"`
Timestamp time.Time `json:"timestamp"`
// CPU
CPUUsage float64 `json:"cpu_usage"`
CPUTemperature float64 `json:"cpu_temperature"`
CPUCoreUsage []float64 `json:"cpu_core_usage"`
// Memory
MemoryTotal uint64 `json:"memory_total"`
MemoryUsed uint64 `json:"memory_used"`
MemoryUsage float64 `json:"memory_usage"`
// GPU
GPUUsage float64 `json:"gpu_usage"`
GPUTemperature float64 `json:"gpu_temperature"`
GPUMemoryTotal uint64 `json:"gpu_memory_total"`
GPUMemoryUsed uint64 `json:"gpu_memory_used"`
GPUName string `json:"gpu_name"`
// Network
NetworkInterfaces []NetInterface `json:"network_interfaces"`
// Disk
Disks []DiskInfo `json:"disks"`
// Power
PowerStatus string `json:"power_status"`
BatteryLevel int `json:"battery_level"`
PowerSource string `json:"power_source"`
}
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"`
}
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"`
}

View File

@@ -0,0 +1,88 @@
package repository
import (
"database/sql"
"pc-monitor-server/model"
"time"
)
type AlertRepository struct {
db *sql.DB
}
func NewAlertRepository(db *sql.DB) *AlertRepository {
return &AlertRepository{db: db}
}
func (r *AlertRepository) CreateRule(rule *model.AlertRule) error {
_, err := r.db.Exec(
`INSERT INTO alert_rules (id, device_id, metric, operator, threshold, duration, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
rule.ID, rule.DeviceID, rule.Metric, rule.Operator, rule.Threshold, rule.Duration, rule.CreatedAt,
)
return err
}
func (r *AlertRepository) GetRulesByDevice(deviceID string) ([]model.AlertRule, error) {
rows, err := r.db.Query(
`SELECT id, device_id, metric, operator, threshold, duration, created_at
FROM alert_rules WHERE device_id=?`, deviceID)
if err != nil {
return nil, err
}
defer rows.Close()
var rules []model.AlertRule
for rows.Next() {
var rule model.AlertRule
if err := rows.Scan(&rule.ID, &rule.DeviceID, &rule.Metric, &rule.Operator,
&rule.Threshold, &rule.Duration, &rule.CreatedAt); err != nil {
return nil, err
}
rules = append(rules, rule)
}
return rules, nil
}
func (r *AlertRepository) DeleteRule(id string) error {
_, err := r.db.Exec("DELETE FROM alert_rules WHERE id=?", id)
return err
}
func (r *AlertRepository) CreateAlert(alert *model.Alert) error {
_, err := r.db.Exec(
`INSERT INTO alerts (id, device_id, rule_id, metric, value, message, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
alert.ID, alert.DeviceID, alert.RuleID, alert.Metric,
alert.Value, alert.Message, alert.Status, alert.CreatedAt,
)
return err
}
func (r *AlertRepository) GetActiveAlerts() ([]model.Alert, error) {
rows, err := r.db.Query(
`SELECT id, device_id, rule_id, metric, value, message, status, created_at, resolved_at
FROM alerts WHERE status='active' ORDER BY created_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var alerts []model.Alert
for rows.Next() {
var a model.Alert
if err := rows.Scan(&a.ID, &a.DeviceID, &a.RuleID, &a.Metric,
&a.Value, &a.Message, &a.Status, &a.CreatedAt, &a.ResolvedAt); err != nil {
return nil, err
}
alerts = append(alerts, a)
}
return alerts, nil
}
func (r *AlertRepository) ResolveAlert(id string) error {
now := time.Now()
_, err := r.db.Exec(
`UPDATE alerts SET status='resolved', resolved_at=? WHERE id=?`, now, id)
return err
}

94
server/repository/db.go Normal file
View File

@@ -0,0 +1,94 @@
package repository
import (
"database/sql"
"os"
"path/filepath"
_ "github.com/mattn/go-sqlite3"
)
func NewDB(dbPath string) (*sql.DB, error) {
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, err
}
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL")
if err != nil {
return nil, err
}
if err := initSchema(db); err != nil {
db.Close()
return nil, err
}
return db, nil
}
func initSchema(db *sql.DB) error {
schema := `
CREATE TABLE IF NOT EXISTS devices (
id TEXT PRIMARY KEY,
hostname TEXT NOT NULL,
os TEXT NOT NULL,
ip TEXT NOT NULL,
registered_at DATETIME NOT NULL,
last_report_at DATETIME NOT NULL,
status TEXT NOT NULL DEFAULT 'offline'
);
CREATE TABLE IF NOT EXISTS metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
timestamp DATETIME NOT NULL,
cpu_usage REAL,
cpu_temperature REAL,
cpu_core_usage TEXT,
memory_total INTEGER,
memory_used INTEGER,
memory_usage REAL,
gpu_usage REAL,
gpu_temperature REAL,
gpu_memory_total INTEGER,
gpu_memory_used INTEGER,
gpu_name TEXT,
network_interfaces TEXT,
disks TEXT,
power_status TEXT,
battery_level INTEGER,
power_source TEXT,
FOREIGN KEY (device_id) REFERENCES devices(id)
);
CREATE INDEX IF NOT EXISTS idx_metrics_device_time ON metrics(device_id, timestamp);
CREATE TABLE IF NOT EXISTS alert_rules (
id TEXT PRIMARY KEY,
device_id TEXT NOT NULL,
metric TEXT NOT NULL,
operator TEXT NOT NULL,
threshold REAL NOT NULL,
duration INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
FOREIGN KEY (device_id) REFERENCES devices(id)
);
CREATE TABLE IF NOT EXISTS alerts (
id TEXT PRIMARY KEY,
device_id TEXT NOT NULL,
rule_id TEXT NOT NULL,
metric TEXT NOT NULL,
value REAL NOT NULL,
message TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL,
resolved_at DATETIME,
FOREIGN KEY (device_id) REFERENCES devices(id),
FOREIGN KEY (rule_id) REFERENCES alert_rules(id)
);
`
_, err := db.Exec(schema)
return err
}

View File

@@ -0,0 +1,87 @@
package repository
import (
"database/sql"
"pc-monitor-server/model"
"time"
)
type DeviceRepository struct {
db *sql.DB
}
func NewDeviceRepository(db *sql.DB) *DeviceRepository {
return &DeviceRepository{db: db}
}
func (r *DeviceRepository) Create(device *model.Device) error {
_, err := r.db.Exec(
`INSERT INTO devices (id, hostname, os, ip, registered_at, last_report_at, status)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
device.ID, device.Hostname, device.OS, device.IP,
device.RegisteredAt, device.LastReportAt, device.Status,
)
return err
}
func (r *DeviceRepository) Update(device *model.Device) error {
_, err := r.db.Exec(
`UPDATE devices SET hostname=?, os=?, ip=?, last_report_at=?, status=?
WHERE id=?`,
device.Hostname, device.OS, device.IP,
device.LastReportAt, device.Status, device.ID,
)
return err
}
func (r *DeviceRepository) GetByID(id string) (*model.Device, error) {
device := &model.Device{}
err := r.db.QueryRow(
`SELECT id, hostname, os, ip, registered_at, last_report_at, status
FROM devices WHERE id=?`, id,
).Scan(&device.ID, &device.Hostname, &device.OS, &device.IP,
&device.RegisteredAt, &device.LastReportAt, &device.Status)
if err != nil {
return nil, err
}
return device, nil
}
func (r *DeviceRepository) GetAll() ([]model.Device, error) {
rows, err := r.db.Query(
`SELECT id, hostname, os, ip, registered_at, last_report_at, status
FROM devices ORDER BY last_report_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
var devices []model.Device
for rows.Next() {
var d model.Device
if err := rows.Scan(&d.ID, &d.Hostname, &d.OS, &d.IP,
&d.RegisteredAt, &d.LastReportAt, &d.Status); err != nil {
return nil, err
}
devices = append(devices, d)
}
return devices, nil
}
func (r *DeviceRepository) Delete(id string) error {
_, err := r.db.Exec("DELETE FROM devices WHERE id=?", id)
return err
}
func (r *DeviceRepository) UpdateStatus(id string, status string) error {
_, err := r.db.Exec("UPDATE devices SET status=? WHERE id=?", status, id)
return err
}
func (r *DeviceRepository) MarkOffline(threshold time.Duration) error {
cutoff := time.Now().Add(-threshold)
_, err := r.db.Exec(
`UPDATE devices SET status='offline'
WHERE last_report_at < ? AND status='online'`, cutoff)
return err
}

View File

@@ -0,0 +1,106 @@
package repository
import (
"database/sql"
"encoding/json"
"pc-monitor-server/model"
"time"
)
type MetricsRepository struct {
db *sql.DB
}
func NewMetricsRepository(db *sql.DB) *MetricsRepository {
return &MetricsRepository{db: db}
}
func (r *MetricsRepository) Save(m *model.Metrics) error {
cpuCoreJSON, _ := json.Marshal(m.CPUCoreUsage)
netJSON, _ := json.Marshal(m.NetworkInterfaces)
diskJSON, _ := json.Marshal(m.Disks)
_, err := r.db.Exec(
`INSERT INTO metrics (
device_id, timestamp, cpu_usage, cpu_temperature, cpu_core_usage,
memory_total, memory_used, memory_usage,
gpu_usage, gpu_temperature, gpu_memory_total, gpu_memory_used, gpu_name,
network_interfaces, disks,
power_status, battery_level, power_source
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
m.DeviceID, m.Timestamp,
m.CPUUsage, m.CPUTemperature, string(cpuCoreJSON),
m.MemoryTotal, m.MemoryUsed, m.MemoryUsage,
m.GPUUsage, m.GPUTemperature, m.GPUMemoryTotal, m.GPUMemoryUsed, m.GPUName,
string(netJSON), string(diskJSON),
m.PowerStatus, m.BatteryLevel, m.PowerSource,
)
return err
}
func (r *MetricsRepository) GetLatest(deviceID string) (*model.Metrics, error) {
m := &model.Metrics{}
var cpuCoreJSON, netJSON, diskJSON string
err := r.db.QueryRow(
`SELECT device_id, timestamp, cpu_usage, cpu_temperature, cpu_core_usage,
memory_total, memory_used, memory_usage,
gpu_usage, gpu_temperature, gpu_memory_total, gpu_memory_used, gpu_name,
network_interfaces, disks,
power_status, battery_level, power_source
FROM metrics WHERE device_id=? ORDER BY timestamp DESC LIMIT 1`, deviceID,
).Scan(&m.DeviceID, &m.Timestamp,
&m.CPUUsage, &m.CPUTemperature, &cpuCoreJSON,
&m.MemoryTotal, &m.MemoryUsed, &m.MemoryUsage,
&m.GPUUsage, &m.GPUTemperature, &m.GPUMemoryTotal, &m.GPUMemoryUsed, &m.GPUName,
&netJSON, &diskJSON,
&m.PowerStatus, &m.BatteryLevel, &m.PowerSource)
if err != nil {
return nil, err
}
json.Unmarshal([]byte(cpuCoreJSON), &m.CPUCoreUsage)
json.Unmarshal([]byte(netJSON), &m.NetworkInterfaces)
json.Unmarshal([]byte(diskJSON), &m.Disks)
return m, nil
}
func (r *MetricsRepository) GetHistory(deviceID string, start, end time.Time, limit int) ([]model.Metrics, error) {
rows, err := r.db.Query(
`SELECT device_id, timestamp, cpu_usage, cpu_temperature, cpu_core_usage,
memory_total, memory_used, memory_usage,
gpu_usage, gpu_temperature, gpu_memory_total, gpu_memory_used, gpu_name,
network_interfaces, disks,
power_status, battery_level, power_source
FROM metrics WHERE device_id=? AND timestamp BETWEEN ? AND ?
ORDER BY timestamp DESC LIMIT ?`, deviceID, start, end, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var metrics []model.Metrics
for rows.Next() {
var m model.Metrics
var cpuCoreJSON, netJSON, diskJSON string
if err := rows.Scan(&m.DeviceID, &m.Timestamp,
&m.CPUUsage, &m.CPUTemperature, &cpuCoreJSON,
&m.MemoryTotal, &m.MemoryUsed, &m.MemoryUsage,
&m.GPUUsage, &m.GPUTemperature, &m.GPUMemoryTotal, &m.GPUMemoryUsed, &m.GPUName,
&netJSON, &diskJSON,
&m.PowerStatus, &m.BatteryLevel, &m.PowerSource); err != nil {
return nil, err
}
json.Unmarshal([]byte(cpuCoreJSON), &m.CPUCoreUsage)
json.Unmarshal([]byte(netJSON), &m.NetworkInterfaces)
json.Unmarshal([]byte(diskJSON), &m.Disks)
metrics = append(metrics, m)
}
return metrics, nil
}
func (r *MetricsRepository) Cleanup(before time.Time) error {
_, err := r.db.Exec("DELETE FROM metrics WHERE timestamp < ?", before)
return err
}

99
server/service/alert.go Normal file
View File

@@ -0,0 +1,99 @@
package service
import (
"fmt"
"pc-monitor-server/model"
"pc-monitor-server/repository"
"time"
)
type AlertService struct {
alertRepo *repository.AlertRepository
metricsRepo *repository.MetricsRepository
}
func NewAlertService(alertRepo *repository.AlertRepository, metricsRepo *repository.MetricsRepository) *AlertService {
return &AlertService{alertRepo: alertRepo, metricsRepo: metricsRepo}
}
func (s *AlertService) CreateRule(rule *model.AlertRule) error {
rule.CreatedAt = time.Now()
return s.alertRepo.CreateRule(rule)
}
func (s *AlertService) GetRulesByDevice(deviceID string) ([]model.AlertRule, error) {
return s.alertRepo.GetRulesByDevice(deviceID)
}
func (s *AlertService) DeleteRule(id string) error {
return s.alertRepo.DeleteRule(id)
}
func (s *AlertService) GetActiveAlerts() ([]model.Alert, error) {
return s.alertRepo.GetActiveAlerts()
}
func (s *AlertService) ResolveAlert(id string) error {
return s.alertRepo.ResolveAlert(id)
}
func (s *AlertService) CheckAlerts(deviceID string, metrics *model.Metrics) error {
rules, err := s.alertRepo.GetRulesByDevice(deviceID)
if err != nil {
return err
}
for _, rule := range rules {
value := getMetricValue(metrics, rule.Metric)
if checkThreshold(value, rule.Operator, rule.Threshold) {
alert := &model.Alert{
ID: fmt.Sprintf("%s-%s-%d", deviceID, rule.ID, time.Now().Unix()),
DeviceID: deviceID,
RuleID: rule.ID,
Metric: rule.Metric,
Value: value,
Message: fmt.Sprintf("%s %s %.2f (threshold: %.2f)", rule.Metric, rule.Operator, value, rule.Threshold),
Status: "active",
CreatedAt: time.Now(),
}
s.alertRepo.CreateAlert(alert)
}
}
return nil
}
func getMetricValue(m *model.Metrics, metric string) float64 {
switch metric {
case "cpu_usage":
return m.CPUUsage
case "cpu_temperature":
return m.CPUTemperature
case "memory_usage":
return m.MemoryUsage
case "gpu_usage":
return m.GPUUsage
case "gpu_temperature":
return m.GPUTemperature
case "battery_level":
return float64(m.BatteryLevel)
default:
return 0
}
}
func checkThreshold(value float64, operator string, threshold float64) bool {
switch operator {
case ">":
return value > threshold
case ">=":
return value >= threshold
case "<":
return value < threshold
case "<=":
return value <= threshold
case "==":
return value == threshold
default:
return false
}
}

73
server/service/device.go Normal file
View File

@@ -0,0 +1,73 @@
package service
import (
"crypto/sha256"
"fmt"
"pc-monitor-server/model"
"pc-monitor-server/repository"
"time"
)
type DeviceService struct {
repo *repository.DeviceRepository
}
func NewDeviceService(repo *repository.DeviceRepository) *DeviceService {
return &DeviceService{repo: repo}
}
func (s *DeviceService) Register(hostname, osName, ip string) (*model.Device, error) {
id := generateDeviceID(hostname, ip)
device, err := s.repo.GetByID(id)
if err == nil {
device.LastReportAt = time.Now()
device.Status = "online"
s.repo.Update(device)
return device, nil
}
device = &model.Device{
ID: id,
Hostname: hostname,
OS: osName,
IP: ip,
RegisteredAt: time.Now(),
LastReportAt: time.Now(),
Status: "online",
}
if err := s.repo.Create(device); err != nil {
return nil, err
}
return device, nil
}
func (s *DeviceService) GetAll() ([]model.Device, error) {
return s.repo.GetAll()
}
func (s *DeviceService) GetByID(id string) (*model.Device, error) {
return s.repo.GetByID(id)
}
func (s *DeviceService) Delete(id string) error {
return s.repo.Delete(id)
}
func (s *DeviceService) Heartbeat(id string) error {
device, err := s.repo.GetByID(id)
if err != nil {
return err
}
device.LastReportAt = time.Now()
device.Status = "online"
return s.repo.Update(device)
}
func (s *DeviceService) CheckOffline() {
s.repo.MarkOffline(2 * time.Minute)
}
func generateDeviceID(hostname, ip string) string {
hash := sha256.Sum256([]byte(fmt.Sprintf("%s-%s", hostname, ip)))
return fmt.Sprintf("%x", hash[:8])
}

35
server/service/metrics.go Normal file
View File

@@ -0,0 +1,35 @@
package service
import (
"pc-monitor-server/model"
"pc-monitor-server/repository"
"time"
)
type MetricsService struct {
repo *repository.MetricsRepository
}
func NewMetricsService(repo *repository.MetricsRepository) *MetricsService {
return &MetricsService{repo: repo}
}
func (s *MetricsService) Save(m *model.Metrics) error {
return s.repo.Save(m)
}
func (s *MetricsService) GetLatest(deviceID string) (*model.Metrics, error) {
return s.repo.GetLatest(deviceID)
}
func (s *MetricsService) GetHistory(deviceID string, start, end time.Time, limit int) ([]model.Metrics, error) {
if limit <= 0 {
limit = 1000
}
return s.repo.GetHistory(deviceID, start, end, limit)
}
func (s *MetricsService) Cleanup(retentionDays int) error {
before := time.Now().AddDate(0, 0, -retentionDays)
return s.repo.Cleanup(before)
}