package app import ( "context" "log" "time" "heartbeat/internal/alerts" "heartbeat/internal/config" "heartbeat/internal/discord" "heartbeat/internal/metrics" ) type Runner struct { cfg config.Config sampler *metrics.Sampler evaluator *alerts.Evaluator discord *discord.Client } func New(cfg config.Config) (*Runner, error) { return &Runner{ cfg: cfg, sampler: metrics.NewSampler(cfg.RequestTimeout), evaluator: alerts.NewEvaluator(), discord: discord.New(cfg.ServerName, cfg.DiscordWebhookURL, cfg.NotifyRoleID, cfg.RequestTimeout), }, nil } func (r *Runner) Run(ctx context.Context) error { if err := r.tick(ctx, true); err != nil { log.Printf("initial tick failed: %v", err) } sampleTicker := time.NewTicker(r.cfg.SampleInterval) defer sampleTicker.Stop() summaryTicker := time.NewTicker(r.cfg.SummaryInterval) defer summaryTicker.Stop() for { select { case <-ctx.Done(): return nil case <-sampleTicker.C: if err := r.tick(ctx, false); err != nil { log.Printf("sample tick failed: %v", err) } case <-summaryTicker.C: if err := r.sendSummary(ctx); err != nil { log.Printf("summary failed: %v", err) } } } } func (r *Runner) tick(ctx context.Context, sendSummary bool) error { sample, err := r.sampler.Collect(ctx, r.cfg) if err != nil { return err } if sendSummary { if err := r.discord.SendSummary(ctx, sample, r.cfg.SummaryInterval); err != nil { log.Printf("summary send failed: %v", err) } } for _, event := range r.evaluator.Evaluate(r.cfg, sample) { if err := r.discord.SendEvent(ctx, sample, event); err != nil { log.Printf("event send failed for %s: %v", event.Key, err) } } return nil } func (r *Runner) sendSummary(ctx context.Context) error { sample, err := r.sampler.Collect(ctx, r.cfg) if err != nil { return err } return r.discord.SendSummary(ctx, sample, r.cfg.SummaryInterval) }