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:
14
client/build.bat
Normal file
14
client/build.bat
Normal file
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
echo Building PC Monitor Client for Windows...
|
||||
|
||||
set GOOS=windows
|
||||
set GOARCH=amd64
|
||||
|
||||
go build -o pc-monitor-client.exe -ldflags="-s -w" .
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo Build successful: pc-monitor-client.exe
|
||||
) else (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
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
|
||||
}
|
||||
65
client/config.go
Normal file
65
client/config.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Collect CollectConfig `yaml:"collect"`
|
||||
Report ReportConfig `yaml:"report"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
URL string `yaml:"url"`
|
||||
Token string `yaml:"token"`
|
||||
}
|
||||
|
||||
type CollectConfig struct {
|
||||
Interval time.Duration `yaml:"interval"`
|
||||
}
|
||||
|
||||
type ReportConfig struct {
|
||||
Interval time.Duration `yaml:"interval"`
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{
|
||||
URL: "http://localhost:8080",
|
||||
},
|
||||
Collect: CollectConfig{
|
||||
Interval: 30 * time.Second,
|
||||
},
|
||||
Report: ReportConfig{
|
||||
Interval: 60 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
data, _ = yaml.Marshal(cfg)
|
||||
os.WriteFile(path, data, 0644)
|
||||
return cfg, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func SaveConfig(path string, cfg *Config) error {
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
8
client/go.mod
Normal file
8
client/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module pc-monitor-client
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
87
client/install/install.ps1
Normal file
87
client/install/install.ps1
Normal file
@@ -0,0 +1,87 @@
|
||||
# PC Monitor Client Installation Script
|
||||
# Run as Administrator
|
||||
|
||||
param(
|
||||
[string]$ServerUrl = "http://your-server:8080",
|
||||
[string]$InstallDir = "C:\Program Files\PCMonitor"
|
||||
)
|
||||
|
||||
# Check if running as Administrator
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Error "Please run this script as Administrator"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Installing PC Monitor Client..." -ForegroundColor Green
|
||||
|
||||
# Create installation directory
|
||||
if (-not (Test-Path $InstallDir)) {
|
||||
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Copy executable
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$exePath = Join-Path $scriptDir "pc-monitor-client.exe"
|
||||
|
||||
if (-not (Test-Path $exePath)) {
|
||||
Write-Error "pc-monitor-client.exe not found in script directory"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Copy-Item $exePath -Destination $InstallDir -Force
|
||||
|
||||
# Create config file
|
||||
$configPath = Join-Path $InstallDir "config.yaml"
|
||||
@"
|
||||
server:
|
||||
url: "$ServerUrl"
|
||||
token: ""
|
||||
collect:
|
||||
interval: 30s
|
||||
report:
|
||||
interval: 60s
|
||||
"@ | Out-File -FilePath $configPath -Encoding UTF8
|
||||
|
||||
# Create Windows Service using NSSM
|
||||
$nssmPath = Join-Path $InstallDir "nssm.exe"
|
||||
if (-not (Test-Path $nssmPath)) {
|
||||
Write-Host "Downloading NSSM..." -ForegroundColor Yellow
|
||||
$nssmUrl = "https://nssm.cc/release/nssm-2.24.zip"
|
||||
$zipPath = Join-Path $InstallDir "nssm.zip"
|
||||
Invoke-WebRequest -Uri $nssmUrl -OutFile $zipPath
|
||||
Expand-Archive -Path $zipPath -DestinationPath $InstallDir -Force
|
||||
$nssmExe = Get-ChildItem -Path $InstallDir -Recurse -Filter "nssm.exe" | Where-Object { $_.FullName -match "win64" } | Select-Object -First 1
|
||||
if ($nssmExe) {
|
||||
Copy-Item $nssmExe.FullName -Destination $nssmPath -Force
|
||||
}
|
||||
Remove-Item $zipPath -Force
|
||||
}
|
||||
|
||||
# Install service
|
||||
$serviceName = "PCMonitor"
|
||||
$clientExe = Join-Path $InstallDir "pc-monitor-client.exe"
|
||||
|
||||
& $nssmPath install $serviceName $clientExe
|
||||
& $nssmPath set $serviceName AppDirectory $InstallDir
|
||||
& $nssmPath set $serviceName DisplayName "PC Monitor Client"
|
||||
& $nssmPath set $serviceName Description "Monitors PC hardware metrics and reports to server"
|
||||
& $nssmPath set $serviceName Start SERVICE_AUTO_START
|
||||
& $nssmPath set $serviceName AppStdout (Join-Path $InstallDir "stdout.log")
|
||||
& $nssmPath set $serviceName AppStderr (Join-Path $InstallDir "stderr.log")
|
||||
|
||||
# Start service
|
||||
Start-Service $serviceName
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Installation complete!" -ForegroundColor Green
|
||||
Write-Host "Service '$serviceName' installed and started." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Configuration file: $configPath" -ForegroundColor Cyan
|
||||
Write-Host "Please edit the config file to set your server URL and token." -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Useful commands:" -ForegroundColor Yellow
|
||||
Write-Host " Start service: Start-Service $serviceName"
|
||||
Write-Host " Stop service: Stop-Service $serviceName"
|
||||
Write-Host " Service status: Get-Service $serviceName"
|
||||
Write-Host " View logs: Get-Content (Join-Path $InstallDir 'stdout.log') -Tail 50"
|
||||
123
client/main.go
Normal file
123
client/main.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"pc-monitor-client/collector"
|
||||
"pc-monitor-client/reporter"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.Println("PC Monitor Client starting...")
|
||||
|
||||
// Load config
|
||||
cfg, err := LoadConfig("config.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Get hostname and IP
|
||||
hostname, _ := os.Hostname()
|
||||
ip := getLocalIP()
|
||||
|
||||
// Create reporter
|
||||
r := reporter.NewReporter(cfg.Server.URL, cfg.Server.Token)
|
||||
|
||||
// Register device
|
||||
log.Printf("Registering device: %s (%s)", hostname, ip)
|
||||
deviceID, err := r.Register(hostname, runtime.GOOS, ip)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to register: %v", err)
|
||||
log.Println("Will retry on next report...")
|
||||
} else {
|
||||
log.Printf("Device registered with ID: %s", deviceID)
|
||||
}
|
||||
|
||||
// Create ticker for collection
|
||||
collectTicker := time.NewTicker(cfg.Collect.Interval)
|
||||
defer collectTicker.Stop()
|
||||
|
||||
reportTicker := time.NewTicker(cfg.Report.Interval)
|
||||
defer reportTicker.Stop()
|
||||
|
||||
// Create channel for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// Last collected metrics
|
||||
var lastMetrics *collector.Metrics
|
||||
|
||||
log.Printf("Collecting metrics every %v", cfg.Collect.Interval)
|
||||
log.Printf("Reporting metrics every %v", cfg.Report.Interval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-collectTicker.C:
|
||||
metrics, err := collector.CollectAll()
|
||||
if err != nil {
|
||||
log.Printf("Error collecting metrics: %v", err)
|
||||
continue
|
||||
}
|
||||
metrics.DeviceID = deviceID
|
||||
lastMetrics = metrics
|
||||
log.Printf("Metrics collected: CPU=%.1f%%, Memory=%.1f%%",
|
||||
metrics.CPU.Usage, metrics.Memory.Usage)
|
||||
|
||||
case <-reportTicker.C:
|
||||
if lastMetrics == nil {
|
||||
log.Println("No metrics to report yet")
|
||||
continue
|
||||
}
|
||||
|
||||
if deviceID == "" {
|
||||
// Try to register again
|
||||
deviceID, err = r.Register(hostname, runtime.GOOS, ip)
|
||||
if err != nil {
|
||||
log.Printf("Failed to register: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Printf("Device registered with ID: %s", deviceID)
|
||||
}
|
||||
|
||||
if err := r.Report(lastMetrics); err != nil {
|
||||
log.Printf("Error reporting metrics: %v", err)
|
||||
} else {
|
||||
log.Println("Metrics reported successfully")
|
||||
}
|
||||
|
||||
// Send heartbeat
|
||||
if err := r.Heartbeat(deviceID); err != nil {
|
||||
log.Printf("Error sending heartbeat: %v", err)
|
||||
}
|
||||
|
||||
case <-sigChan:
|
||||
log.Println("Shutting down...")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||
if ipNet.IP.To4() != nil {
|
||||
return ipNet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
119
client/reporter/http.go
Normal file
119
client/reporter/http.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package reporter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Reporter struct {
|
||||
serverURL string
|
||||
token string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewReporter(serverURL, token string) *Reporter {
|
||||
return &Reporter{
|
||||
serverURL: serverURL,
|
||||
token: token,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Hostname string `json:"hostname"`
|
||||
OS string `json:"os"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
type RegisterResponse struct {
|
||||
Device struct {
|
||||
ID string `json:"id"`
|
||||
Token string `json:"token"`
|
||||
} `json:"device"`
|
||||
}
|
||||
|
||||
func (r *Reporter) Register(hostname, osName, ip string) (string, error) {
|
||||
req := RegisterRequest{
|
||||
Hostname: hostname,
|
||||
OS: osName,
|
||||
IP: ip,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := r.client.Post(r.serverURL+"/api/v1/register", "application/json", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to register: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("registration failed: %s", string(respBody))
|
||||
}
|
||||
|
||||
var regResp RegisterResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(®Resp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return regResp.Device.ID, nil
|
||||
}
|
||||
|
||||
func (r *Reporter) Report(metrics interface{}) error {
|
||||
body, err := json.Marshal(metrics)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", r.serverURL+"/api/v1/report", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if r.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+r.token)
|
||||
}
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to report: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("report failed: %s", string(respBody))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reporter) Heartbeat(deviceID string) error {
|
||||
req, err := http.NewRequest("POST", r.serverURL+"/api/v1/devices/"+deviceID+"/heartbeat", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+r.token)
|
||||
}
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user