package main import ( "crypto/sha256" "encoding/json" "fmt" "os" "path/filepath" "sort" "time" ) type State struct { LastRun time.Time `json:"last_run"` TemplateCommit string `json:"template_commit"` TemplateVersion string `json:"template_version"` ClientsHash string `json:"clients_hash"` ClientCount int `json:"client_count"` DomainCount int `json:"domain_count"` SIDs []string `json:"sids"` LastChangeReason string `json:"last_change_reason,omitempty"` AlertsHash string `json:"alerts_hash,omitempty"` // Хэш лимитов алертов из БД AlertTemplateHash string `json:"alert_template_hash,omitempty"` // Хэш файла alert_rules_template.json } type StateManager struct { stateFile string state *State } func NewStateManager(stateFile string) *StateManager { return &StateManager{ stateFile: stateFile, } } func (sm *StateManager) Load() error { data, err := os.ReadFile(sm.stateFile) if err != nil { if os.IsNotExist(err) { // First run - no state file sm.state = &State{} return nil } return fmt.Errorf("failed to read state file: %w", err) } sm.state = &State{} if err := json.Unmarshal(data, sm.state); err != nil { return fmt.Errorf("failed to parse state file: %w", err) } return nil } func (sm *StateManager) Save() error { sm.state.LastRun = time.Now() // Create directory if it doesn't exist dir := filepath.Dir(sm.stateFile) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create state directory: %w", err) } data, err := json.MarshalIndent(sm.state, "", " ") if err != nil { return fmt.Errorf("failed to marshal state: %w", err) } if err := os.WriteFile(sm.stateFile, data, 0644); err != nil { return fmt.Errorf("failed to write state file: %w", err) } return nil } func (sm *StateManager) CheckChanges(templateCommit, templateVersion string, clients map[string]ClientData) (bool, string) { if sm.state == nil { return true, "First run - no previous state" } reasons := []string{} // Check template commit if sm.state.TemplateCommit != templateCommit { reasons = append(reasons, fmt.Sprintf("Template commit changed: %s -> %s", truncateHash(sm.state.TemplateCommit), truncateHash(templateCommit))) } // Check template version if sm.state.TemplateVersion != templateVersion { reasons = append(reasons, fmt.Sprintf("Template version changed: %s -> %s", sm.state.TemplateVersion, templateVersion)) } // Calculate new hash newHash := calculateClientsHash(clients) if sm.state.ClientsHash != newHash { reasons = append(reasons, "Database content changed") // Get details about what changed newSIDs, removedSIDs, totalNew := compareSIDs(sm.state.SIDs, extractSIDs(clients)) if len(newSIDs) > 0 { reasons = append(reasons, fmt.Sprintf(" - New SIDs: %v", newSIDs)) } if len(removedSIDs) > 0 { reasons = append(reasons, fmt.Sprintf(" - Removed SIDs: %v", removedSIDs)) } if totalNew == 0 && len(newSIDs) == 0 && len(removedSIDs) == 0 { reasons = append(reasons, " - Data modified (domains, aliases, etc.)") } } // Check client count newClientCount := len(clients) if sm.state.ClientCount != newClientCount { reasons = append(reasons, fmt.Sprintf("Client count changed: %d -> %d", sm.state.ClientCount, newClientCount)) } // Check domain count newDomainCount := countDomains(clients) if sm.state.DomainCount != newDomainCount { reasons = append(reasons, fmt.Sprintf("Domain count changed: %d -> %d", sm.state.DomainCount, newDomainCount)) } hasChanges := len(reasons) > 0 changeReason := "" if hasChanges { changeReason = fmt.Sprintf("Changes detected:\n %s", joinReasons(reasons)) } return hasChanges, changeReason } func (sm *StateManager) Update(templateCommit, templateVersion string, clients map[string]ClientData, changeReason string) { if sm.state == nil { sm.state = &State{} } sm.state.TemplateCommit = templateCommit sm.state.TemplateVersion = templateVersion sm.state.ClientsHash = calculateClientsHash(clients) sm.state.ClientCount = len(clients) sm.state.DomainCount = countDomains(clients) sm.state.SIDs = extractSIDs(clients) sm.state.LastChangeReason = changeReason } func calculateClientsHash(clients map[string]ClientData) string { // Create a deterministic representation type hashData struct { ClientTitle string RPSLimit int Domains []struct { SID string DomainName string Aliases string } } var data []hashData for _, client := range clients { hd := hashData{ClientTitle: client.ClientTitle, RPSLimit: client.RPSLimit} for _, domain := range client.Domains { hd.Domains = append(hd.Domains, struct { SID string DomainName string Aliases string }{ SID: domain.SID, DomainName: domain.DomainName, Aliases: domain.Aliases, }) } // Sort domains for consistent hash sort.Slice(hd.Domains, func(i, j int) bool { return hd.Domains[i].SID < hd.Domains[j].SID }) data = append(data, hd) } // Sort by client title for consistent hash sort.Slice(data, func(i, j int) bool { return data[i].ClientTitle < data[j].ClientTitle }) // Calculate hash jsonData, _ := json.Marshal(data) hash := sha256.Sum256(jsonData) return fmt.Sprintf("%x", hash) } // calculateAlertsHash считает хэш только от данных нужных для алертов (client_title + rps_limit) func calculateAlertsHash(clients map[string]ClientData) string { type alertData struct { ClientTitle string RPSLimit int Limit4xx int Limit5xx int } var data []alertData for _, client := range clients { if client.RPSLimit > 0 || client.Limit4xx > 0 || client.Limit5xx > 0 { data = append(data, alertData{ ClientTitle: client.ClientTitle, RPSLimit: client.RPSLimit, Limit4xx: client.Limit4xx, Limit5xx: client.Limit5xx, }) } } sort.Slice(data, func(i, j int) bool { return data[i].ClientTitle < data[j].ClientTitle }) jsonData, _ := json.Marshal(data) hash := sha256.Sum256(jsonData) return fmt.Sprintf("%x", hash) } // AlertTemplateChanged проверяет изменился ли файл alert_rules_template.json func (sm *StateManager) AlertTemplateChanged(templateHash string) bool { if sm.state == nil { return true } return sm.state.AlertTemplateHash != templateHash } // UpdateAlertTemplateHash сохраняет хэш шаблона алертов func (sm *StateManager) UpdateAlertTemplateHash(templateHash string) { if sm.state == nil { sm.state = &State{} } sm.state.AlertTemplateHash = templateHash } // AlertsNeedUpdate проверяет изменился ли хэш алертов func (sm *StateManager) AlertsNeedUpdate(clients map[string]ClientData) bool { if sm.state == nil { return true } return sm.state.AlertsHash != calculateAlertsHash(clients) } // UpdateAlertsHash сохраняет хэш алертов в state func (sm *StateManager) UpdateAlertsHash(clients map[string]ClientData) { if sm.state == nil { sm.state = &State{} } sm.state.AlertsHash = calculateAlertsHash(clients) } func extractSIDs(clients map[string]ClientData) []string { sidMap := make(map[string]bool) for _, client := range clients { for _, domain := range client.Domains { sidMap[domain.SID] = true } } sids := make([]string, 0, len(sidMap)) for sid := range sidMap { sids = append(sids, sid) } sort.Strings(sids) return sids } func compareSIDs(oldSIDs, newSIDs []string) (added, removed []string, total int) { oldMap := make(map[string]bool) for _, sid := range oldSIDs { oldMap[sid] = true } newMap := make(map[string]bool) for _, sid := range newSIDs { newMap[sid] = true if !oldMap[sid] { added = append(added, sid) } } for _, sid := range oldSIDs { if !newMap[sid] { removed = append(removed, sid) } } total = len(added) + len(removed) return } func countDomains(clients map[string]ClientData) int { count := 0 for _, client := range clients { count += len(client.Domains) } return count } func joinReasons(reasons []string) string { result := "" for i, reason := range reasons { if i > 0 { result += "\n " } result += reason } return result } func (sm *StateManager) GetLastRun() time.Time { if sm.state == nil { return time.Time{} } return sm.state.LastRun } func (sm *StateManager) GetStats() (clientCount, domainCount, sidCount int) { if sm.state == nil { return 0, 0, 0 } return sm.state.ClientCount, sm.state.DomainCount, len(sm.state.SIDs) }