internal/webhooks/webhook_service_alert_config.go
Rama main · réplica de lectura del repositorio original.
package webhooks
import (
"context"
"net/mail"
"github.com/google/uuid"
svcerrors "github.com/sarathsp06/sparrow/internal/errors"
"github.com/sarathsp06/sparrow/internal/storage"
"github.com/sarathsp06/sparrow/internal/tenant"
"github.com/sarathsp06/sparrow/internal/webhooks/store"
)
// systemEventTypes are the Sparrow-generated event types alert configs may
// subscribe to. Kept in sync with the emission call sites in
// internal/webhooks/queue/webhook_worker.go.
var systemEventTypes = map[string]bool{
"sparrow.webhook.health_changed": true,
"sparrow.webhook.delivery_failed": true,
"sparrow.webhook.disabled": true,
}
// CreateAlertConfig registers an email recipient for one or more Sparrow
// system event types, either for one webhook (webhookID set) or every
// webhook in the consumer (webhookID empty).
func (s *WebhookService) CreateAlertConfig(ctx context.Context, consumer, webhookID, email string, eventTypes []string) (*store.AlertConfig, error) {
if consumer == "" {
return nil, svcerrors.Error(svcerrors.InvalidArgument, "consumer is required")
}
if _, err := mail.ParseAddress(email); err != nil {
return nil, svcerrors.Errorf(svcerrors.InvalidArgument, "invalid email: %v", err)
}
if len(eventTypes) == 0 {
return nil, svcerrors.Error(svcerrors.InvalidArgument, "event_types must not be empty")
}
for _, et := range eventTypes {
if !systemEventTypes[et] {
return nil, svcerrors.Errorf(svcerrors.InvalidArgument, "unsupported event_type %q", et)
}
}
tenantID := tenant.FromContext(ctx)
cfg := &store.AlertConfig{
TenantID: tenantID,
Consumer: consumer,
Email: email,
EventTypes: eventTypes,
}
if webhookID != "" {
id, err := parseUUID(webhookID, "webhook ID")
if err != nil {
return nil, err
}
if _, err := s.webhookRepo.GetWebhookByID(ctx, tenantID, id, consumer); err != nil {
if storage.IsNotFound(err) {
return nil, svcerrors.Error(svcerrors.NotFound, "webhook not found in consumer")
}
return nil, err
}
cfg.WebhookID = &id
}
if err := s.webhookRepo.CreateAlertConfig(ctx, tenantID, cfg); err != nil {
return nil, err
}
return cfg, nil
}
// ListAlertConfigs lists alert configs in a consumer, optionally scoped to
// one webhook's own configs (webhookID != ""). Listing without a webhookID
// returns every config in the consumer, including consumer-wide ones.
func (s *WebhookService) ListAlertConfigs(ctx context.Context, consumer string, webhookID string) ([]*store.AlertConfig, error) {
if consumer == "" {
return nil, svcerrors.Error(svcerrors.InvalidArgument, "consumer is required")
}
var whPtr *uuid.UUID
if webhookID != "" {
id, err := parseUUID(webhookID, "webhook ID")
if err != nil {
return nil, err
}
whPtr = &id
}
return s.webhookRepo.ListAlertConfigs(ctx, tenant.FromContext(ctx), consumer, whPtr)
}
// DeleteAlertConfig deletes an alert config by id, verifying it belongs to
// the given consumer.
func (s *WebhookService) DeleteAlertConfig(ctx context.Context, consumer, id string) error {
if consumer == "" {
return svcerrors.Error(svcerrors.InvalidArgument, "consumer is required")
}
parsedID, err := parseUUID(id, "alert config ID")
if err != nil {
return err
}
tenantID := tenant.FromContext(ctx)
cfg, err := s.webhookRepo.GetAlertConfig(ctx, tenantID, parsedID)
if err != nil {
if storage.IsNotFound(err) {
return svcerrors.Error(svcerrors.NotFound, "alert config not found")
}
return err
}
if cfg.Consumer != consumer {
return svcerrors.Error(svcerrors.NotFound, "alert config not found in consumer")
}
return s.webhookRepo.DeleteAlertConfig(ctx, tenantID, parsedID)
}
// AlertDeliveryConfigured reports whether alert emails can actually go out:
// an active webhook under the _sparrow consumer with an unpaused subscription
// to Sparrow's system events. Without one, alert configs are recorded but
// nothing is sent.
func (s *WebhookService) AlertDeliveryConfigured(ctx context.Context) (bool, error) {
tenantID := tenant.FromContext(ctx)
hooks, err := s.webhookRepo.ListWebhooks(ctx, tenantID, tenant.SystemConsumer, "", true)
if err != nil {
return false, err
}
ids := make([]uuid.UUID, 0, len(hooks))
for _, h := range hooks {
ids = append(ids, h.ID)
}
subs, err := s.webhookRepo.ListSubscriptionsByWebhookIDs(ctx, tenantID, ids)
if err != nil {
return false, err
}
for _, sub := range subs {
if sub.PausedAt == nil && (sub.EventName == store.CatchAllEventName || systemEventTypes[sub.EventName]) {
return true, nil
}
}
return false, nil
}