211 lines
4.6 KiB
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)
|
|
}
|
|
}
|