package models import ( "encoding/json" "strings" "time" "unicode" "github.com/lib/pq" "gorm.io/datatypes" "gorm.io/gorm" ) // Check provides functionality. type Check struct { ID int64 `gorm:"primarykey" json:"id"` Enabled *bool `gorm:"not null;default:true" json:"enabled"` MonitorID int64 `gorm:"index;type:bigint REFERENCES monitors(id)" json:"monitor_id,omitempty"` Monitor *Monitor `json:"-"` Name *string `json:"name"` Kind string `json:"kind"` Interval int `json:"interval" validate:"required,gte=60"` // URL to monitor URL *string `json:"url,omitempty"` // Other settings of the check Settings datatypes.JSON `gorm:"not null;" json:"settings"` State string `gorm:"not null;default:'UNK'" json:"state"` LastStart *time.Time `json:"last_start"` LastEnd *time.Time `json:"last_end"` LastOk *time.Time `json:"last_ok"` LastFail *time.Time `json:"last_fail"` WasUp *time.Time `json:"was_up"` Fails int `json:"fails"` Expires *time.Time `json:"expires"` Error *string `json:"error"` Warnings pq.StringArray `gorm:"type:varchar(255)[]" json:"warnings"` Infos pq.StringArray `gorm:"type:varchar(255)[]" json:"infos"` // RequireQuorum enables multi-region result aggregation (Phase 3 of // docs/todo.md): when >1 the check's State is NOT written directly by // ApplyRemoteCheckResult — instead CheckRegionResult rows accumulate // until app/models/check_aggregator.go decides OK/ERR/DEGRADED. // Default 1 keeps the legacy single-region behavior unchanged. RequireQuorum int `gorm:"not null;default:1" json:"require_quorum"` // AggregationWindowSeconds is how long the aggregator waits for // regional CheckRegionResult rows before deciding the check's State. // Stored as int seconds (matching the existing GORM style — no // time.Duration columns) and exposed via AggregationWindow(). Default // 5s; ignored when RequireQuorum <= 1. AggregationWindowSeconds int `gorm:"not null;default:5" json:"aggregation_window_seconds"` IsNew bool `gorm:"-:all" sql:"-" json:"is_new,omitempty"` Deleted bool `gorm:"-:all" sql:"-" json:"deleted,omitempty"` Events []Event `json:"-" gorm:"many2many:event_checks;"` Audited } // ExpScope provides functionality. func ExpScope(q *gorm.DB) *gorm.DB { return q.Where("kind IN ('whois', 'ssl')"). Preload("Monitor"). Preload("Monitor.Group"). Preload("Monitor.Group.Notifications"). Preload("Monitor.Group.Notifications.Contacts"). Where("expires < ?", time.Now().Add(time.Hour*7*24)) } // IntervalOK provides functionality. func (c *Check) IntervalOK() bool { if c.Kind == kindRKN { return true } if c.Kind == kindWhois { return c.Interval >= 43200 } return c.Interval >= 30 } // GetLabel provides functionality. func (c *Check) GetLabel() string { if c.Name != nil { return *c.Name } if c.URL != nil { return *c.URL } return c.Kind } // KindLabel provides functionality. func (c *Check) KindLabel() string { if c.Kind == kindWhois { return "регистрация домена" } if c.Kind == kindSSL { return "SSL сертификат" } return c.Kind } // GetSettings provides functionality. func (c *Check) GetSettings() CheckSettings { d := CheckSettings{} err := json.Unmarshal(c.Settings, &d) if err != nil { panic(err) } return d } // ValidateSettings provides functionality. func (c *Check) ValidateSettings() error { if len(c.Settings) == 0 { c.Settings = []byte("{}") } return nil } // GetURL provides functionality. func (c *Check) GetURL() (string, error) { // return c.GetSettings()["url"].(string) if c.URL != nil { return *c.URL, nil } return "http://" + c.Monitor.Host, nil } // MetricName provides functionality. func (c *Check) MetricName() string { sanitized := strings.Map(func(r rune) rune { if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == ':' { return r } return '_' }, c.Kind) return "c" + sanitized } // QuorumEnabled reports whether this check should be aggregated by // app/models/check_aggregator.go. When false (RequireQuorum <= 1), // ApplyRemoteCheckResult keeps the legacy direct State update path. func (c *Check) QuorumEnabled() bool { return c.RequireQuorum > 1 } // AggregationWindow returns AggregationWindowSeconds as a time.Duration. // Defaults to 5s when the underlying int is zero/negative, mirroring the // GORM column default; callers can rely on a strictly positive value. func (c *Check) AggregationWindow() time.Duration { if c.AggregationWindowSeconds <= 0 { return 5 * time.Second } return time.Duration(c.AggregationWindowSeconds) * time.Second }