Files
og/cmd/worker-telemetry/main.go
T
2026-09-11 06:14:38 +02:00

181 lines
5.6 KiB
Go

package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/example/ollama-fair-gateway/internal/hoststats"
)
type telemetry struct {
MemoryUsedBytes int64 `json:"memory_used_bytes,omitempty"`
MemoryTotalBytes int64 `json:"memory_total_bytes,omitempty"`
VRAMUsedBytes int64 `json:"vram_used_bytes,omitempty"`
VRAMTotalBytes int64 `json:"vram_total_bytes,omitempty"`
GPUUtilizationPct float64 `json:"gpu_utilization_percent,omitempty"`
GPUTemperatureC float64 `json:"gpu_temperature_c,omitempty"`
GPUPowerWatts float64 `json:"gpu_power_watts,omitempty"`
Source string `json:"source,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
Error string `json:"error,omitempty"`
}
type collector struct {
nvidia bool
nvidiaGPU string
amd bool
amdDevice string
}
func (c collector) collect(ctx context.Context) telemetry {
out := telemetry{UpdatedAt: time.Now().UTC()}
var errs []string
mctx, cancel := context.WithTimeout(ctx, 1500*time.Millisecond)
m, err := hoststats.ReadMemory(mctx)
cancel()
if err != nil {
errs = append(errs, "memory: "+err.Error())
} else {
out.MemoryTotalBytes, out.MemoryUsedBytes = m.TotalBytes, m.UsedBytes
out.Source = appendSource(out.Source, "host-memory")
}
if c.nvidia {
gctx, cancel := context.WithTimeout(ctx, 1500*time.Millisecond)
g, err := hoststats.ReadNVIDIA(gctx, c.nvidiaGPU)
cancel()
if err != nil {
errs = append(errs, "nvidia: "+err.Error())
} else {
out.VRAMUsedBytes, out.VRAMTotalBytes = g.MemoryUsedBytes, g.MemoryTotalBytes
out.GPUUtilizationPct, out.GPUTemperatureC, out.GPUPowerWatts = g.UtilizationPercent, g.TemperatureC, g.PowerWatts
out.Source = appendSource(out.Source, "nvidia-smi")
}
}
if c.amd {
gctx, cancel := context.WithTimeout(ctx, 1500*time.Millisecond)
g, err := hoststats.ReadAMD(gctx, c.amdDevice)
cancel()
if err != nil {
errs = append(errs, "amd: "+err.Error())
} else {
out.VRAMUsedBytes, out.VRAMTotalBytes = g.MemoryUsedBytes, g.MemoryTotalBytes
out.GPUUtilizationPct, out.GPUTemperatureC, out.GPUPowerWatts = g.UtilizationPercent, g.TemperatureC, g.PowerWatts
out.Source = appendSource(out.Source, "amdgpu-sysfs")
}
}
out.Error = strings.Join(errs, "; ")
return out
}
func appendSource(cur, next string) string {
if cur == "" {
return next
}
return cur + "+" + next
}
type cidrAllowlist struct{ nets []*net.IPNet }
func parseCIDRs(raw string) (cidrAllowlist, error) {
var out cidrAllowlist
for _, part := range strings.Split(raw, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
_, n, err := net.ParseCIDR(part)
if err != nil {
return out, fmt.Errorf("invalid allow CIDR %q: %w", part, err)
}
out.nets = append(out.nets, n)
}
if len(out.nets) == 0 {
return out, fmt.Errorf("at least one allow CIDR is required")
}
return out, nil
}
func (a cidrAllowlist) allowed(remote string) bool {
host, _, err := net.SplitHostPort(remote)
if err != nil {
host = remote
}
ip := net.ParseIP(strings.Trim(host, "[]"))
if ip == nil {
return false
}
for _, n := range a.nets {
if n.Contains(ip) {
return true
}
}
return false
}
func main() {
listen := flag.String("listen", "127.0.0.1:11500", "listen address")
path := flag.String("path", "/telemetry", "telemetry path")
allow := flag.String("allow-cidrs", "127.0.0.1/32,::1/128", "comma-separated client CIDRs allowed to read telemetry")
nvidia := flag.Bool("nvidia-smi", false, "collect NVIDIA telemetry with nvidia-smi")
nvidiaGPU := flag.String("nvidia-gpu", "", "optional nvidia-smi GPU selector")
amd := flag.Bool("amd-sysfs", false, "collect Linux AMDGPU telemetry from sysfs")
amdDevice := flag.String("amd-device", "", "optional AMDGPU device path such as /sys/class/drm/card0/device; empty auto-detects")
once := flag.Bool("once", false, "print one telemetry sample as JSON and exit")
flag.Parse()
if !strings.HasPrefix(*path, "/") {
log.Fatal("-path must begin with /")
}
acl, err := parseCIDRs(*allow)
if err != nil {
log.Fatal(err)
}
c := collector{nvidia: *nvidia, nvidiaGPU: *nvidiaGPU, amd: *amd, amdDevice: *amdDevice}
if *once {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(c.collect(context.Background())); err != nil {
log.Fatal(err)
}
return
}
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })
mux.HandleFunc(*path, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if !acl.allowed(r.RemoteAddr) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("X-Content-Type-Options", "nosniff")
_ = json.NewEncoder(w).Encode(c.collect(r.Context()))
})
srv := &http.Server{Addr: *listen, Handler: mux, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: time.Minute}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
<-ctx.Done()
shutdown, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_ = srv.Shutdown(shutdown)
}()
log.Printf("worker telemetry listening on http://%s%s allowed=%s nvidia=%t amd=%t", *listen, *path, *allow, *nvidia, *amd)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}