316 lines
9.3 KiB
Go
316 lines
9.3 KiB
Go
package metrics
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/shirou/gopsutil/v3/cpu"
|
|
"github.com/shirou/gopsutil/v3/disk"
|
|
"github.com/shirou/gopsutil/v3/host"
|
|
"github.com/shirou/gopsutil/v3/load"
|
|
"github.com/shirou/gopsutil/v3/mem"
|
|
psnet "github.com/shirou/gopsutil/v3/net"
|
|
|
|
"nannyagentv2/internal/types"
|
|
)
|
|
|
|
// Collector handles system metrics collection
|
|
type Collector struct {
|
|
agentVersion string
|
|
}
|
|
|
|
// NewCollector creates a new metrics collector
|
|
func NewCollector(agentVersion string) *Collector {
|
|
return &Collector{
|
|
agentVersion: agentVersion,
|
|
}
|
|
}
|
|
|
|
// GatherSystemMetrics collects comprehensive system metrics
|
|
func (c *Collector) GatherSystemMetrics() (*types.SystemMetrics, error) {
|
|
metrics := &types.SystemMetrics{
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
// System Information
|
|
if hostInfo, err := host.Info(); err == nil {
|
|
metrics.Hostname = hostInfo.Hostname
|
|
metrics.Platform = hostInfo.Platform
|
|
metrics.PlatformFamily = hostInfo.PlatformFamily
|
|
metrics.PlatformVersion = hostInfo.PlatformVersion
|
|
metrics.KernelVersion = hostInfo.KernelVersion
|
|
metrics.KernelArch = hostInfo.KernelArch
|
|
}
|
|
|
|
// CPU Metrics
|
|
if percentages, err := cpu.Percent(time.Second, false); err == nil && len(percentages) > 0 {
|
|
metrics.CPUUsage = math.Round(percentages[0]*100) / 100
|
|
}
|
|
|
|
if cpuInfo, err := cpu.Info(); err == nil && len(cpuInfo) > 0 {
|
|
metrics.CPUCores = len(cpuInfo)
|
|
metrics.CPUModel = cpuInfo[0].ModelName
|
|
}
|
|
|
|
// Memory Metrics
|
|
if memInfo, err := mem.VirtualMemory(); err == nil {
|
|
metrics.MemoryUsage = math.Round(float64(memInfo.Used)/(1024*1024)*100) / 100 // MB
|
|
metrics.MemoryTotal = memInfo.Total
|
|
metrics.MemoryUsed = memInfo.Used
|
|
metrics.MemoryFree = memInfo.Free
|
|
metrics.MemoryAvailable = memInfo.Available
|
|
}
|
|
|
|
if swapInfo, err := mem.SwapMemory(); err == nil {
|
|
metrics.SwapTotal = swapInfo.Total
|
|
metrics.SwapUsed = swapInfo.Used
|
|
metrics.SwapFree = swapInfo.Free
|
|
}
|
|
|
|
// Disk Metrics
|
|
if diskInfo, err := disk.Usage("/"); err == nil {
|
|
metrics.DiskUsage = math.Round(diskInfo.UsedPercent*100) / 100
|
|
metrics.DiskTotal = diskInfo.Total
|
|
metrics.DiskUsed = diskInfo.Used
|
|
metrics.DiskFree = diskInfo.Free
|
|
}
|
|
|
|
// Load Averages
|
|
if loadAvg, err := load.Avg(); err == nil {
|
|
metrics.LoadAvg1 = math.Round(loadAvg.Load1*100) / 100
|
|
metrics.LoadAvg5 = math.Round(loadAvg.Load5*100) / 100
|
|
metrics.LoadAvg15 = math.Round(loadAvg.Load15*100) / 100
|
|
}
|
|
|
|
// Process Count (simplified - using a constant for now)
|
|
// Note: gopsutil doesn't have host.Processes(), would need process.Processes()
|
|
metrics.ProcessCount = 0 // Placeholder
|
|
|
|
// Network Metrics
|
|
netIn, netOut := c.getNetworkStats()
|
|
metrics.NetworkInKbps = netIn
|
|
metrics.NetworkOutKbps = netOut
|
|
|
|
if netIOCounters, err := psnet.IOCounters(false); err == nil && len(netIOCounters) > 0 {
|
|
netIO := netIOCounters[0]
|
|
metrics.NetworkInBytes = netIO.BytesRecv
|
|
metrics.NetworkOutBytes = netIO.BytesSent
|
|
}
|
|
|
|
// IP Address and Location
|
|
metrics.IPAddress = c.getIPAddress()
|
|
metrics.Location = c.getLocation() // Placeholder
|
|
|
|
// Filesystem Information
|
|
metrics.FilesystemInfo = c.getFilesystemInfo()
|
|
|
|
// Block Devices
|
|
metrics.BlockDevices = c.getBlockDevices()
|
|
|
|
return metrics, nil
|
|
}
|
|
|
|
// getNetworkStats returns network input/output rates in Kbps
|
|
func (c *Collector) getNetworkStats() (float64, float64) {
|
|
netIOCounters, err := psnet.IOCounters(false)
|
|
if err != nil || len(netIOCounters) == 0 {
|
|
return 0.0, 0.0
|
|
}
|
|
|
|
// Use the first interface for aggregate stats
|
|
netIO := netIOCounters[0]
|
|
|
|
// Convert bytes to kilobits per second (simplified - cumulative bytes to kilobits)
|
|
netInKbps := float64(netIO.BytesRecv) * 8 / 1024
|
|
netOutKbps := float64(netIO.BytesSent) * 8 / 1024
|
|
|
|
return netInKbps, netOutKbps
|
|
}
|
|
|
|
// getIPAddress returns the primary IP address of the system
|
|
func (c *Collector) getIPAddress() string {
|
|
interfaces, err := psnet.Interfaces()
|
|
if err != nil {
|
|
return "unknown"
|
|
}
|
|
|
|
for _, iface := range interfaces {
|
|
if len(iface.Addrs) > 0 && !strings.Contains(iface.Addrs[0].Addr, "127.0.0.1") {
|
|
return strings.Split(iface.Addrs[0].Addr, "/")[0] // Remove CIDR if present
|
|
}
|
|
}
|
|
|
|
return "unknown"
|
|
}
|
|
|
|
// getLocation returns basic location information (placeholder)
|
|
func (c *Collector) getLocation() string {
|
|
return "unknown" // Would integrate with GeoIP service
|
|
}
|
|
|
|
// getFilesystemInfo returns information about mounted filesystems
|
|
func (c *Collector) getFilesystemInfo() []types.FilesystemInfo {
|
|
partitions, err := disk.Partitions(false)
|
|
if err != nil {
|
|
return []types.FilesystemInfo{}
|
|
}
|
|
|
|
var filesystems []types.FilesystemInfo
|
|
for _, partition := range partitions {
|
|
usage, err := disk.Usage(partition.Mountpoint)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
fs := types.FilesystemInfo{
|
|
Mountpoint: partition.Mountpoint,
|
|
Fstype: partition.Fstype,
|
|
Total: usage.Total,
|
|
Used: usage.Used,
|
|
Free: usage.Free,
|
|
UsagePercent: math.Round(usage.UsedPercent*100) / 100,
|
|
}
|
|
filesystems = append(filesystems, fs)
|
|
}
|
|
|
|
return filesystems
|
|
}
|
|
|
|
// getBlockDevices returns information about block devices
|
|
func (c *Collector) getBlockDevices() []types.BlockDevice {
|
|
partitions, err := disk.Partitions(true)
|
|
if err != nil {
|
|
return []types.BlockDevice{}
|
|
}
|
|
|
|
var devices []types.BlockDevice
|
|
deviceMap := make(map[string]bool)
|
|
|
|
for _, partition := range partitions {
|
|
// Only include actual block devices
|
|
if strings.HasPrefix(partition.Device, "/dev/") {
|
|
deviceName := partition.Device
|
|
if !deviceMap[deviceName] {
|
|
deviceMap[deviceName] = true
|
|
|
|
device := types.BlockDevice{
|
|
Name: deviceName,
|
|
Model: "unknown",
|
|
Size: 0,
|
|
SerialNumber: "unknown",
|
|
}
|
|
devices = append(devices, device)
|
|
}
|
|
}
|
|
}
|
|
|
|
return devices
|
|
}
|
|
|
|
// SendMetrics sends system metrics to the agent-auth-api endpoint
|
|
func (c *Collector) SendMetrics(agentAuthURL, accessToken, agentID string, metrics *types.SystemMetrics) error {
|
|
// Create flattened metrics request for agent-auth-api
|
|
metricsReq := c.CreateMetricsRequest(agentID, metrics)
|
|
|
|
return c.sendMetricsRequest(agentAuthURL, accessToken, metricsReq)
|
|
}
|
|
|
|
// CreateMetricsRequest converts SystemMetrics to the flattened format expected by agent-auth-api
|
|
func (c *Collector) CreateMetricsRequest(agentID string, systemMetrics *types.SystemMetrics) *types.MetricsRequest {
|
|
return &types.MetricsRequest{
|
|
AgentID: agentID,
|
|
CPUUsage: systemMetrics.CPUUsage,
|
|
MemoryUsage: systemMetrics.MemoryUsage,
|
|
DiskUsage: systemMetrics.DiskUsage,
|
|
NetworkInKbps: systemMetrics.NetworkInKbps,
|
|
NetworkOutKbps: systemMetrics.NetworkOutKbps,
|
|
IPAddress: systemMetrics.IPAddress,
|
|
Location: systemMetrics.Location,
|
|
AgentVersion: c.agentVersion,
|
|
KernelVersion: systemMetrics.KernelVersion,
|
|
DeviceFingerprint: c.generateDeviceFingerprint(systemMetrics),
|
|
LoadAverages: map[string]float64{
|
|
"load1": systemMetrics.LoadAvg1,
|
|
"load5": systemMetrics.LoadAvg5,
|
|
"load15": systemMetrics.LoadAvg15,
|
|
},
|
|
OSInfo: map[string]string{
|
|
"platform": systemMetrics.Platform,
|
|
"platform_family": systemMetrics.PlatformFamily,
|
|
"platform_version": systemMetrics.PlatformVersion,
|
|
"kernel_version": systemMetrics.KernelVersion,
|
|
"kernel_arch": systemMetrics.KernelArch,
|
|
},
|
|
FilesystemInfo: systemMetrics.FilesystemInfo,
|
|
BlockDevices: systemMetrics.BlockDevices,
|
|
NetworkStats: map[string]uint64{
|
|
"bytes_sent": systemMetrics.NetworkOutBytes,
|
|
"bytes_recv": systemMetrics.NetworkInBytes,
|
|
"total_bytes": systemMetrics.NetworkInBytes + systemMetrics.NetworkOutBytes,
|
|
},
|
|
}
|
|
}
|
|
|
|
// sendMetricsRequest sends the metrics request to the agent-auth-api
|
|
func (c *Collector) sendMetricsRequest(agentAuthURL, accessToken string, metricsReq *types.MetricsRequest) error {
|
|
// Wrap metrics in the expected payload structure
|
|
payload := map[string]interface{}{
|
|
"metrics": metricsReq,
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
|
|
jsonData, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal metrics: %w", err)
|
|
}
|
|
|
|
// Send to /metrics endpoint
|
|
metricsURL := fmt.Sprintf("%s/metrics", agentAuthURL)
|
|
req, err := http.NewRequest("POST", metricsURL, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send metrics: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read response
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
// Check response status
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
return fmt.Errorf("unauthorized")
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("metrics request failed with status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateDeviceFingerprint creates a unique device identifier
|
|
func (c *Collector) generateDeviceFingerprint(metrics *types.SystemMetrics) string {
|
|
fingerprint := fmt.Sprintf("%s-%s-%s", metrics.Hostname, metrics.Platform, metrics.KernelVersion)
|
|
hasher := sha256.New()
|
|
hasher.Write([]byte(fingerprint))
|
|
return fmt.Sprintf("%x", hasher.Sum(nil))[:16]
|
|
}
|