aboutsummaryrefslogtreecommitdiff
path: root/backend/internal/monitor/worker.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/internal/monitor/worker.go')
-rw-r--r--backend/internal/monitor/worker.go148
1 files changed, 148 insertions, 0 deletions
diff --git a/backend/internal/monitor/worker.go b/backend/internal/monitor/worker.go
new file mode 100644
index 0000000..0cf980a
--- /dev/null
+++ b/backend/internal/monitor/worker.go
@@ -0,0 +1,148 @@
+package monitor
+
+import (
+ "context"
+ "encoding/json"
+ "log"
+ "time"
+
+ "yaum/internal/alerts"
+ "yaum/internal/model"
+ "yaum/internal/sse"
+ "yaum/internal/store"
+)
+
+type checkResult struct {
+ Heartbeat model.Heartbeat
+ Service model.Service
+}
+
+type Worker struct {
+ store store.Store
+ interval time.Duration
+ timeout time.Duration
+ results chan checkResult
+ broadcaster *sse.Broadcaster
+ smtpCfg *alerts.SMTPConfig
+ quit chan struct{}
+}
+
+func NewWorker(s store.Store, interval, timeout time.Duration, b *sse.Broadcaster) *Worker {
+ return &Worker{
+ store: s,
+ interval: interval,
+ timeout: timeout,
+ results: make(chan checkResult, 100),
+ broadcaster: b,
+ quit: make(chan struct{}),
+ }
+}
+
+func (w *Worker) WithSMTP(cfg *alerts.SMTPConfig) *Worker {
+ w.smtpCfg = cfg
+ return w
+}
+
+func (w *Worker) Start(ctx context.Context) {
+ ticker := time.NewTicker(w.interval)
+ defer ticker.Stop()
+
+ log.Printf("[worker] iniciado | intervalo=%v timeout=%v", w.interval, w.timeout)
+
+ go w.resultConsumer()
+
+ w.runChecks(ctx)
+
+ for {
+ select {
+ case <-ticker.C:
+ w.runChecks(ctx)
+ case <-w.quit:
+ log.Println("[worker] encerrando...")
+ return
+ }
+ }
+}
+
+func (w *Worker) resultConsumer() {
+ for {
+ select {
+ case res := <-w.results:
+ hb := res.Heartbeat
+ svc := res.Service
+
+ w.checkTransition(svc, hb)
+
+ if err := w.store.AddHeartbeat(hb); err != nil {
+ log.Printf("[worker] erro ao salvar heartbeat: %v", err)
+ }
+
+ if w.broadcaster != nil {
+ data, err := json.Marshal(hb)
+ if err == nil {
+ w.broadcaster.Publish(data)
+ }
+ }
+
+ status := "UP"
+ if !hb.IsUp {
+ status = "DOWN"
+ }
+ log.Printf("[worker] service=%d status=%s code=%d time=%dms",
+ hb.ServiceID, status, hb.StatusCode, hb.ResponseTimeMs)
+ case <-w.quit:
+ return
+ }
+ }
+}
+
+func (w *Worker) checkTransition(svc model.Service, hb model.Heartbeat) {
+ prev, err := w.store.GetLastHeartbeat(svc.ID)
+ if err != nil || prev == nil {
+ return
+ }
+
+ if prev.IsUp == hb.IsUp {
+ return
+ }
+
+ log.Printf("[worker] transicao de status service=%d: %v -> %v", svc.ID, prev.IsUp, hb.IsUp)
+
+ w.fireAlerts(svc, hb, prev.IsUp)
+}
+
+func (w *Worker) fireAlerts(svc model.Service, hb model.Heartbeat, wasUp bool) {
+ if svc.DiscordWebhookURL != "" {
+ go alerts.SendDiscord(svc.DiscordWebhookURL, svc, hb, wasUp)
+ }
+ if svc.AlertEmail != "" && w.smtpCfg != nil {
+ go alerts.SendEmail(svc, hb, wasUp, w.smtpCfg)
+ }
+}
+
+func (w *Worker) runChecks(ctx context.Context) {
+ services, err := w.store.ListActiveServices()
+ if err != nil {
+ log.Printf("[worker] erro ao listar servicos ativos: %v", err)
+ return
+ }
+ log.Printf("[worker] verificando %d servico(s)", len(services))
+
+ for _, svc := range services {
+ go w.checkService(ctx, svc)
+ }
+}
+
+func (w *Worker) checkService(ctx context.Context, svc model.Service) {
+ checkCtx, cancel := context.WithTimeout(ctx, w.timeout)
+ defer cancel()
+
+ hb := CheckHTTP(checkCtx, svc.URL, svc.KeywordToFind)
+ hb.ServiceID = svc.ID
+
+ w.results <- checkResult{Heartbeat: hb, Service: svc}
+}
+
+func (w *Worker) Stop() {
+ close(w.quit)
+}