package monitor import ( "fmt" "io" "log" "net/http" "strings" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/config" ) // Pinger sends health check pings to a Healthchecks.io-compatible server. type Pinger struct { baseURL string httpClient *http.Client logger *log.Logger enabled bool } // NewPinger creates a new Pinger from monitoring config. func NewPinger(cfg *config.MonitoringConfig, logger *log.Logger) *Pinger { return &Pinger{ baseURL: strings.TrimRight(cfg.HealthchecksBase, "/"), httpClient: &http.Client{ Timeout: 10 * time.Second, }, logger: logger, enabled: cfg.Enabled, } } // Ping sends a success signal with optional diagnostic body. func (p *Pinger) Ping(uuid string, body string) error { return p.send(uuid, "", body) } // Fail sends a failure signal with diagnostic body. func (p *Pinger) Fail(uuid string, body string) error { return p.send(uuid, "/fail", body) } // Start sends a "job started" signal (for duration tracking). func (p *Pinger) Start(uuid string) error { return p.send(uuid, "/start", "") } func (p *Pinger) send(uuid, suffix, body string) error { if !p.enabled { return nil } if uuid == "" || strings.HasPrefix(uuid, "CHANGEME") { return nil } url := fmt.Sprintf("%s/ping/%s%s", p.baseURL, uuid, suffix) var lastErr error for attempt := 0; attempt < 3; attempt++ { if attempt > 0 { time.Sleep(2 * time.Second) } var bodyReader io.Reader if body != "" { bodyReader = strings.NewReader(body) } req, err := http.NewRequest(http.MethodPost, url, bodyReader) if err != nil { lastErr = err continue } resp, err := p.httpClient.Do(req) if err != nil { lastErr = err continue } resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil } lastErr = fmt.Errorf("HTTP %d", resp.StatusCode) } p.logger.Printf("[WARN] Health ping failed after 3 attempts (%s): %v", uuid, lastErr) return nil // Never let ping failures affect the caller }