mirror of https://github.com/jetkvm/kvm.git
263 lines
6.5 KiB
Go
263 lines
6.5 KiB
Go
package audio
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/jetkvm/kvm/internal/logging"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// ProcessMetrics represents CPU and memory usage metrics for a process
|
|
type ProcessMetrics struct {
|
|
PID int `json:"pid"`
|
|
CPUPercent float64 `json:"cpu_percent"`
|
|
MemoryRSS int64 `json:"memory_rss_bytes"`
|
|
MemoryVMS int64 `json:"memory_vms_bytes"`
|
|
MemoryPercent float64 `json:"memory_percent"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
ProcessName string `json:"process_name"`
|
|
}
|
|
|
|
// ProcessMonitor monitors CPU and memory usage of processes
|
|
type ProcessMonitor struct {
|
|
logger zerolog.Logger
|
|
mutex sync.RWMutex
|
|
monitoredPIDs map[int]*processState
|
|
running bool
|
|
stopChan chan struct{}
|
|
metricsChan chan ProcessMetrics
|
|
updateInterval time.Duration
|
|
}
|
|
|
|
// processState tracks the state needed for CPU calculation
|
|
type processState struct {
|
|
name string
|
|
lastCPUTime int64
|
|
lastSysTime int64
|
|
lastUserTime int64
|
|
lastSample time.Time
|
|
}
|
|
|
|
// NewProcessMonitor creates a new process monitor
|
|
func NewProcessMonitor() *ProcessMonitor {
|
|
return &ProcessMonitor{
|
|
logger: logging.GetDefaultLogger().With().Str("component", "process-monitor").Logger(),
|
|
monitoredPIDs: make(map[int]*processState),
|
|
stopChan: make(chan struct{}),
|
|
metricsChan: make(chan ProcessMetrics, 100),
|
|
updateInterval: 2 * time.Second, // Update every 2 seconds
|
|
}
|
|
}
|
|
|
|
// Start begins monitoring processes
|
|
func (pm *ProcessMonitor) Start() {
|
|
pm.mutex.Lock()
|
|
defer pm.mutex.Unlock()
|
|
|
|
if pm.running {
|
|
return
|
|
}
|
|
|
|
pm.running = true
|
|
go pm.monitorLoop()
|
|
pm.logger.Info().Msg("Process monitor started")
|
|
}
|
|
|
|
// Stop stops monitoring processes
|
|
func (pm *ProcessMonitor) Stop() {
|
|
pm.mutex.Lock()
|
|
defer pm.mutex.Unlock()
|
|
|
|
if !pm.running {
|
|
return
|
|
}
|
|
|
|
pm.running = false
|
|
close(pm.stopChan)
|
|
pm.logger.Info().Msg("Process monitor stopped")
|
|
}
|
|
|
|
// AddProcess adds a process to monitor
|
|
func (pm *ProcessMonitor) AddProcess(pid int, name string) {
|
|
pm.mutex.Lock()
|
|
defer pm.mutex.Unlock()
|
|
|
|
pm.monitoredPIDs[pid] = &processState{
|
|
name: name,
|
|
lastSample: time.Now(),
|
|
}
|
|
pm.logger.Info().Int("pid", pid).Str("name", name).Msg("Added process to monitor")
|
|
}
|
|
|
|
// RemoveProcess removes a process from monitoring
|
|
func (pm *ProcessMonitor) RemoveProcess(pid int) {
|
|
pm.mutex.Lock()
|
|
defer pm.mutex.Unlock()
|
|
|
|
delete(pm.monitoredPIDs, pid)
|
|
pm.logger.Info().Int("pid", pid).Msg("Removed process from monitor")
|
|
}
|
|
|
|
// GetMetricsChan returns the channel for receiving metrics
|
|
func (pm *ProcessMonitor) GetMetricsChan() <-chan ProcessMetrics {
|
|
return pm.metricsChan
|
|
}
|
|
|
|
// GetCurrentMetrics returns current metrics for all monitored processes
|
|
func (pm *ProcessMonitor) GetCurrentMetrics() []ProcessMetrics {
|
|
pm.mutex.RLock()
|
|
defer pm.mutex.RUnlock()
|
|
|
|
var metrics []ProcessMetrics
|
|
for pid, state := range pm.monitoredPIDs {
|
|
if metric, err := pm.collectMetrics(pid, state); err == nil {
|
|
metrics = append(metrics, metric)
|
|
}
|
|
}
|
|
return metrics
|
|
}
|
|
|
|
// monitorLoop is the main monitoring loop
|
|
func (pm *ProcessMonitor) monitorLoop() {
|
|
ticker := time.NewTicker(pm.updateInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-pm.stopChan:
|
|
return
|
|
case <-ticker.C:
|
|
pm.collectAllMetrics()
|
|
}
|
|
}
|
|
}
|
|
|
|
// collectAllMetrics collects metrics for all monitored processes
|
|
func (pm *ProcessMonitor) collectAllMetrics() {
|
|
pm.mutex.RLock()
|
|
pids := make(map[int]*processState)
|
|
for pid, state := range pm.monitoredPIDs {
|
|
pids[pid] = state
|
|
}
|
|
pm.mutex.RUnlock()
|
|
|
|
for pid, state := range pids {
|
|
if metric, err := pm.collectMetrics(pid, state); err == nil {
|
|
select {
|
|
case pm.metricsChan <- metric:
|
|
default:
|
|
// Channel full, skip this metric
|
|
}
|
|
} else {
|
|
// Process might have died, remove it
|
|
pm.RemoveProcess(pid)
|
|
}
|
|
}
|
|
}
|
|
|
|
// collectMetrics collects metrics for a specific process
|
|
func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessMetrics, error) {
|
|
now := time.Now()
|
|
metric := ProcessMetrics{
|
|
PID: pid,
|
|
Timestamp: now,
|
|
ProcessName: state.name,
|
|
}
|
|
|
|
// Read /proc/[pid]/stat for CPU and memory info
|
|
statPath := fmt.Sprintf("/proc/%d/stat", pid)
|
|
statData, err := os.ReadFile(statPath)
|
|
if err != nil {
|
|
return metric, fmt.Errorf("failed to read stat file: %w", err)
|
|
}
|
|
|
|
// Parse stat file
|
|
fields := strings.Fields(string(statData))
|
|
if len(fields) < 24 {
|
|
return metric, fmt.Errorf("invalid stat file format")
|
|
}
|
|
|
|
// Extract CPU times (fields 13, 14 are utime, stime in clock ticks)
|
|
utime, _ := strconv.ParseInt(fields[13], 10, 64)
|
|
stime, _ := strconv.ParseInt(fields[14], 10, 64)
|
|
totalCPUTime := utime + stime
|
|
|
|
// Extract memory info (field 22 is vsize, field 23 is rss in pages)
|
|
vsize, _ := strconv.ParseInt(fields[22], 10, 64)
|
|
rss, _ := strconv.ParseInt(fields[23], 10, 64)
|
|
|
|
// Convert RSS from pages to bytes (assuming 4KB pages)
|
|
pageSize := int64(4096)
|
|
metric.MemoryRSS = rss * pageSize
|
|
metric.MemoryVMS = vsize
|
|
|
|
// Calculate CPU percentage
|
|
if !state.lastSample.IsZero() {
|
|
timeDelta := now.Sub(state.lastSample).Seconds()
|
|
cpuDelta := float64(totalCPUTime - state.lastCPUTime)
|
|
|
|
// Convert from clock ticks to seconds (assuming 100 Hz)
|
|
clockTicks := 100.0
|
|
cpuSeconds := cpuDelta / clockTicks
|
|
|
|
if timeDelta > 0 {
|
|
metric.CPUPercent = (cpuSeconds / timeDelta) * 100.0
|
|
}
|
|
}
|
|
|
|
// Calculate memory percentage (RSS / total system memory)
|
|
if totalMem := pm.getTotalMemory(); totalMem > 0 {
|
|
metric.MemoryPercent = float64(metric.MemoryRSS) / float64(totalMem) * 100.0
|
|
}
|
|
|
|
// Update state for next calculation
|
|
state.lastCPUTime = totalCPUTime
|
|
state.lastUserTime = utime
|
|
state.lastSysTime = stime
|
|
state.lastSample = now
|
|
|
|
return metric, nil
|
|
}
|
|
|
|
// getTotalMemory returns total system memory in bytes
|
|
func (pm *ProcessMonitor) getTotalMemory() int64 {
|
|
file, err := os.Open("/proc/meminfo")
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.HasPrefix(line, "MemTotal:") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 2 {
|
|
if kb, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
|
|
return kb * 1024 // Convert KB to bytes
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Global process monitor instance
|
|
var globalProcessMonitor *ProcessMonitor
|
|
var processMonitorOnce sync.Once
|
|
|
|
// GetProcessMonitor returns the global process monitor instance
|
|
func GetProcessMonitor() *ProcessMonitor {
|
|
processMonitorOnce.Do(func() {
|
|
globalProcessMonitor = NewProcessMonitor()
|
|
globalProcessMonitor.Start()
|
|
})
|
|
return globalProcessMonitor
|
|
} |