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:
89
server/api/handler/alert.go
Normal file
89
server/api/handler/alert.go
Normal 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"})
|
||||
}
|
||||
75
server/api/handler/device.go
Normal file
75
server/api/handler/device.go
Normal 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"})
|
||||
}
|
||||
80
server/api/handler/metrics.go
Normal file
80
server/api/handler/metrics.go
Normal 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
57
server/api/router.go
Normal 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
9
server/config.yaml
Normal 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
51
server/config/config.go
Normal 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
9
server/go.mod
Normal 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
52
server/main.go
Normal 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
25
server/model/alert.go
Normal 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
13
server/model/device.go
Normal 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
54
server/model/metrics.go
Normal 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"`
|
||||
}
|
||||
88
server/repository/alert.go
Normal file
88
server/repository/alert.go
Normal 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
94
server/repository/db.go
Normal 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
|
||||
}
|
||||
87
server/repository/device.go
Normal file
87
server/repository/device.go
Normal 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
|
||||
}
|
||||
106
server/repository/metrics.go
Normal file
106
server/repository/metrics.go
Normal 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
99
server/service/alert.go
Normal 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
73
server/service/device.go
Normal 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
35
server/service/metrics.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user