Files
pulse/internal/unifiedresources/metrics.go
T
2026-07-06 16:11:36 +01:00

830 lines
26 KiB
Go

package unifiedresources
import (
"hash/fnv"
"math"
"math/rand"
"strings"
"github.com/rcourtman/pulse-go-rewrite/internal/mockmode"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
)
func metricsFromProxmoxNode(node models.Node) *ResourceMetrics {
metrics := &ResourceMetrics{}
cpuPercent := percentFromUsage(node.CPU)
metrics.CPU = &MetricValue{Value: cpuPercent, Percent: cpuPercent, Unit: "percent", Source: SourceProxmox}
if node.Memory.Total > 0 {
percent := percentFromUsage(node.Memory.Usage)
metrics.Memory = &MetricValue{Used: &node.Memory.Used, Total: &node.Memory.Total, Percent: percent, Unit: "bytes", Source: SourceProxmox}
}
if node.Disk.Total > 0 {
percent := percentFromUsage(node.Disk.Usage)
metrics.Disk = &MetricValue{Used: &node.Disk.Used, Total: &node.Disk.Total, Percent: percent, Unit: "bytes", Source: SourceProxmox}
}
return metrics
}
func metricsFromHost(host models.Host) *ResourceMetrics {
return buildHostMetricPayload(
host.CPUUsage,
host.Memory,
host.Disks,
host.NetInRate,
host.NetOutRate,
host.DiskReadRate,
host.DiskWriteRate,
SourceAgent,
)
}
func metricsFromUnraidStorage(host models.Host) *ResourceMetrics {
metrics := &ResourceMetrics{}
total, used, _, percent := unraidStorageCapacity(host)
if total > 0 {
metrics.Disk = unraidDiskMetric(total, used, percent)
}
return metrics
}
func metricsFromUnraidDiskCapacity(total, used int64, percent float64) *ResourceMetrics {
metrics := &ResourceMetrics{}
if total > 0 {
metrics.Disk = unraidDiskMetric(total, used, percent)
}
return metrics
}
func unraidDiskMetric(total, used int64, percent float64) *MetricValue {
return &MetricValue{
Used: &used,
Total: &total,
Percent: percent,
Unit: "bytes",
Source: SourceAgent,
}
}
func metricsFromDockerHost(host models.DockerHost) *ResourceMetrics {
return buildHostMetricPayload(
host.CPUUsage,
host.Memory,
host.Disks,
host.NetInRate,
host.NetOutRate,
host.DiskReadRate,
host.DiskWriteRate,
SourceDocker,
)
}
func buildHostMetricPayload(
cpuUsage float64,
memory models.Memory,
disks []models.Disk,
netInRate, netOutRate, diskReadRate, diskWriteRate float64,
source DataSource,
) *ResourceMetrics {
metrics := &ResourceMetrics{}
cpuPercent := percentFromReportedPercent(cpuUsage)
metrics.CPU = &MetricValue{Value: cpuPercent, Percent: cpuPercent, Unit: "percent", Source: source}
if memory.Total > 0 {
percent := percentFromReportedPercent(memory.Usage)
metrics.Memory = &MetricValue{Used: &memory.Used, Total: &memory.Total, Percent: percent, Unit: "bytes", Source: source}
}
if len(disks) > 0 {
disk := disks[0]
if disk.Total > 0 {
percent := percentFromReportedPercent(disk.Usage)
metrics.Disk = &MetricValue{Used: &disk.Used, Total: &disk.Total, Percent: percent, Unit: "bytes", Source: source}
}
}
setNetworkAndDiskIOMetricsHost(metrics, netInRate, netOutRate, diskReadRate, diskWriteRate, source)
return metrics
}
func metricsFromPBSInstance(instance models.PBSInstance) *ResourceMetrics {
metrics := &ResourceMetrics{}
cpuPercent := percentFromUsage(instance.CPU)
metrics.CPU = &MetricValue{Value: cpuPercent, Percent: cpuPercent, Unit: "percent", Source: SourcePBS}
if instance.MemoryTotal > 0 {
percent := percentFromUsage(instance.Memory)
metrics.Memory = &MetricValue{
Used: &instance.MemoryUsed,
Total: &instance.MemoryTotal,
Percent: percent,
Unit: "bytes",
Source: SourcePBS,
}
}
return metrics
}
func metricsFromPBSDatastore(datastore models.PBSDatastore) *ResourceMetrics {
metrics := &ResourceMetrics{}
if datastore.Total <= 0 {
return metrics
}
used := datastore.Used
total := datastore.Total
percent := percentFromUsage(datastore.Usage)
if percent == 0 && total > 0 {
percent = clampMetricValue((float64(used)/float64(total))*100, 0, 100)
}
metrics.Disk = &MetricValue{
Used: &used,
Total: &total,
Percent: percent,
Unit: "bytes",
Source: SourcePBS,
}
return metrics
}
func metricsFromPMGInstance(instance models.PMGInstance) *ResourceMetrics {
// PMG currently exposes aggregate queue/mail counters, not point-in-time
// throughput rates. Return an empty metrics object to avoid mislabeling
// aggregate totals as realtime I/O.
return &ResourceMetrics{}
}
func metricsFromVM(vm models.VM) *ResourceMetrics {
return buildVMMetricPayload(
vm.CPU,
vm.Memory,
vm.Disk,
vm.NetworkIn,
vm.NetworkOut,
vm.DiskRead,
vm.DiskWrite,
SourceProxmox,
)
}
func metricsFromContainer(ct models.Container) *ResourceMetrics {
return buildVMMetricPayload(
ct.CPU,
ct.Memory,
ct.Disk,
ct.NetworkIn,
ct.NetworkOut,
ct.DiskRead,
ct.DiskWrite,
SourceProxmox,
)
}
func buildVMMetricPayload(
cpu float64,
memory models.Memory,
disk models.Disk,
netIn, netOut, diskRead, diskWrite int64,
source DataSource,
) *ResourceMetrics {
metrics := &ResourceMetrics{}
cpuPercent := percentFromUsage(cpu)
metrics.CPU = &MetricValue{Value: cpuPercent, Percent: cpuPercent, Unit: "percent", Source: source}
if memory.Total > 0 {
percent := percentFromUsage(memory.Usage)
metrics.Memory = &MetricValue{Used: &memory.Used, Total: &memory.Total, Percent: percent, Unit: "bytes", Source: source}
}
if disk.Total > 0 {
percent := percentFromUsage(disk.Usage)
metrics.Disk = &MetricValue{Used: &disk.Used, Total: &disk.Total, Percent: percent, Unit: "bytes", Source: source}
}
setNetworkAndDiskIOMetricsVM(metrics, netIn, netOut, diskRead, diskWrite, source)
return metrics
}
func metricsFromStorage(storage models.Storage) *ResourceMetrics {
metrics := &ResourceMetrics{}
if storage.Total <= 0 {
return metrics
}
used := storage.Used
total := storage.Total
percent := percentFromUsage(storage.Usage)
if percent == 0 && total > 0 {
percent = clampMetricValue((float64(used)/float64(total))*100, 0, 100)
}
metrics.Disk = &MetricValue{
Used: &used,
Total: &total,
Percent: percent,
Unit: "bytes",
Source: SourceProxmox,
}
return metrics
}
func metricsFromCephCluster(cluster models.CephCluster) *ResourceMetrics {
metrics := &ResourceMetrics{}
if cluster.TotalBytes > 0 {
used := cluster.UsedBytes
total := cluster.TotalBytes
percent := clampMetricValue(cluster.UsagePercent, 0, 100)
if percent == 0 && total > 0 {
percent = clampMetricValue((float64(used)/float64(total))*100, 0, 100)
}
metrics.Disk = &MetricValue{
Used: &used,
Total: &total,
Percent: percent,
Unit: "bytes",
Source: SourceProxmox,
}
}
return metrics
}
func metricsFromPhysicalDisk(disk models.PhysicalDisk) *ResourceMetrics {
metrics := &ResourceMetrics{}
if disk.Size > 0 {
used := int64(0)
total := disk.Size
metrics.Disk = &MetricValue{
Used: &used,
Total: &total,
Unit: "bytes",
Source: SourceProxmox,
}
}
return metrics
}
func metricsFromDockerContainer(ct models.DockerContainer, hostCPUs ...int) *ResourceMetrics {
metrics := &ResourceMetrics{}
cpuCapacity := 0
if len(hostCPUs) > 0 {
cpuCapacity = hostCPUs[0]
}
cpuPercent := percentFromReportedPercent(models.DockerContainerCPUCapacityPercent(ct, cpuCapacity))
metrics.CPU = &MetricValue{Value: cpuPercent, Percent: cpuPercent, Unit: "percent", Source: SourceDocker}
if ct.MemoryLimit > 0 {
percent := percentFromReportedPercent(ct.MemoryPercent)
metrics.Memory = &MetricValue{Used: &ct.MemoryUsage, Total: &ct.MemoryLimit, Percent: percent, Unit: "bytes", Source: SourceDocker}
}
if ct.RootFilesystemBytes > 0 {
used := ct.WritableLayerBytes
if used < 0 {
used = 0
}
if used > ct.RootFilesystemBytes {
used = ct.RootFilesystemBytes
}
percent := clampMetricValue((float64(used)/float64(ct.RootFilesystemBytes))*100, 0, 100)
metrics.Disk = &MetricValue{Used: &used, Total: &ct.RootFilesystemBytes, Percent: percent, Unit: "bytes", Source: SourceDocker}
}
if ct.NetInRate > 0 {
metrics.NetIn = &MetricValue{Value: ct.NetInRate, Unit: "bytes/s", Source: SourceDocker}
}
if ct.NetOutRate > 0 {
metrics.NetOut = &MetricValue{Value: ct.NetOutRate, Unit: "bytes/s", Source: SourceDocker}
}
if ct.BlockIO != nil {
if ct.BlockIO.ReadRateBytesPerSecond != nil && *ct.BlockIO.ReadRateBytesPerSecond > 0 {
metrics.DiskRead = &MetricValue{Value: *ct.BlockIO.ReadRateBytesPerSecond, Unit: "bytes/s", Source: SourceDocker}
}
if ct.BlockIO.WriteRateBytesPerSecond != nil && *ct.BlockIO.WriteRateBytesPerSecond > 0 {
metrics.DiskWrite = &MetricValue{Value: *ct.BlockIO.WriteRateBytesPerSecond, Unit: "bytes/s", Source: SourceDocker}
}
}
if mockmode.IsEnabled() && (metrics.NetIn == nil || metrics.NetOut == nil || metrics.DiskRead == nil || metrics.DiskWrite == nil) {
synthetic := syntheticDockerContainerIOMetrics(ct)
if metrics.NetIn == nil {
metrics.NetIn = &MetricValue{Value: synthetic.NetIn, Unit: "bytes/s", Source: SourceDocker}
}
if metrics.NetOut == nil {
metrics.NetOut = &MetricValue{Value: synthetic.NetOut, Unit: "bytes/s", Source: SourceDocker}
}
if metrics.DiskRead == nil {
metrics.DiskRead = &MetricValue{Value: synthetic.DiskRead, Unit: "bytes/s", Source: SourceDocker}
}
if metrics.DiskWrite == nil {
metrics.DiskWrite = &MetricValue{Value: synthetic.DiskWrite, Unit: "bytes/s", Source: SourceDocker}
}
}
return metrics
}
func metricsFromKubernetesCluster(cluster models.KubernetesCluster, linkedHosts []*models.Host) *ResourceMetrics {
metrics := &ResourceMetrics{}
var cpuSum float64
var cpuCount int
var memoryUsed int64
var memoryTotal int64
var diskUsed int64
var diskTotal int64
var netIn float64
var netOut float64
var diskRead float64
var diskWrite float64
var hasNetIn bool
var hasNetOut bool
var hasDiskRead bool
var hasDiskWrite bool
linkedNodeNames := make(map[string]struct{}, len(linkedHosts)*2)
for _, host := range linkedHosts {
if host == nil {
continue
}
cpuSum += percentFromReportedPercent(host.CPUUsage)
cpuCount++
hostName := strings.TrimSpace(host.Hostname)
if hostName != "" {
linkedNodeNames[strings.ToLower(hostName)] = struct{}{}
linkedNodeNames[strings.ToLower(NormalizeHostname(hostName))] = struct{}{}
}
if host.Memory.Total > 0 {
memoryTotal += host.Memory.Total
memoryUsed += host.Memory.Used
}
if len(host.Disks) > 0 {
disk := host.Disks[0]
if disk.Total > 0 {
diskTotal += disk.Total
diskUsed += disk.Used
}
}
if host.NetInRate > 0 {
netIn += host.NetInRate
hasNetIn = true
}
if host.NetOutRate > 0 {
netOut += host.NetOutRate
hasNetOut = true
}
if host.DiskReadRate > 0 {
diskRead += host.DiskReadRate
hasDiskRead = true
}
if host.DiskWriteRate > 0 {
diskWrite += host.DiskWriteRate
hasDiskWrite = true
}
}
// Fall back to Kubernetes metrics API usage for nodes without a linked agent host.
for _, node := range cluster.Nodes {
nodeName := strings.TrimSpace(node.Name)
normalized := strings.ToLower(NormalizeHostname(nodeName))
if _, linked := linkedNodeNames[strings.ToLower(nodeName)]; linked {
continue
}
if normalized != "" {
if _, linked := linkedNodeNames[normalized]; linked {
continue
}
}
if node.UsageCPUPercent > 0 {
cpuSum += clampMetricValue(node.UsageCPUPercent, 0, 100)
cpuCount++
}
if node.UsageMemoryBytes > 0 {
total := node.AllocMemoryBytes
if total <= 0 {
total = node.CapacityMemoryBytes
}
if total > 0 {
memoryUsed += node.UsageMemoryBytes
memoryTotal += total
}
}
}
if cpuCount > 0 {
cpuPercent := clampMetricValue(cpuSum/float64(cpuCount), 0, 100)
metrics.CPU = &MetricValue{Value: cpuPercent, Percent: cpuPercent, Unit: "percent", Source: SourceAgent}
}
if memoryTotal > 0 {
percent := clampMetricValue((float64(memoryUsed)/float64(memoryTotal))*100, 0, 100)
metrics.Memory = &MetricValue{Used: &memoryUsed, Total: &memoryTotal, Percent: percent, Unit: "bytes", Source: SourceAgent}
}
if diskTotal > 0 {
percent := clampMetricValue((float64(diskUsed)/float64(diskTotal))*100, 0, 100)
metrics.Disk = &MetricValue{Used: &diskUsed, Total: &diskTotal, Percent: percent, Unit: "bytes", Source: SourceAgent}
}
if hasNetIn {
metrics.NetIn = &MetricValue{Value: netIn, Unit: "bytes/s", Source: SourceAgent}
}
if hasNetOut {
metrics.NetOut = &MetricValue{Value: netOut, Unit: "bytes/s", Source: SourceAgent}
}
if hasDiskRead {
metrics.DiskRead = &MetricValue{Value: diskRead, Unit: "bytes/s", Source: SourceAgent}
}
if hasDiskWrite {
metrics.DiskWrite = &MetricValue{Value: diskWrite, Unit: "bytes/s", Source: SourceAgent}
}
return metrics
}
func metricsFromKubernetesNode(_ models.KubernetesCluster, node models.KubernetesNode, linkedHost *models.Host) *ResourceMetrics {
// Kubernetes node usage comes from the pulse-agent host module when running on the
// same infrastructure node (unified agent). If no linked host exists, use
// Kubernetes metrics API usage fields when available.
if linkedHost != nil {
return metricsFromHost(*linkedHost)
}
metrics := &ResourceMetrics{}
if node.UsageCPUPercent > 0 {
cpuPercent := clampMetricValue(node.UsageCPUPercent, 0, 100)
metrics.CPU = &MetricValue{Value: cpuPercent, Percent: cpuPercent, Unit: "percent", Source: SourceK8s}
}
if node.UsageMemoryBytes > 0 {
total := node.AllocMemoryBytes
if total <= 0 {
total = node.CapacityMemoryBytes
}
if total > 0 {
used := node.UsageMemoryBytes
percent := clampMetricValue((float64(used)/float64(total))*100, 0, 100)
metrics.Memory = &MetricValue{Used: &used, Total: &total, Percent: percent, Unit: "bytes", Source: SourceK8s}
}
}
return metrics
}
// metricsFromKubernetesDeployment projects deployment-level metrics so the
// platform-page Deployments table stops rendering dashes for CPU / Memory /
// Disk. Real upstream Deployments do not natively expose resource metrics —
// they are scheduling abstractions over their controlled pods — so this
// helper currently synthesises mock-mode values from the deployment's
// replica state. Production deployments without aggregated pod metrics
// still return nil here; once the canonical adapter learns how to roll up
// pod metrics into the owning deployment, swap the synthetic branch for the
// real aggregate while keeping the synthetic fallback for mock mode.
func metricsFromKubernetesDeployment(cluster models.KubernetesCluster, deployment models.KubernetesDeployment) *ResourceMetrics {
if !mockmode.IsEnabled() {
return nil
}
values := syntheticKubernetesDeploymentMetrics(cluster, deployment)
return &ResourceMetrics{
CPU: &MetricValue{Value: values.CPU, Percent: values.CPU, Unit: "percent", Source: SourceK8s},
Memory: &MetricValue{Value: values.Memory, Percent: values.Memory, Unit: "percent", Source: SourceK8s},
Disk: &MetricValue{Value: values.Disk, Percent: values.Disk, Unit: "percent", Source: SourceK8s},
NetIn: &MetricValue{Value: values.NetIn, Unit: "bytes/s", Source: SourceK8s},
NetOut: &MetricValue{Value: values.NetOut, Unit: "bytes/s", Source: SourceK8s},
DiskRead: &MetricValue{Value: values.DiskRead, Unit: "bytes/s", Source: SourceK8s},
DiskWrite: &MetricValue{Value: values.DiskWrite, Unit: "bytes/s", Source: SourceK8s},
}
}
type kubernetesDeploymentSyntheticMetrics struct {
CPU float64
Memory float64
Disk float64
NetIn float64
NetOut float64
DiskRead float64
DiskWrite float64
}
func syntheticKubernetesDeploymentMetrics(cluster models.KubernetesCluster, deployment models.KubernetesDeployment) kubernetesDeploymentSyntheticMetrics {
seed := hashMetricsSeed(
cluster.ID,
cluster.Name,
cluster.DisplayName,
deployment.UID,
deployment.Namespace,
deployment.Name,
)
rng := rand.New(rand.NewSource(int64(seed)))
desired := float64(deployment.DesiredReplicas)
if desired <= 0 {
desired = 1
}
ready := float64(deployment.ReadyReplicas)
if ready < 0 {
ready = 0
}
if ready > desired {
ready = desired
}
available := float64(deployment.AvailableReplicas)
if available < 0 {
available = 0
}
if available > desired {
available = desired
}
readiness := ready / desired
availability := available / desired
// Healthy deployments cluster around moderate utilisation; degraded
// deployments (ready < desired) read as elevated CPU / memory pressure
// because the surviving pods absorb the dropped replicas' load.
pressure := 1.0 + (1.0-readiness)*0.85
scale := math.Min(1.0+desired*0.05, 1.6)
cpu := (24 + rng.Float64()*18) * pressure * scale
memory := (38 + rng.Float64()*22) * pressure * scale
disk := (12 + rng.Float64()*16) * scale
netIn := (12_000 + rng.Float64()*60_000) * scale * availability
netOut := (9_000 + rng.Float64()*48_000) * scale * availability
diskRead := (4_000 + rng.Float64()*32_000) * scale * availability
diskWrite := (3_000 + rng.Float64()*24_000) * scale * availability
return kubernetesDeploymentSyntheticMetrics{
CPU: clampMetricValue(cpu, 0, 100),
Memory: clampMetricValue(memory, 0, 100),
Disk: clampMetricValue(disk, 0, 100),
NetIn: netIn,
NetOut: netOut,
DiskRead: diskRead,
DiskWrite: diskWrite,
}
}
func metricsFromKubernetesPod(cluster models.KubernetesCluster, pod models.KubernetesPod) *ResourceMetrics {
metrics := &ResourceMetrics{}
if pod.UsageCPUPercent > 0 {
cpuPercent := clampMetricValue(pod.UsageCPUPercent, 0, 100)
metrics.CPU = &MetricValue{Value: cpuPercent, Percent: cpuPercent, Unit: "percent", Source: SourceK8s}
}
if pod.UsageMemoryBytes > 0 {
total := kubernetesPodMemoryTotalBytes(cluster, pod)
if total > 0 {
used := pod.UsageMemoryBytes
percent := clampMetricValue((float64(used)/float64(total))*100, 0, 100)
metrics.Memory = &MetricValue{Used: &used, Total: &total, Percent: percent, Unit: "bytes", Source: SourceK8s}
} else if pod.UsageMemoryPercent > 0 {
memPercent := clampMetricValue(pod.UsageMemoryPercent, 0, 100)
metrics.Memory = &MetricValue{Value: memPercent, Percent: memPercent, Unit: "percent", Source: SourceK8s}
}
}
if pod.DiskUsagePercent > 0 {
diskPercent := clampMetricValue(pod.DiskUsagePercent, 0, 100)
metrics.Disk = &MetricValue{Value: diskPercent, Percent: diskPercent, Unit: "percent", Source: SourceK8s}
}
if pod.NetInRate > 0 {
metrics.NetIn = &MetricValue{Value: pod.NetInRate, Unit: "bytes/s", Source: SourceK8s}
}
if pod.NetOutRate > 0 {
metrics.NetOut = &MetricValue{Value: pod.NetOutRate, Unit: "bytes/s", Source: SourceK8s}
}
if !mockmode.IsEnabled() {
return metrics
}
values := syntheticKubernetesPodMetrics(cluster, pod)
if metrics.CPU == nil {
metrics.CPU = &MetricValue{Value: values.CPU, Percent: values.CPU, Unit: "percent", Source: SourceK8s}
}
if metrics.Memory == nil {
metrics.Memory = &MetricValue{Value: values.Memory, Percent: values.Memory, Unit: "percent", Source: SourceK8s}
}
if metrics.Disk == nil {
metrics.Disk = &MetricValue{Value: values.Disk, Percent: values.Disk, Unit: "percent", Source: SourceK8s}
}
if metrics.NetIn == nil {
metrics.NetIn = &MetricValue{Value: values.NetIn, Unit: "bytes/s", Source: SourceK8s}
}
if metrics.NetOut == nil {
metrics.NetOut = &MetricValue{Value: values.NetOut, Unit: "bytes/s", Source: SourceK8s}
}
return metrics
}
func kubernetesPodMemoryTotalBytes(cluster models.KubernetesCluster, pod models.KubernetesPod) int64 {
nodeName := strings.TrimSpace(pod.NodeName)
if nodeName == "" {
return 0
}
for _, node := range cluster.Nodes {
if !strings.EqualFold(strings.TrimSpace(node.Name), nodeName) {
continue
}
if node.AllocMemoryBytes > 0 {
return node.AllocMemoryBytes
}
if node.CapacityMemoryBytes > 0 {
return node.CapacityMemoryBytes
}
return 0
}
return 0
}
type kubernetesPodSyntheticMetrics struct {
CPU float64
Memory float64
Disk float64
NetIn float64
NetOut float64
DiskRead float64
DiskWrite float64
}
type dockerContainerSyntheticIOMetrics struct {
NetIn float64
NetOut float64
DiskRead float64
DiskWrite float64
}
func syntheticDockerContainerIOMetrics(ct models.DockerContainer) dockerContainerSyntheticIOMetrics {
seed := hashMetricsSeed(ct.ID, ct.Name, ct.Image, ct.State)
rng := rand.New(rand.NewSource(int64(seed)))
state := strings.ToLower(strings.TrimSpace(ct.State))
running := state == "running" || state == "restarting"
cpuActivity := percentFromReportedPercent(models.DockerContainerCPUCapacityPercent(ct, 0))
memoryActivity := percentFromReportedPercent(ct.MemoryPercent)
activity := clampMetricValue((cpuActivity+memoryActivity)/180.0, 0.12, 1.25)
if !running {
activity = clampMetricValue(activity*0.32, 0.05, 0.45)
}
restartMultiplier := 1 + math.Min(float64(ct.RestartCount)*0.08, 0.55)
netIn := (28 + activity*540 + rng.Float64()*210) * restartMultiplier
netOut := (22 + activity*470 + rng.Float64()*180) * restartMultiplier
diskRead := (16 + activity*320 + rng.Float64()*140) * restartMultiplier
diskWrite := (14 + activity*300 + rng.Float64()*130) * restartMultiplier
return dockerContainerSyntheticIOMetrics{
NetIn: clampMetricValue(netIn, 4, 3200),
NetOut: clampMetricValue(netOut, 3, 2800),
DiskRead: clampMetricValue(diskRead, 2, 2400),
DiskWrite: clampMetricValue(diskWrite, 2, 2200),
}
}
func syntheticKubernetesPodMetrics(cluster models.KubernetesCluster, pod models.KubernetesPod) kubernetesPodSyntheticMetrics {
seed := hashMetricsSeed(
cluster.ID,
cluster.Name,
cluster.DisplayName,
pod.UID,
pod.Namespace,
pod.Name,
)
rng := rand.New(rand.NewSource(int64(seed)))
phase := strings.ToLower(strings.TrimSpace(pod.Phase))
totalContainers := len(pod.Containers)
if totalContainers <= 0 {
totalContainers = 1
}
readyContainers := 0
for _, container := range pod.Containers {
if container.Ready {
readyContainers++
}
}
readiness := float64(readyContainers) / float64(totalContainers)
if readiness <= 0 && phase == "running" {
readiness = 0.35
}
restarts := float64(pod.Restarts)
if restarts < 0 {
restarts = 0
}
restartFactor := math.Min(restarts*1.6, 16)
cpu := 0.0
memory := 0.0
disk := 0.0
netIn := 0.0
netOut := 0.0
diskRead := 0.0
diskWrite := 0.0
switch phase {
case "running":
cpu = 7 + readiness*52 + rng.Float64()*14 - restartFactor*0.35
memory = 26 + readiness*46 + rng.Float64()*14 + restartFactor*0.25
disk = 17 + readiness*42 + rng.Float64()*16
netIn = 14 + readiness*220 + rng.Float64()*70 + restarts*2
netOut = 10 + readiness*180 + rng.Float64()*55 + restarts*1.6
diskRead = 8 + readiness*75 + rng.Float64()*32 + restarts*1.8
diskWrite = 6 + readiness*68 + rng.Float64()*28 + restarts*1.6
case "pending":
cpu = 2 + rng.Float64()*7
memory = 14 + rng.Float64()*16
disk = 8 + rng.Float64()*14
netIn = 1 + rng.Float64()*10
netOut = 1 + rng.Float64()*8
diskRead = 1 + rng.Float64()*8
diskWrite = 1 + rng.Float64()*8
case "failed", "unknown":
cpu = 1 + rng.Float64()*8
memory = 9 + rng.Float64()*15 + restartFactor*0.5
disk = 7 + rng.Float64()*16
netIn = 1 + rng.Float64()*14 + restarts*1.4
netOut = 1 + rng.Float64()*11 + restarts*1.2
diskRead = 1 + rng.Float64()*10 + restarts*1.2
diskWrite = 1 + rng.Float64()*10 + restarts*1.1
default:
cpu = 1 + rng.Float64()*6
memory = 8 + rng.Float64()*12
disk = 6 + rng.Float64()*12
netIn = 1 + rng.Float64()*7
netOut = 1 + rng.Float64()*6
diskRead = 1 + rng.Float64()*6
diskWrite = 1 + rng.Float64()*6
}
if totalContainers > 1 {
multi := 1 + math.Min(float64(totalContainers-1)*0.08, 0.6)
cpu *= multi
memory *= 1 + math.Min(float64(totalContainers-1)*0.1, 0.8)
disk *= 1 + math.Min(float64(totalContainers-1)*0.07, 0.5)
netIn *= 1 + math.Min(float64(totalContainers-1)*0.09, 0.7)
netOut *= 1 + math.Min(float64(totalContainers-1)*0.08, 0.65)
diskRead *= 1 + math.Min(float64(totalContainers-1)*0.11, 0.8)
diskWrite *= 1 + math.Min(float64(totalContainers-1)*0.1, 0.7)
}
return kubernetesPodSyntheticMetrics{
CPU: clampMetricValue(cpu, 0, 100),
Memory: clampMetricValue(memory, 0, 100),
Disk: clampMetricValue(disk, 0, 100),
NetIn: clampMetricValue(netIn, 0, math.Max(1800, netIn+50)),
NetOut: clampMetricValue(netOut, 0, math.Max(1700, netOut+40)),
DiskRead: clampMetricValue(diskRead, 0, math.Max(1200, diskRead+30)),
DiskWrite: clampMetricValue(diskWrite, 0, math.Max(1200, diskWrite+30)),
}
}
func hashMetricsSeed(parts ...string) uint64 {
h := fnv.New64a()
for _, p := range parts {
_, _ = h.Write([]byte(p))
_, _ = h.Write([]byte{0})
}
return h.Sum64()
}
func clampMetricValue(value, min, max float64) float64 {
if math.IsNaN(value) || math.IsInf(value, 0) {
return min
}
if value < min {
return min
}
if value > max {
return max
}
return value
}
func percentFromUsage(value float64) float64 {
if value <= 1.0 {
return value * 100
}
return value
}
func percentFromReportedPercent(value float64) float64 {
return clampMetricValue(value, 0, 100)
}
func setNetworkAndDiskIOMetricsHost(metrics *ResourceMetrics, netIn, netOut, diskRead, diskWrite float64, source DataSource) {
if netIn > 0 {
metrics.NetIn = &MetricValue{Value: netIn, Unit: "bytes/s", Source: source}
}
if netOut > 0 {
metrics.NetOut = &MetricValue{Value: netOut, Unit: "bytes/s", Source: source}
}
if diskRead > 0 {
metrics.DiskRead = &MetricValue{Value: diskRead, Unit: "bytes/s", Source: source}
}
if diskWrite > 0 {
metrics.DiskWrite = &MetricValue{Value: diskWrite, Unit: "bytes/s", Source: source}
}
}
func setNetworkAndDiskIOMetricsVM(metrics *ResourceMetrics, netIn, netOut, diskRead, diskWrite int64, source DataSource) {
if netIn != 0 {
metrics.NetIn = &MetricValue{Value: float64(netIn), Unit: "bytes/s", Source: source}
}
if netOut != 0 {
metrics.NetOut = &MetricValue{Value: float64(netOut), Unit: "bytes/s", Source: source}
}
if diskRead != 0 {
metrics.DiskRead = &MetricValue{Value: float64(diskRead), Unit: "bytes/s", Source: source}
}
if diskWrite != 0 {
metrics.DiskWrite = &MetricValue{Value: float64(diskWrite), Unit: "bytes/s", Source: source}
}
}