first commit
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user