- Remove old review reports (keep latest only) - Move docs/ to deploy/docs-backup/ - Move performance-testing/ to deploy/ - Clean up test output files - Organize root directory
259 lines
8.2 KiB
Go
259 lines
8.2 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"strings"
|
||
"time"
|
||
|
||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||
)
|
||
|
||
func (s *OpsService) ListAlertRules(ctx context.Context) ([]*OpsAlertRule, error) {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return []*OpsAlertRule{}, nil
|
||
}
|
||
return s.opsRepo.ListAlertRules(ctx)
|
||
}
|
||
|
||
func (s *OpsService) CreateAlertRule(ctx context.Context, rule *OpsAlertRule) (*OpsAlertRule, error) {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return nil, infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if rule == nil {
|
||
return nil, infraerrors.BadRequest("INVALID_RULE", "invalid rule")
|
||
}
|
||
|
||
created, err := s.opsRepo.CreateAlertRule(ctx, rule)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return created, nil
|
||
}
|
||
|
||
func (s *OpsService) UpdateAlertRule(ctx context.Context, rule *OpsAlertRule) (*OpsAlertRule, error) {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return nil, infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if rule == nil || rule.ID <= 0 {
|
||
return nil, infraerrors.BadRequest("INVALID_RULE", "invalid rule")
|
||
}
|
||
|
||
updated, err := s.opsRepo.UpdateAlertRule(ctx, rule)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return nil, infraerrors.NotFound("OPS_ALERT_RULE_NOT_FOUND", "alert rule not found")
|
||
}
|
||
return nil, err
|
||
}
|
||
return updated, nil
|
||
}
|
||
|
||
func (s *OpsService) DeleteAlertRule(ctx context.Context, id int64) error {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if id <= 0 {
|
||
return infraerrors.BadRequest("INVALID_RULE_ID", "invalid rule id")
|
||
}
|
||
if err := s.opsRepo.DeleteAlertRule(ctx, id); err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return infraerrors.NotFound("OPS_ALERT_RULE_NOT_FOUND", "alert rule not found")
|
||
}
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CreateExternalAlertEvent persists an alert event that originated from an external
|
||
// source (e.g. Prometheus / Alertmanager webhook).
|
||
//
|
||
// Unlike the native evaluator path, external events use RuleID=0 and do NOT
|
||
// trigger deduplication against existing active events — each webhook call
|
||
// creates a new row so that the ops dashboard surfaces the full Prometheus
|
||
// firing/resolved timeline faithfully.
|
||
//
|
||
// If monitoring is disabled this is a silent no-op (returns nil).
|
||
func (s *OpsService) CreateExternalAlertEvent(ctx context.Context, event *OpsAlertEvent) error {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
// Monitoring disabled – drop silently so the webhook handler always returns 2xx
|
||
// and Alertmanager does not back-off and spam retries.
|
||
return nil
|
||
}
|
||
if s.opsRepo == nil {
|
||
return nil
|
||
}
|
||
if event == nil {
|
||
return infraerrors.BadRequest("INVALID_EVENT", "event must not be nil")
|
||
}
|
||
|
||
_, err := s.opsRepo.CreateAlertEvent(ctx, event)
|
||
return err
|
||
}
|
||
|
||
func (s *OpsService) ListAlertEvents(ctx context.Context, filter *OpsAlertEventFilter) ([]*OpsAlertEvent, error) {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return []*OpsAlertEvent{}, nil
|
||
}
|
||
return s.opsRepo.ListAlertEvents(ctx, filter)
|
||
}
|
||
|
||
func (s *OpsService) GetAlertEventByID(ctx context.Context, eventID int64) (*OpsAlertEvent, error) {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return nil, infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if eventID <= 0 {
|
||
return nil, infraerrors.BadRequest("INVALID_EVENT_ID", "invalid event id")
|
||
}
|
||
ev, err := s.opsRepo.GetAlertEventByID(ctx, eventID)
|
||
if err != nil {
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return nil, infraerrors.NotFound("OPS_ALERT_EVENT_NOT_FOUND", "alert event not found")
|
||
}
|
||
return nil, err
|
||
}
|
||
if ev == nil {
|
||
return nil, infraerrors.NotFound("OPS_ALERT_EVENT_NOT_FOUND", "alert event not found")
|
||
}
|
||
return ev, nil
|
||
}
|
||
|
||
func (s *OpsService) GetActiveAlertEvent(ctx context.Context, ruleID int64) (*OpsAlertEvent, error) {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return nil, infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if ruleID <= 0 {
|
||
return nil, infraerrors.BadRequest("INVALID_RULE_ID", "invalid rule id")
|
||
}
|
||
return s.opsRepo.GetActiveAlertEvent(ctx, ruleID)
|
||
}
|
||
|
||
func (s *OpsService) CreateAlertSilence(ctx context.Context, input *OpsAlertSilence) (*OpsAlertSilence, error) {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return nil, infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if input == nil {
|
||
return nil, infraerrors.BadRequest("INVALID_SILENCE", "invalid silence")
|
||
}
|
||
if input.RuleID <= 0 {
|
||
return nil, infraerrors.BadRequest("INVALID_RULE_ID", "invalid rule id")
|
||
}
|
||
if strings.TrimSpace(input.Platform) == "" {
|
||
return nil, infraerrors.BadRequest("INVALID_PLATFORM", "invalid platform")
|
||
}
|
||
if input.Until.IsZero() {
|
||
return nil, infraerrors.BadRequest("INVALID_UNTIL", "invalid until")
|
||
}
|
||
|
||
created, err := s.opsRepo.CreateAlertSilence(ctx, input)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return created, nil
|
||
}
|
||
|
||
func (s *OpsService) IsAlertSilenced(ctx context.Context, ruleID int64, platform string, groupID *int64, region *string, now time.Time) (bool, error) {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return false, err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return false, infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if ruleID <= 0 {
|
||
return false, infraerrors.BadRequest("INVALID_RULE_ID", "invalid rule id")
|
||
}
|
||
if strings.TrimSpace(platform) == "" {
|
||
return false, nil
|
||
}
|
||
return s.opsRepo.IsAlertSilenced(ctx, ruleID, platform, groupID, region, now)
|
||
}
|
||
|
||
func (s *OpsService) GetLatestAlertEvent(ctx context.Context, ruleID int64) (*OpsAlertEvent, error) {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return nil, infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if ruleID <= 0 {
|
||
return nil, infraerrors.BadRequest("INVALID_RULE_ID", "invalid rule id")
|
||
}
|
||
return s.opsRepo.GetLatestAlertEvent(ctx, ruleID)
|
||
}
|
||
|
||
func (s *OpsService) CreateAlertEvent(ctx context.Context, event *OpsAlertEvent) (*OpsAlertEvent, error) {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return nil, infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if event == nil {
|
||
return nil, infraerrors.BadRequest("INVALID_EVENT", "invalid event")
|
||
}
|
||
|
||
created, err := s.opsRepo.CreateAlertEvent(ctx, event)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return created, nil
|
||
}
|
||
|
||
func (s *OpsService) UpdateAlertEventStatus(ctx context.Context, eventID int64, status string, resolvedAt *time.Time) error {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if eventID <= 0 {
|
||
return infraerrors.BadRequest("INVALID_EVENT_ID", "invalid event id")
|
||
}
|
||
status = strings.TrimSpace(status)
|
||
if status == "" {
|
||
return infraerrors.BadRequest("INVALID_STATUS", "invalid status")
|
||
}
|
||
if status != OpsAlertStatusResolved && status != OpsAlertStatusManualResolved {
|
||
return infraerrors.BadRequest("INVALID_STATUS", "invalid status")
|
||
}
|
||
return s.opsRepo.UpdateAlertEventStatus(ctx, eventID, status, resolvedAt)
|
||
}
|
||
|
||
func (s *OpsService) UpdateAlertEventEmailSent(ctx context.Context, eventID int64, emailSent bool) error {
|
||
if err := s.RequireMonitoringEnabled(ctx); err != nil {
|
||
return err
|
||
}
|
||
if s.opsRepo == nil {
|
||
return infraerrors.ServiceUnavailable("OPS_REPO_UNAVAILABLE", "Ops repository not available")
|
||
}
|
||
if eventID <= 0 {
|
||
return infraerrors.BadRequest("INVALID_EVENT_ID", "invalid event id")
|
||
}
|
||
return s.opsRepo.UpdateAlertEventEmailSent(ctx, eventID, emailSent)
|
||
}
|