feat(bell): add alert acknowledgement vertical slice
Harness governance / validate (pull_request) Has been cancelled
Harness governance / validate (pull_request) Has been cancelled
This commit is contained in:
+123
-15
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -19,23 +20,32 @@ import (
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
|
||||
"yovision/bell/contracts"
|
||||
"yovision/bell/internal/alert"
|
||||
"yovision/bell/internal/audit"
|
||||
"yovision/bell/internal/event"
|
||||
"yovision/bell/internal/ingress"
|
||||
"yovision/bell/internal/store"
|
||||
bellweb "yovision/bell/web"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
type configuration struct {
|
||||
address string
|
||||
dsn string
|
||||
keyFile string
|
||||
tlsCert string
|
||||
tlsKey string
|
||||
eventIngressEnabled bool
|
||||
eventKeyFile string
|
||||
forbiddenNamesFile string
|
||||
address string
|
||||
dsn string
|
||||
keyFile string
|
||||
tlsCert string
|
||||
tlsKey string
|
||||
eventIngressEnabled bool
|
||||
eventKeyFile string
|
||||
forbiddenNamesFile string
|
||||
alertsEnabled bool
|
||||
alertRulesFile string
|
||||
alertConsoleEnabled bool
|
||||
alertConsoleTokenFile string
|
||||
alertConsoleTenantID int64
|
||||
alertConsoleSiteID int64
|
||||
alertConsoleActorRef string
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -48,13 +58,16 @@ func main() {
|
||||
|
||||
func loadConfiguration() (configuration, error) {
|
||||
value := configuration{
|
||||
address: envOr("BELL_HTTP_ADDR", "127.0.0.1:8081"),
|
||||
dsn: os.Getenv("BELL_DB_DSN"),
|
||||
keyFile: os.Getenv("BELL_AUDIT_KEYS_FILE"),
|
||||
tlsCert: os.Getenv("BELL_TLS_CERT_FILE"),
|
||||
tlsKey: os.Getenv("BELL_TLS_KEY_FILE"),
|
||||
eventKeyFile: os.Getenv("BELL_EVENT_INGRESS_KEYS_FILE"),
|
||||
forbiddenNamesFile: os.Getenv("BELL_EVIDENCE_FORBIDDEN_NAMES_FILE"),
|
||||
address: envOr("BELL_HTTP_ADDR", "127.0.0.1:8081"),
|
||||
dsn: os.Getenv("BELL_DB_DSN"),
|
||||
keyFile: os.Getenv("BELL_AUDIT_KEYS_FILE"),
|
||||
tlsCert: os.Getenv("BELL_TLS_CERT_FILE"),
|
||||
tlsKey: os.Getenv("BELL_TLS_KEY_FILE"),
|
||||
eventKeyFile: os.Getenv("BELL_EVENT_INGRESS_KEYS_FILE"),
|
||||
forbiddenNamesFile: os.Getenv("BELL_EVIDENCE_FORBIDDEN_NAMES_FILE"),
|
||||
alertRulesFile: os.Getenv("BELL_ALERT_RULES_FILE"),
|
||||
alertConsoleTokenFile: os.Getenv("BELL_ALERT_CONSOLE_TOKEN_FILE"),
|
||||
alertConsoleActorRef: os.Getenv("BELL_ALERT_CONSOLE_ACTOR_REF"),
|
||||
}
|
||||
switch os.Getenv("BELL_EVENT_INGRESS_ENABLED") {
|
||||
case "", "false":
|
||||
@@ -89,6 +102,36 @@ func loadConfiguration() (configuration, error) {
|
||||
return configuration{}, errors.New("BELL_EVIDENCE_FORBIDDEN_NAMES_FILE must be an absolute external path when event ingress is enabled")
|
||||
}
|
||||
}
|
||||
value.alertsEnabled, err = strictBoolEnv("BELL_ALERTS_ENABLED")
|
||||
if err != nil {
|
||||
return configuration{}, err
|
||||
}
|
||||
value.alertConsoleEnabled, err = strictBoolEnv("BELL_ALERT_CONSOLE_ENABLED")
|
||||
if err != nil {
|
||||
return configuration{}, err
|
||||
}
|
||||
if value.alertsEnabled && (value.alertRulesFile == "" || !filepath.IsAbs(value.alertRulesFile)) {
|
||||
return configuration{}, errors.New("BELL_ALERT_RULES_FILE must be an absolute external path when alerts are enabled")
|
||||
}
|
||||
if value.alertConsoleEnabled {
|
||||
if !value.alertsEnabled || !loopback || os.Getenv("BELL_HTTP_ADDR") == "" {
|
||||
return configuration{}, errors.New("Bell alert console requires alerts and an explicit loopback bind")
|
||||
}
|
||||
if value.alertConsoleTokenFile == "" || !filepath.IsAbs(value.alertConsoleTokenFile) {
|
||||
return configuration{}, errors.New("BELL_ALERT_CONSOLE_TOKEN_FILE must be an absolute external path")
|
||||
}
|
||||
value.alertConsoleTenantID, err = positiveEnv("BELL_ALERT_CONSOLE_TENANT_ID")
|
||||
if err != nil {
|
||||
return configuration{}, err
|
||||
}
|
||||
value.alertConsoleSiteID, err = positiveEnv("BELL_ALERT_CONSOLE_SITE_ID")
|
||||
if err != nil {
|
||||
return configuration{}, err
|
||||
}
|
||||
if value.alertConsoleActorRef == "" {
|
||||
return configuration{}, errors.New("BELL_ALERT_CONSOLE_ACTOR_REF is required")
|
||||
}
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
@@ -156,6 +199,34 @@ func run(logger *slog.Logger) error {
|
||||
}
|
||||
mux.Handle(ingress.Path, eventHandler)
|
||||
}
|
||||
if cfg.alertsEnabled {
|
||||
if err := repository.AlertReady(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
rules, err := alert.LoadRules(cfg.alertRulesFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := alert.Publish(ctx, repository, rules); err != nil {
|
||||
return err
|
||||
}
|
||||
go alert.RunWorker(ctx, repository, func(workerErr error) {
|
||||
logger.Error("Bell alert worker retrying", "error", workerErr)
|
||||
})
|
||||
}
|
||||
if cfg.alertConsoleEnabled {
|
||||
token, err := loadConsoleToken(cfg.alertConsoleTokenFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
console, err := bellweb.NewHandler(repository, bellweb.Config{
|
||||
Token: token, TenantID: cfg.alertConsoleTenantID, SiteID: cfg.alertConsoleSiteID, ActorRef: cfg.alertConsoleActorRef,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
console.Register(mux)
|
||||
}
|
||||
mux.HandleFunc("GET /healthz", func(writer http.ResponseWriter, _ *http.Request) {
|
||||
writeStatus(writer, http.StatusOK, "ok")
|
||||
})
|
||||
@@ -170,6 +241,12 @@ func run(logger *slog.Logger) error {
|
||||
return
|
||||
}
|
||||
}
|
||||
if cfg.alertsEnabled {
|
||||
if err := repository.AlertReady(request.Context()); err != nil {
|
||||
writeStatus(writer, http.StatusServiceUnavailable, "not_ready")
|
||||
return
|
||||
}
|
||||
}
|
||||
writeStatus(writer, http.StatusOK, "ready")
|
||||
})
|
||||
server := &http.Server{Addr: cfg.address, Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}}
|
||||
@@ -225,3 +302,34 @@ func envOr(name, fallback string) string {
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func strictBoolEnv(name string) (bool, error) {
|
||||
switch os.Getenv(name) {
|
||||
case "", "false":
|
||||
return false, nil
|
||||
case "true":
|
||||
return true, nil
|
||||
default:
|
||||
return false, fmt.Errorf("%s must be true or false", name)
|
||||
}
|
||||
}
|
||||
|
||||
func positiveEnv(name string) (int64, error) {
|
||||
value, err := strconv.ParseInt(os.Getenv(name), 10, 64)
|
||||
if err != nil || value < 1 {
|
||||
return 0, fmt.Errorf("%s must be a positive integer", name)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func loadConsoleToken(path string) (string, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil || len(raw) > 4096 {
|
||||
return "", errors.New("read Bell alert console token file")
|
||||
}
|
||||
value := strings.TrimSpace(string(raw))
|
||||
if len(value) < 32 || len(value) > 256 || strings.ContainsAny(value, " \t\r\n") {
|
||||
return "", errors.New("Bell alert console token must contain 32 to 256 non-whitespace characters")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user