logs-analyzer/signoz/pkg/query-service/rules/maintenance.go

424 lines
14 KiB
Go
Raw Normal View History

2024-09-02 22:47:30 +03:00
package rules
import (
"database/sql/driver"
"encoding/json"
"slices"
"strings"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
)
var (
ErrMissingName = errors.New("missing name")
ErrMissingSchedule = errors.New("missing schedule")
ErrMissingTimezone = errors.New("missing timezone")
ErrMissingRepeatType = errors.New("missing repeat type")
ErrMissingDuration = errors.New("missing duration")
)
type PlannedMaintenance struct {
Id int64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
Schedule *Schedule `json:"schedule" db:"schedule"`
AlertIds *AlertIds `json:"alertIds" db:"alert_ids"`
CreatedAt time.Time `json:"createdAt" db:"created_at"`
CreatedBy string `json:"createdBy" db:"created_by"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
UpdatedBy string `json:"updatedBy" db:"updated_by"`
Status string `json:"status"`
Kind string `json:"kind"`
}
type AlertIds []string
func (a *AlertIds) Scan(src interface{}) error {
if data, ok := src.([]byte); ok {
return json.Unmarshal(data, a)
}
return nil
}
func (a *AlertIds) Value() (driver.Value, error) {
return json.Marshal(a)
}
type Schedule struct {
Timezone string `json:"timezone"`
StartTime time.Time `json:"startTime,omitempty"`
EndTime time.Time `json:"endTime,omitempty"`
Recurrence *Recurrence `json:"recurrence"`
}
func (s *Schedule) Scan(src interface{}) error {
if data, ok := src.([]byte); ok {
return json.Unmarshal(data, s)
}
return nil
}
func (s *Schedule) Value() (driver.Value, error) {
return json.Marshal(s)
}
type RepeatType string
const (
RepeatTypeDaily RepeatType = "daily"
RepeatTypeWeekly RepeatType = "weekly"
RepeatTypeMonthly RepeatType = "monthly"
)
type RepeatOn string
const (
RepeatOnSunday RepeatOn = "sunday"
RepeatOnMonday RepeatOn = "monday"
RepeatOnTuesday RepeatOn = "tuesday"
RepeatOnWednesday RepeatOn = "wednesday"
RepeatOnThursday RepeatOn = "thursday"
RepeatOnFriday RepeatOn = "friday"
RepeatOnSaturday RepeatOn = "saturday"
)
type Recurrence struct {
StartTime time.Time `json:"startTime"`
EndTime *time.Time `json:"endTime,omitempty"`
Duration Duration `json:"duration"`
RepeatType RepeatType `json:"repeatType"`
RepeatOn []RepeatOn `json:"repeatOn"`
}
func (r *Recurrence) Scan(src interface{}) error {
if data, ok := src.([]byte); ok {
return json.Unmarshal(data, r)
}
return nil
}
func (r *Recurrence) Value() (driver.Value, error) {
return json.Marshal(r)
}
func (s Schedule) MarshalJSON() ([]byte, error) {
loc, err := time.LoadLocation(s.Timezone)
if err != nil {
return nil, err
}
var startTime, endTime time.Time
if !s.StartTime.IsZero() {
startTime = time.Date(s.StartTime.Year(), s.StartTime.Month(), s.StartTime.Day(), s.StartTime.Hour(), s.StartTime.Minute(), s.StartTime.Second(), s.StartTime.Nanosecond(), loc)
}
if !s.EndTime.IsZero() {
endTime = time.Date(s.EndTime.Year(), s.EndTime.Month(), s.EndTime.Day(), s.EndTime.Hour(), s.EndTime.Minute(), s.EndTime.Second(), s.EndTime.Nanosecond(), loc)
}
var recurrence *Recurrence
if s.Recurrence != nil {
recStartTime := time.Date(s.Recurrence.StartTime.Year(), s.Recurrence.StartTime.Month(), s.Recurrence.StartTime.Day(), s.Recurrence.StartTime.Hour(), s.Recurrence.StartTime.Minute(), s.Recurrence.StartTime.Second(), s.Recurrence.StartTime.Nanosecond(), loc)
var recEndTime *time.Time
if s.Recurrence.EndTime != nil {
end := time.Date(s.Recurrence.EndTime.Year(), s.Recurrence.EndTime.Month(), s.Recurrence.EndTime.Day(), s.Recurrence.EndTime.Hour(), s.Recurrence.EndTime.Minute(), s.Recurrence.EndTime.Second(), s.Recurrence.EndTime.Nanosecond(), loc)
recEndTime = &end
}
recurrence = &Recurrence{
StartTime: recStartTime,
EndTime: recEndTime,
Duration: s.Recurrence.Duration,
RepeatType: s.Recurrence.RepeatType,
RepeatOn: s.Recurrence.RepeatOn,
}
}
return json.Marshal(&struct {
Timezone string `json:"timezone"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
Recurrence *Recurrence `json:"recurrence,omitempty"`
}{
Timezone: s.Timezone,
StartTime: startTime.Format(time.RFC3339),
EndTime: endTime.Format(time.RFC3339),
Recurrence: recurrence,
})
}
func (s *Schedule) UnmarshalJSON(data []byte) error {
aux := &struct {
Timezone string `json:"timezone"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
Recurrence *Recurrence `json:"recurrence,omitempty"`
}{}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
loc, err := time.LoadLocation(aux.Timezone)
if err != nil {
return err
}
var startTime time.Time
if aux.StartTime != "" {
startTime, err = time.Parse(time.RFC3339, aux.StartTime)
if err != nil {
return err
}
s.StartTime = time.Date(startTime.Year(), startTime.Month(), startTime.Day(), startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(), loc)
}
var endTime time.Time
if aux.EndTime != "" {
endTime, err = time.Parse(time.RFC3339, aux.EndTime)
if err != nil {
return err
}
s.EndTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), endTime.Hour(), endTime.Minute(), endTime.Second(), endTime.Nanosecond(), loc)
}
s.Timezone = aux.Timezone
if aux.Recurrence != nil {
recStartTime, err := time.Parse(time.RFC3339, aux.Recurrence.StartTime.Format(time.RFC3339))
if err != nil {
return err
}
var recEndTime *time.Time
if aux.Recurrence.EndTime != nil {
end, err := time.Parse(time.RFC3339, aux.Recurrence.EndTime.Format(time.RFC3339))
if err != nil {
return err
}
endConverted := time.Date(end.Year(), end.Month(), end.Day(), end.Hour(), end.Minute(), end.Second(), end.Nanosecond(), loc)
recEndTime = &endConverted
}
s.Recurrence = &Recurrence{
StartTime: time.Date(recStartTime.Year(), recStartTime.Month(), recStartTime.Day(), recStartTime.Hour(), recStartTime.Minute(), recStartTime.Second(), recStartTime.Nanosecond(), loc),
EndTime: recEndTime,
Duration: aux.Recurrence.Duration,
RepeatType: aux.Recurrence.RepeatType,
RepeatOn: aux.Recurrence.RepeatOn,
}
}
return nil
}
func (m *PlannedMaintenance) shouldSkip(ruleID string, now time.Time) bool {
found := false
if m.AlertIds != nil {
for _, alertID := range *m.AlertIds {
if alertID == ruleID {
found = true
break
}
}
}
// If no alert ids, then skip all alerts
if m.AlertIds == nil || len(*m.AlertIds) == 0 {
found = true
}
if found {
zap.L().Info("alert found in maintenance", zap.String("alert", ruleID), zap.Any("maintenance", m.Name))
// If alert is found, we check if it should be skipped based on the schedule
// If it should be skipped, we return true
// If it should not be skipped, we return false
// fixed schedule
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
// if the current time in the timezone is between the start and end time
loc, err := time.LoadLocation(m.Schedule.Timezone)
if err != nil {
zap.L().Error("Error loading location", zap.String("timezone", m.Schedule.Timezone), zap.Error(err))
return false
}
currentTime := now.In(loc)
zap.L().Info("checking fixed schedule", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", m.Schedule.StartTime), zap.Time("endTime", m.Schedule.EndTime))
if currentTime.After(m.Schedule.StartTime) && currentTime.Before(m.Schedule.EndTime) {
return true
}
}
// recurring schedule
if m.Schedule.Recurrence != nil {
zap.L().Info("evaluating recurrence schedule")
start := m.Schedule.Recurrence.StartTime
end := m.Schedule.Recurrence.StartTime.Add(time.Duration(m.Schedule.Recurrence.Duration))
// if the current time in the timezone is between the start and end time
loc, err := time.LoadLocation(m.Schedule.Timezone)
if err != nil {
zap.L().Error("Error loading location", zap.String("timezone", m.Schedule.Timezone), zap.Error(err))
return false
}
currentTime := now.In(loc)
zap.L().Info("checking recurring schedule", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", start), zap.Time("endTime", end))
// make sure the start time is not after the current time
if currentTime.Before(start.In(loc)) {
zap.L().Info("current time is before start time", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", start.In(loc)))
return false
}
var endTime time.Time
if m.Schedule.Recurrence.EndTime != nil {
endTime = *m.Schedule.Recurrence.EndTime
}
if !endTime.IsZero() && currentTime.After(endTime.In(loc)) {
zap.L().Info("current time is after end time", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("endTime", end.In(loc)))
return false
}
switch m.Schedule.Recurrence.RepeatType {
case RepeatTypeDaily:
// take the hours and minutes from the start time and add them to the current time
startTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), start.Hour(), start.Minute(), 0, 0, loc)
endTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), end.Hour(), end.Minute(), 0, 0, loc)
zap.L().Info("checking daily schedule", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", startTime), zap.Time("endTime", endTime))
if currentTime.After(startTime) && currentTime.Before(endTime) {
return true
}
case RepeatTypeWeekly:
// if the current time in the timezone is between the start and end time on the RepeatOn day
startTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), start.Hour(), start.Minute(), 0, 0, loc)
endTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), end.Hour(), end.Minute(), 0, 0, loc)
zap.L().Info("checking weekly schedule", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", startTime), zap.Time("endTime", endTime))
if currentTime.After(startTime) && currentTime.Before(endTime) {
if len(m.Schedule.Recurrence.RepeatOn) == 0 {
return true
} else if slices.Contains(m.Schedule.Recurrence.RepeatOn, RepeatOn(strings.ToLower(currentTime.Weekday().String()))) {
return true
}
}
case RepeatTypeMonthly:
// if the current time in the timezone is between the start and end time on the day of the current month
startTime := time.Date(currentTime.Year(), currentTime.Month(), start.Day(), start.Hour(), start.Minute(), 0, 0, loc)
endTime := time.Date(currentTime.Year(), currentTime.Month(), end.Day(), end.Hour(), end.Minute(), 0, 0, loc)
zap.L().Info("checking monthly schedule", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", startTime), zap.Time("endTime", endTime))
if currentTime.After(startTime) && currentTime.Before(endTime) && currentTime.Day() == start.Day() {
return true
}
}
}
}
// If alert is not found, we return false
return false
}
func (m *PlannedMaintenance) IsActive(now time.Time) bool {
ruleID := "maintenance"
if m.AlertIds != nil && len(*m.AlertIds) > 0 {
ruleID = (*m.AlertIds)[0]
}
return m.shouldSkip(ruleID, now)
}
func (m *PlannedMaintenance) IsUpcoming() bool {
now := time.Now().In(time.FixedZone(m.Schedule.Timezone, 0))
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
return now.Before(m.Schedule.StartTime)
}
if m.Schedule.Recurrence != nil {
return now.Before(m.Schedule.Recurrence.StartTime)
}
return false
}
func (m *PlannedMaintenance) IsRecurring() bool {
return m.Schedule.Recurrence != nil
}
func (m *PlannedMaintenance) Validate() error {
if m.Name == "" {
return ErrMissingName
}
if m.Schedule == nil {
return ErrMissingSchedule
}
if m.Schedule.Timezone == "" {
return ErrMissingTimezone
}
_, err := time.LoadLocation(m.Schedule.Timezone)
if err != nil {
return errors.New("invalid timezone")
}
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
if m.Schedule.StartTime.After(m.Schedule.EndTime) {
return errors.New("start time cannot be after end time")
}
}
if m.Schedule.Recurrence != nil {
if m.Schedule.Recurrence.RepeatType == "" {
return ErrMissingRepeatType
}
if m.Schedule.Recurrence.Duration == 0 {
return ErrMissingDuration
}
if m.Schedule.Recurrence.EndTime != nil && m.Schedule.Recurrence.EndTime.Before(m.Schedule.Recurrence.StartTime) {
return errors.New("end time cannot be before start time")
}
}
return nil
}
func (m PlannedMaintenance) MarshalJSON() ([]byte, error) {
now := time.Now().In(time.FixedZone(m.Schedule.Timezone, 0))
var status string
if m.IsActive(now) {
status = "active"
} else if m.IsUpcoming() {
status = "upcoming"
} else {
status = "expired"
}
var kind string
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() && m.Schedule.EndTime.After(m.Schedule.StartTime) {
kind = "fixed"
} else {
kind = "recurring"
}
return json.Marshal(struct {
Id int64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
Schedule *Schedule `json:"schedule" db:"schedule"`
AlertIds *AlertIds `json:"alertIds" db:"alert_ids"`
CreatedAt time.Time `json:"createdAt" db:"created_at"`
CreatedBy string `json:"createdBy" db:"created_by"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
UpdatedBy string `json:"updatedBy" db:"updated_by"`
Status string `json:"status"`
Kind string `json:"kind"`
}{
Id: m.Id,
Name: m.Name,
Description: m.Description,
Schedule: m.Schedule,
AlertIds: m.AlertIds,
CreatedAt: m.CreatedAt,
CreatedBy: m.CreatedBy,
UpdatedAt: m.UpdatedAt,
UpdatedBy: m.UpdatedBy,
Status: status,
Kind: kind,
})
}