feat(bell): add alert acknowledgement vertical slice
Harness governance / validate (pull_request) Has been cancelled

This commit is contained in:
QiuSW
2026-08-11 17:01:53 +08:00
parent 477afa6ba2
commit 8208118904
29 changed files with 1820 additions and 56 deletions
+123 -15
View File
@@ -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
}