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) } }