grafana_gen/state.go

337 lines
8.7 KiB
Go
Executable file

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 считает хэш от данных нужных для алертов (rps_limit + пороги per-SID)
func calculateAlertsHash(clients map[string]ClientData) string {
type domainAlert struct {
SID string
Limit4xx int
Limit5xx int
}
type alertData struct {
ClientTitle string
RPSLimit int
Domains []domainAlert
}
var data []alertData
for _, client := range clients {
var domains []domainAlert
for _, d := range client.Domains {
domains = append(domains, domainAlert{
SID: d.SID,
Limit4xx: d.Limit4xx,
Limit5xx: d.Limit5xx,
})
}
if client.RPSLimit > 0 || len(domains) > 0 {
data = append(data, alertData{
ClientTitle: client.ClientTitle,
RPSLimit: client.RPSLimit,
Domains: domains,
})
}
}
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)
}