vault1984-dashboard/agent/main.go

211 lines
4.6 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
// Config — override via env vars
var (
nodeID = getenv("NODE_ID", "unknown")
hqURL = getenv("HQ_URL", "http://185.218.204.47:8080")
interval = 60 * time.Second
)
func getenv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
// ---- /proc readers ----
func cpuPercent() float64 {
// Read two samples 500ms apart
s1 := readCPUStat()
time.Sleep(500 * time.Millisecond)
s2 := readCPUStat()
idle1 := s1[3]
idle2 := s2[3]
total1, total2 := sum(s1), sum(s2)
totalDiff := total2 - total1
idleDiff := idle2 - idle1
if totalDiff == 0 {
return 0
}
return float64(totalDiff-idleDiff) / float64(totalDiff)
}
func readCPUStat() []uint64 {
data, err := os.ReadFile("/proc/stat")
if err != nil {
return make([]uint64, 10)
}
for _, line := range strings.Split(string(data), "\n") {
if !strings.HasPrefix(line, "cpu ") {
continue
}
fields := strings.Fields(line)[1:] // skip "cpu"
vals := make([]uint64, len(fields))
for i, f := range fields {
vals[i], _ = strconv.ParseUint(f, 10, 64)
}
return vals
}
return make([]uint64, 10)
}
func sum(vals []uint64) uint64 {
var t uint64
for _, v := range vals {
t += v
}
return t
}
func memPercent() float64 {
data, err := os.ReadFile("/proc/meminfo")
if err != nil {
return 0
}
kv := map[string]uint64{}
for _, line := range strings.Split(string(data), "\n") {
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
key := strings.TrimSuffix(parts[0], ":")
val, _ := strconv.ParseUint(parts[1], 10, 64)
kv[key] = val
}
total := kv["MemTotal"]
avail := kv["MemAvailable"]
if total == 0 {
return 0
}
return float64(total-avail) / float64(total)
}
func diskPercent() float64 {
// Use /proc/mounts + statfs-equivalent via df output isn't available
// Read from /proc/diskstats isn't straightforward for percent
// Use /sys/fs (fallback: read statvfs via syscall)
// Simple approach: parse `df /` output via reading /proc/mounts
// Actually the cleanest without CGo: read /proc/self/mountstats or use syscall.Statfs
// We'll use a simple read of /proc/mounts and pick root
// For a CGo-free approach, parse /proc/diskstats for read/write rates instead
data, err := os.ReadFile("/proc/diskstats")
if err != nil {
return 0
}
// Just return a simple proxy: if disk stats exist, device is healthy
_ = data
// Real disk % requires syscall.Statfs — we'll do it inline
return diskUsage("/")
}
// ---- OTLP JSON builder ----
func buildOTLP(cpu, mem, disk float64) []byte {
now := fmt.Sprintf("%d", time.Now().UnixNano())
payload := map[string]interface{}{
"resourceMetrics": []interface{}{
map[string]interface{}{
"resource": map[string]interface{}{
"attributes": []interface{}{
attr("service.name", "vault1984-pop"),
attr("node.id", nodeID),
attr("host.name", hostname()),
},
},
"scopeMetrics": []interface{}{
map[string]interface{}{
"scope": map[string]interface{}{
"name": "vault1984-agent",
"version": "0.1",
},
"metrics": []interface{}{
gauge("system.cpu.utilization", cpu, now),
gauge("system.memory.utilization", mem, now),
gauge("system.disk.utilization", disk, now),
},
},
},
},
},
}
b, _ := json.Marshal(payload)
return b
}
func attr(k, v string) map[string]interface{} {
return map[string]interface{}{
"key": k,
"value": map[string]interface{}{"stringValue": v},
}
}
func gauge(name string, val float64, tsNano string) map[string]interface{} {
return map[string]interface{}{
"name": name,
"gauge": map[string]interface{}{
"dataPoints": []interface{}{
map[string]interface{}{
"timeUnixNano": tsNano,
"asDouble": val,
},
},
},
}
}
func hostname() string {
h, _ := os.Hostname()
return h
}
// ---- Push ----
func push(cpu, mem, disk float64) error {
body := buildOTLP(cpu, mem, disk)
// Push OTLP to HQ
resp, err := http.Post(hqURL+"/v1/metrics", "application/json", bytes.NewReader(body))
if err != nil {
return fmt.Errorf("OTLP push: %w", err)
}
resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("OTLP push status: %d", resp.StatusCode)
}
log.Printf("pushed: cpu=%.1f%% mem=%.1f%% disk=%.1f%%", cpu*100, mem*100, disk*100)
return nil
}
func main() {
log.Printf("=== vault1984-agent node=%s hq=%s interval=%s ===", nodeID, hqURL, interval)
for {
cpu := cpuPercent()
mem := memPercent()
disk := diskPercent()
if err := push(cpu, mem, disk); err != nil {
log.Printf("push error: %v", err)
}
time.Sleep(interval)
}
}