package agent import ( "bytes" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) type Heartbeat struct { cfg Config nodeID string client *http.Client } type HeartbeatPayload struct { NodeID string `json:"node_id"` CPUPct float64 `json:"cpu_percent"` MemPct float64 `json:"memory_percent"` BandwidthMbps float64 `json:"bandwidth_mbps"` ActiveConnections int `json:"active_connections"` HlsReaders int `json:"hls_readers"` NetworkInMbps float64 `json:"network_in_mbps"` NetworkOutMbps float64 `json:"network_out_mbps"` ViewerCount int `json:"viewer_count"` FrameDrops int `json:"frame_drop_indicators"` PacketLoss int `json:"packet_loss_indicators"` MediaAvailable bool `json:"media_metrics_available"` } type RegisterPayload struct { NodeID string `json:"node_id"` Name string `json:"name"` Host string `json:"host"` HlsPort int `json:"hls_port"` ApiPort int `json:"api_port"` Weight int `json:"weight"` Subnet string `json:"subnet"` } func NewHeartbeat(cfg Config, nodeID string) *Heartbeat { return &Heartbeat{ cfg: cfg, nodeID: nodeID, client: &http.Client{Timeout: 10 * time.Second}, } } func (h *Heartbeat) Register() error { payload := RegisterPayload{ NodeID: h.nodeID, Name: h.cfg.NodeName, Host: h.cfg.OriginURL, HlsPort: h.cfg.HlsPort, ApiPort: h.cfg.ApiPort, Weight: h.cfg.Weight, Subnet: h.cfg.Subnet, } return h.post("/api/nodes/register", payload) } func (h *Heartbeat) Discover() error { if h.cfg.DiscoveryURL == "" { return nil } resp, err := h.client.Get(h.cfg.DiscoveryURL) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("discovery status %d", resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 4096)) if err != nil { return err } host := strings.TrimSpace(string(body)) if host == "" { return fmt.Errorf("discovery returned an empty address") } if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") { host = "http://" + host } h.cfg.OriginURL = strings.TrimRight(host, "/") return nil } func (h *Heartbeat) OriginURL() string { return h.cfg.OriginURL } func (h *Heartbeat) Send(m Metrics) error { payload := HeartbeatPayload{ NodeID: h.nodeID, CPUPct: m.CPU, MemPct: m.Memory, BandwidthMbps: m.Bandwidth, ActiveConnections: m.ActiveConns, HlsReaders: m.HlsReaders, NetworkInMbps: m.NetworkIn, NetworkOutMbps: m.NetworkOut, ViewerCount: m.ViewerCount, FrameDrops: m.FrameDrops, PacketLoss: m.PacketLoss, MediaAvailable: m.MediaAvailable, } return h.post("/api/nodes/heartbeat", payload) } func (h *Heartbeat) post(path string, payload interface{}) error { body, err := json.Marshal(payload) if err != nil { return err } url := h.cfg.OriginURL + path resp, err := h.client.Post(url, "application/json", bytes.NewReader(body)) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("status %d", resp.StatusCode) } return nil }