Files
incudal/agent/internal/report/report.go
T
2026-06-20 14:26:29 +08:00

299 lines
6.5 KiB
Go

package report
import (
"os"
"runtime"
"strconv"
"strings"
"syscall"
"time"
)
var incusSocketCandidates = []string{
"/var/lib/incus/unix.socket",
"/var/snap/incus/common/lxd/unix.socket",
"/var/lib/lxd/unix.socket",
}
func HeartbeatPayload(version string, heartbeatIntervalSeconds int) map[string]any {
return map[string]any{
"version": version,
"capabilities": []any{"heartbeat", "report", "host-metrics", "instance-status", "traffic-counters"},
"runtime": map[string]any{
"goos": runtime.GOOS,
"goarch": runtime.GOARCH,
},
"incus": detectIncus(),
"instances": collectIncusInstanceReport(),
"resources": collectResources(),
"metrics": collectMetrics(heartbeatIntervalSeconds),
}
}
func collectResources() map[string]any {
resources := map[string]any{
"cpuTotal": runtime.NumCPU(),
}
if cpuUsagePercent := readCPUUsagePercent(); cpuUsagePercent >= 0 {
resources["cpuUsagePercent"] = cpuUsagePercent
}
for key, value := range readMemoryStats() {
resources[key] = value
}
for key, value := range readDiskStats("/") {
resources[key] = value
}
if processCount := readProcessCount(); processCount >= 0 {
resources["processCount"] = processCount
}
return resources
}
func collectMetrics(heartbeatIntervalSeconds int) map[string]any {
switch {
case heartbeatIntervalSeconds <= 0:
heartbeatIntervalSeconds = 30
case heartbeatIntervalSeconds < 5:
heartbeatIntervalSeconds = 5
case heartbeatIntervalSeconds > 3600:
heartbeatIntervalSeconds = 3600
}
metrics := map[string]any{
"reportedAt": time.Now().UTC().Format(time.RFC3339),
"heartbeatIntervalSeconds": heartbeatIntervalSeconds,
}
if uptimeSeconds := readUptimeSeconds(); uptimeSeconds > 0 {
metrics["uptimeSeconds"] = uptimeSeconds
}
for key, value := range readLoadAverage() {
metrics[key] = value
}
return metrics
}
func detectIncus() map[string]any {
socketPath, ok := detectIncusSocket()
if ok {
return map[string]any{
"available": true,
"socket": socketPath,
}
}
return map[string]any{
"available": false,
"socket": "",
}
}
func detectIncusSocket() (string, bool) {
for _, socketPath := range incusSocketCandidates {
if info, err := os.Stat(socketPath); err == nil && !info.IsDir() {
return socketPath, true
}
}
return "", false
}
type cpuStat struct {
idle uint64
total uint64
}
func readCPUUsagePercent() float64 {
before, ok := readCPUStat()
if !ok {
return -1
}
time.Sleep(200 * time.Millisecond)
after, ok := readCPUStat()
if !ok {
return -1
}
totalDelta := after.total - before.total
idleDelta := after.idle - before.idle
if totalDelta == 0 || idleDelta > totalDelta {
return -1
}
return roundPercent(float64(totalDelta-idleDelta) / float64(totalDelta) * 100)
}
func readCPUStat() (cpuStat, bool) {
content, err := os.ReadFile("/proc/stat")
if err != nil {
return cpuStat{}, false
}
for _, line := range strings.Split(string(content), "\n") {
if !strings.HasPrefix(line, "cpu ") {
continue
}
fields := strings.Fields(line)
if len(fields) < 5 {
return cpuStat{}, false
}
var values []uint64
for _, field := range fields[1:] {
value, err := strconv.ParseUint(field, 10, 64)
if err != nil {
return cpuStat{}, false
}
values = append(values, value)
}
var total uint64
for _, value := range values {
total += value
}
idle := values[3]
if len(values) > 4 {
idle += values[4]
}
return cpuStat{idle: idle, total: total}, true
}
return cpuStat{}, false
}
func readMemoryStats() map[string]any {
meminfo := readMeminfoKB()
stats := map[string]any{}
memTotal := meminfo["MemTotal"]
memAvailable := meminfo["MemAvailable"]
if memTotal > 0 {
memUsed := memTotal - memAvailable
if memUsed < 0 {
memUsed = 0
}
stats["memoryTotalMb"] = memTotal / 1024
stats["memoryAvailableMb"] = memAvailable / 1024
stats["memoryUsedMb"] = memUsed / 1024
stats["memoryUsagePercent"] = roundPercent(float64(memUsed) / float64(memTotal) * 100)
}
swapTotal := meminfo["SwapTotal"]
swapFree := meminfo["SwapFree"]
if swapTotal > 0 {
swapUsed := swapTotal - swapFree
if swapUsed < 0 {
swapUsed = 0
}
stats["swapTotalMb"] = swapTotal / 1024
stats["swapUsedMb"] = swapUsed / 1024
stats["swapUsagePercent"] = roundPercent(float64(swapUsed) / float64(swapTotal) * 100)
} else {
stats["swapTotalMb"] = int64(0)
stats["swapUsedMb"] = int64(0)
stats["swapUsagePercent"] = float64(0)
}
return stats
}
func readMeminfoKB() map[string]int64 {
content, err := os.ReadFile("/proc/meminfo")
if err != nil {
return map[string]int64{}
}
values := map[string]int64{}
for _, line := range strings.Split(string(content), "\n") {
key, rest, ok := strings.Cut(line, ":")
if !ok {
continue
}
fields := strings.Fields(rest)
if len(fields) == 0 {
continue
}
kb, err := strconv.ParseInt(fields[0], 10, 64)
if err != nil {
continue
}
values[key] = kb
}
return values
}
func readDiskStats(path string) map[string]any {
var stat syscall.Statfs_t
if err := syscall.Statfs(path, &stat); err != nil {
return map[string]any{}
}
blockSize := uint64(stat.Bsize)
total := stat.Blocks * blockSize
free := stat.Bfree * blockSize
if total == 0 || free > total {
return map[string]any{}
}
used := total - free
return map[string]any{
"diskMountpoint": path,
"diskTotalBytes": total,
"diskUsedBytes": used,
"diskAvailableBytes": stat.Bavail * blockSize,
"diskUsagePercent": roundPercent(float64(used) / float64(total) * 100),
}
}
func readLoadAverage() map[string]any {
content, err := os.ReadFile("/proc/loadavg")
if err != nil {
return map[string]any{}
}
fields := strings.Fields(string(content))
if len(fields) < 3 {
return map[string]any{}
}
loads := map[string]any{}
keys := []string{"load1", "load5", "load15"}
for index, key := range keys {
value, err := strconv.ParseFloat(fields[index], 64)
if err == nil {
loads[key] = value
}
}
return loads
}
func readProcessCount() int {
entries, err := os.ReadDir("/proc")
if err != nil {
return -1
}
count := 0
for _, entry := range entries {
if !entry.IsDir() {
continue
}
if _, err := strconv.Atoi(entry.Name()); err == nil {
count++
}
}
return count
}
func readUptimeSeconds() int64 {
content, err := os.ReadFile("/proc/uptime")
if err != nil {
return 0
}
fields := strings.Fields(string(content))
if len(fields) == 0 {
return 0
}
value, err := strconv.ParseFloat(fields[0], 64)
if err != nil {
return 0
}
return int64(value)
}
func roundPercent(value float64) float64 {
return float64(int(value*10+0.5)) / 10
}