Files
pulse/internal/monitoring/monitor_libvirt.go
T
2026-07-30 20:22:47 +01:00

216 lines
6.2 KiB
Go

package monitoring
import (
"crypto/sha256"
"encoding/hex"
"math"
"sort"
"strings"
"time"
"unicode/utf8"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
)
const (
maxAgentLibvirtDomains = 128
maxAgentLibvirtDomainNameBytes = 256
)
func (m *Monitor) normalizeAgentLibvirtInventory(
report *agentshost.LibvirtInventory,
hostID string,
previous *models.Host,
observedAt time.Time,
) *models.HostLibvirtInventory {
if report == nil {
if previous != nil && previous.Libvirt != nil {
preserved := *previous.Libvirt
preserved.Domains = append([]models.HostLibvirtDomain(nil), previous.Libvirt.Domains...)
return &preserved
}
return nil
}
previousByID := make(map[string]models.HostLibvirtDomain)
previousAt := time.Time{}
if previous != nil {
previousAt = previous.LastSeen
if previous.Libvirt != nil {
for _, domain := range previous.Libvirt.Domains {
previousByID[domain.ID] = domain
}
}
}
result := &models.HostLibvirtInventory{
Domains: make([]models.HostLibvirtDomain, 0, min(len(report.Domains), maxAgentLibvirtDomains)),
CollectedAt: observedAt.UTC(),
}
seen := make(map[string]struct{}, len(report.Domains))
for _, incoming := range report.Domains {
if len(result.Domains) == maxAgentLibvirtDomains {
break
}
name := strings.TrimSpace(incoming.Name)
if !validAgentLibvirtDomainName(name) {
continue
}
dedupeKey := strings.ToLower(name)
if _, exists := seen[dedupeKey]; exists {
continue
}
seen[dedupeKey] = struct{}{}
domain := models.HostLibvirtDomain{
ID: agentLibvirtDomainID(name),
Name: name,
State: normalizeAgentLibvirtState(incoming.State),
VCPUs: max(0, min(incoming.VCPUs, 4096)),
CPUTimeNanoseconds: incoming.CPUTimeNanoseconds,
MemoryCurrentBytes: max(int64(0), incoming.MemoryCurrentBytes),
MemoryMaximumBytes: max(int64(0), incoming.MemoryMaximumBytes),
NetworkRXBytes: incoming.NetworkRXBytes,
NetworkTXBytes: incoming.NetworkTXBytes,
DiskReadBytes: incoming.DiskReadBytes,
DiskWriteBytes: incoming.DiskWriteBytes,
}
if domain.MemoryMaximumBytes > 0 && domain.MemoryCurrentBytes > domain.MemoryMaximumBytes {
domain.MemoryCurrentBytes = domain.MemoryMaximumBytes
}
if prior, ok := previousByID[domain.ID]; ok {
domain.CPUUsagePercent, domain.CPUUsageValid = libvirtCPUUsagePercent(
prior,
domain,
observedAt.Sub(previousAt),
)
}
if m != nil && m.rateTracker != nil {
diskRead, diskWrite, netIn, netOut := m.rateTracker.CalculateRates(
"libvirt:"+strings.TrimSpace(hostID)+":"+domain.ID,
IOMetrics{
DiskRead: uint64ToInt64(domain.DiskReadBytes),
DiskWrite: uint64ToInt64(domain.DiskWriteBytes),
NetworkIn: uint64ToInt64(domain.NetworkRXBytes),
NetworkOut: uint64ToInt64(domain.NetworkTXBytes),
Timestamp: observedAt,
},
)
if diskRead >= 0 && diskWrite >= 0 && netIn >= 0 && netOut >= 0 {
domain.DiskReadRate = diskRead
domain.DiskWriteRate = diskWrite
domain.NetworkInRate = netIn
domain.NetworkOutRate = netOut
domain.IORatesValid = true
}
}
result.Domains = append(result.Domains, domain)
}
sort.Slice(result.Domains, func(i, j int) bool {
return strings.ToLower(result.Domains[i].Name) < strings.ToLower(result.Domains[j].Name)
})
return result
}
func validAgentLibvirtDomainName(name string) bool {
if name == "" || len(name) > maxAgentLibvirtDomainNameBytes || !utf8.ValidString(name) {
return false
}
for _, r := range name {
if r < 0x20 || r == 0x7f {
return false
}
}
return true
}
func normalizeAgentLibvirtState(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "running", "blocked", "paused", "shutting-down", "shutoff", "crashed", "suspended":
return strings.ToLower(strings.TrimSpace(value))
default:
return "unknown"
}
}
func agentLibvirtDomainID(name string) string {
sum := sha256.Sum256([]byte(name))
return "domain-" + hex.EncodeToString(sum[:8])
}
func libvirtCPUUsagePercent(previous, current models.HostLibvirtDomain, elapsed time.Duration) (float64, bool) {
if elapsed <= 0 || current.CPUTimeNanoseconds < previous.CPUTimeNanoseconds {
return 0, false
}
vcpus := current.VCPUs
if vcpus <= 0 {
vcpus = 1
}
delta := current.CPUTimeNanoseconds - previous.CPUTimeNanoseconds
percent := (float64(delta) / float64(elapsed.Nanoseconds()) / float64(vcpus)) * 100
if math.IsNaN(percent) || math.IsInf(percent, 0) {
return 0, false
}
return math.Max(0, math.Min(100, percent)), true
}
func uint64ToInt64(value uint64) int64 {
if value > math.MaxInt64 {
return math.MaxInt64
}
return int64(value)
}
func libvirtDomainMetricID(hostID, domainID string) string {
return strings.TrimSpace(hostID) + ":libvirt:" + strings.TrimSpace(domainID)
}
func (m *Monitor) writeLibvirtDomainMetrics(host models.Host, observedAt time.Time) {
if m == nil || host.Libvirt == nil {
return
}
for _, domain := range host.Libvirt.Domains {
metricID := libvirtDomainMetricID(host.ID, domain.ID)
if domain.CPUUsageValid {
if m.metricsHistory != nil {
m.metricsHistory.AddGuestMetric(metricID, "cpu", domain.CPUUsagePercent, observedAt)
}
if m.metricsStore != nil {
m.metricsStore.Write("vm", metricID, "cpu", domain.CPUUsagePercent, observedAt)
}
}
if domain.MemoryMaximumBytes > 0 {
memoryPercent := math.Max(0, math.Min(
100,
(float64(domain.MemoryCurrentBytes)/float64(domain.MemoryMaximumBytes))*100,
))
if m.metricsHistory != nil {
m.metricsHistory.AddGuestMetric(metricID, "memory", memoryPercent, observedAt)
}
if m.metricsStore != nil {
m.metricsStore.Write("vm", metricID, "memory", memoryPercent, observedAt)
}
}
if !domain.IORatesValid {
continue
}
for metricType, value := range map[string]float64{
"netin": domain.NetworkInRate,
"netout": domain.NetworkOutRate,
"diskread": domain.DiskReadRate,
"diskwrite": domain.DiskWriteRate,
} {
if m.metricsHistory != nil {
m.metricsHistory.AddGuestMetric(metricID, metricType, value, observedAt)
}
if m.metricsStore != nil {
m.metricsStore.Write("vm", metricID, metricType, value, observedAt)
}
}
}
}