f344bb707b
Each alert carries a generatorURL pointing at the Prometheus rule that fired it. The server now parses that link, queries the same Prometheus for the rule's expression and draws it under the alert, with the current value beside it. The browser never talks to Prometheus, so the CSP stays at connect-src 'self'. The URL comes out of a webhook payload, so whoever holds an integration key chooses it. Querying it is therefore opt-in and bounded by an allowlist, TERDUT_PROMETHEUS_ALLOWED_HOSTS (names, "*" as one label, a leading "**" as several, IPs, CIDRs). The check runs on the address the dialer is about to use, a name that resolves to loopback or link-local is refused even when it matches, and redirects and proxy variables are ignored. A worker takes a snapshot when an incident opens and another when it closes, so the graph outlives Prometheus' retention. It finds its work in the database rather than being triggered from the webhook, so ingestion is untouched and a restart loses nothing. While an incident is open the metrics endpoint also queries live, cached for 15 seconds. The rule expression keeps its threshold, so Prometheus returns data only while the alert condition holds; "no data" for the current value means the condition cleared. Stripping the threshold needs a PromQL parser and is left out. Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
104 lines
2.9 KiB
Go
104 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"git.ryuvia.com/niklas/terdut-server/internal/api"
|
|
"git.ryuvia.com/niklas/terdut-server/internal/config"
|
|
"git.ryuvia.com/niklas/terdut-server/internal/db"
|
|
"git.ryuvia.com/niklas/terdut-server/internal/notify"
|
|
)
|
|
|
|
var version = "dev"
|
|
|
|
func main() {
|
|
cfg := config.Load()
|
|
if err := cfg.Validate(); err != nil {
|
|
log.Fatalf("config: %v", err)
|
|
}
|
|
|
|
database, err := db.Open(cfg.DSN)
|
|
if err != nil {
|
|
log.Fatalf("open db: %v", err)
|
|
}
|
|
defer database.Close()
|
|
|
|
if err := db.Migrate(database); err != nil {
|
|
log.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
notifyCfg := api.NotifyConfig{
|
|
Providers: notifyProviders(cfg),
|
|
Fallback: notifyFallback(cfg),
|
|
PublicURL: cfg.PublicURL,
|
|
RepeatEvery: cfg.NotifyRepeat,
|
|
}
|
|
|
|
// The behaviour knobs move into the database on first start, after which an
|
|
// administrator owns them and a redeploy leaves them alone.
|
|
if err := api.SeedSettings(context.Background(), database, cfg); err != nil {
|
|
log.Fatalf("seed settings: %v", err)
|
|
}
|
|
|
|
if err := api.SeedOperatorKey(context.Background(), database, cfg.OperatorKey); err != nil {
|
|
log.Fatalf("%v", err)
|
|
}
|
|
|
|
router := api.NewRouter(database, notifyCfg, cfg, version)
|
|
|
|
srv := &http.Server{
|
|
Addr: cfg.Addr,
|
|
Handler: router,
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 15 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
go api.StartArchiver(ctx, database, cfg.ArchiveAfter, cfg.StaleAfter, notifyCfg)
|
|
go api.StartNotifier(ctx, database, notifyCfg)
|
|
go api.StartMetricsWorker(ctx, database, cfg)
|
|
|
|
go func() {
|
|
log.Printf("terdut-server %s listening on %s", version, cfg.Addr)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("listen: %v", err)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
log.Println("shutting down")
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
|
log.Printf("shutdown: %v", err)
|
|
}
|
|
}
|
|
|
|
// notifyProviders registers the notification providers this install has
|
|
// configuration for. A provider with no configuration is simply absent, which is
|
|
// what keeps it out of the web UI's list of channel types.
|
|
func notifyProviders(cfg config.Config) *notify.Registry {
|
|
var providers []notify.Provider
|
|
if cfg.NtfyURL != "" {
|
|
providers = append(providers, notify.NewNtfy(cfg.NtfyURL, cfg.NtfyToken))
|
|
}
|
|
return notify.NewRegistry(providers...)
|
|
}
|
|
|
|
// notifyFallback is where an incident goes when nobody on call can be reached.
|
|
// Only ntfy has a fallback setting today; a second provider adds its own.
|
|
func notifyFallback(cfg config.Config) notify.Target {
|
|
if cfg.NtfyURL != "" && cfg.NtfyFallbackTopic != "" {
|
|
return notify.Target{Type: notify.TypeNtfy, Address: cfg.NtfyFallbackTopic}
|
|
}
|
|
return notify.Target{}
|
|
}
|