package main import ( "encoding/json" "fmt" "log" "os" "os/exec" "strings" "sync" "time" ) // ── Типы ───────────────────────────────────────────────────────────────────── type scServiceStatus struct { Name string Active bool State string // active, inactive, failed, activating, deactivating, unknown SubState string // running, dead, exited, etc. Description string } type scStateFile struct { Timestamp string `json:"timestamp"` Services map[string]string `json:"services"` // name → state } type scStateChange struct { Name string PrevState string CurrState string CurrInfo scServiceStatus } // ── Точка входа ─────────────────────────────────────────────────────────────── func runServiceCheck() error { hostname, err := os.Hostname() if err != nil { hostname = "unknown" log.Printf("⚠️ Не удалось получить hostname: %v", err) } services := cfgServicesToCheck if len(services) == 0 { log.Println("ℹ️ Список служб для проверки пуст (SERVICES_TO_CHECK не задан)") return nil } log.Printf("🔍 ServiceCheck запущен на хосте: %s", hostname) log.Printf("📋 Служб для проверки: %d (%s)", len(services), strings.Join(services, ", ")) // Параллельная проверка всех служб statusChan := make(chan scServiceStatus, len(services)) var wg sync.WaitGroup for _, svc := range services { wg.Add(1) go func(name string) { defer wg.Done() statusChan <- scCheckService(name) }(svc) } go func() { wg.Wait() close(statusChan) }() // Собираем результаты в map для удобного сравнения current := make(map[string]scServiceStatus) for status := range statusChan { current[status.Name] = status if status.Active { log.Printf("✅ %-20s | %s (%s)", status.Name, status.State, status.SubState) } else { log.Printf("❌ %-20s | %s (%s)", status.Name, status.State, status.SubState) } } // Загружаем предыдущее состояние prevState := scLoadState() // Сравниваем — ищем изменения changes := scCompareStates(prevState.Services, current) // Статистика okCount := 0 failCount := 0 for _, s := range current { if s.Active { okCount++ } else { failCount++ } } log.Println("\n📊 Итоги проверки служб:") log.Printf(" ✅ Работают нормально: %d", okCount) log.Printf(" ❌ Проблемных служб: %d", failCount) log.Printf(" 🔄 Изменений статуса: %d", len(changes)) // Обрабатываем изменения if len(changes) > 0 { log.Println("\n🔄 Изменения с последней проверки:") log.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") for _, ch := range changes { arrow := scChangeArrow(ch.PrevState, ch.CurrState) log.Printf(" %s %-20s | %s → %s", arrow, ch.Name, ch.PrevState, ch.CurrState) } log.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") if isTelegramConfigured() { scSendAlert(hostname, changes) } else { log.Println("\nℹ️ Telegram уведомления отключены (не настроены данные)") } } else { if len(prevState.Services) == 0 { log.Println("\nℹ️ Первый запуск — состояние сохранено, уведомления при изменениях") } else { log.Println("\n✅ Изменений нет — уведомления не отправляются") } } // Сохраняем текущее состояние scSaveState(current) log.Println("=== Проверка служб завершена ===") return nil } // ── Проверка одной службы ──────────────────────────────────────────────────── func scCheckService(name string) scServiceStatus { status := scServiceStatus{Name: name} // is-active возвращает non-zero если служба не active — это нормально activeOut, _ := exec.Command("systemctl", "is-active", name).Output() activeState := strings.TrimSpace(string(activeOut)) if activeState == "" { activeState = "unknown" } status.State = activeState status.Active = activeState == "active" // Получаем SubState и Description через show showOut, err := exec.Command( "systemctl", "show", name, "--property=SubState,Description", "--no-page", ).Output() if err == nil { for _, line := range strings.Split(string(showOut), "\n") { parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue } switch parts[0] { case "SubState": status.SubState = strings.TrimSpace(parts[1]) case "Description": status.Description = strings.TrimSpace(parts[1]) } } } if status.SubState == "" { status.SubState = "—" } return status } // ── Сравнение состояний ────────────────────────────────────────────────────── func scCompareStates(prev map[string]string, curr map[string]scServiceStatus) []scStateChange { var changes []scStateChange for name, currStatus := range curr { prevState, existed := prev[name] if !existed { // Первый запуск или новая служба добавлена в конфиг — не алертим continue } if prevState != currStatus.State { changes = append(changes, scStateChange{ Name: name, PrevState: prevState, CurrState: currStatus.State, CurrInfo: currStatus, }) } } return changes } // ── State-файл ──────────────────────────────────────────────────────────────── func scLoadState() scStateFile { data, err := os.ReadFile(cfgServiceStateFilePath) if err != nil { // Файл не существует — первый запуск return scStateFile{Services: make(map[string]string)} } var state scStateFile if err := json.Unmarshal(data, &state); err != nil { log.Printf("⚠️ Не удалось прочитать state-файл служб: %v", err) return scStateFile{Services: make(map[string]string)} } return state } func scSaveState(current map[string]scServiceStatus) { services := make(map[string]string, len(current)) for name, status := range current { services[name] = status.State } state := scStateFile{ Timestamp: time.Now().Format("2006-01-02 15:04:05"), Services: services, } data, err := json.MarshalIndent(state, "", " ") if err != nil { log.Printf("⚠️ Ошибка сериализации state-файла: %v", err) return } if err := os.WriteFile(cfgServiceStateFilePath, data, 0644); err != nil { log.Printf("⚠️ Ошибка записи state-файла %s: %v", cfgServiceStateFilePath, err) } } // ── Отправка алерта ────────────────────────────────────────────────────────── func scSendAlert(hostname string, changes []scStateChange) { // Разбиваем на падения и восстановления var fallen, recovered []scStateChange for _, ch := range changes { if ch.CurrInfo.Active { recovered = append(recovered, ch) } else { fallen = append(fallen, ch) } } // Заголовок по наиболее критичному событию var header string switch { case len(fallen) > 0 && len(recovered) > 0: header = "⚠️ Изменения в состоянии служб" case len(fallen) > 0: header = "🚨 АЛЕРТ: Службы упали!" default: header = "✅ Службы восстановлены" } message := fmt.Sprintf( "%s\n\n"+ "🖥 Хост: %s\n"+ "⏰ Время: %s\n\n", header, hostname, time.Now().Format("2006-01-02 15:04:05"), ) // Упавшие службы if len(fallen) > 0 { message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" message += fmt.Sprintf("❌ Упали (%d):\n\n", len(fallen)) for _, ch := range fallen { emoji := scStateEmoji(ch.CurrState) message += fmt.Sprintf( "%s %s\n"+ " 📌 %s%s / %s\n"+ " 🔧 systemctl status %s\n\n", emoji, ch.Name, ch.PrevState, ch.CurrState, ch.CurrInfo.SubState, ch.Name, ) } } // Восстановленные службы if len(recovered) > 0 { message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" message += fmt.Sprintf("✅ Восстановились (%d):\n\n", len(recovered)) for _, ch := range recovered { message += fmt.Sprintf( "✅ %s\n"+ " 📌 %s%s\n\n", ch.Name, ch.PrevState, ch.CurrState, ) } } message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" message += "💡 journalctl -u <служба> -n 50 --no-pager" if err := telegramSendHTML(message); err != nil { log.Printf("❌ Ошибка отправки в Telegram: %v", err) } else { log.Println("✅ Алерт успешно отправлен в Telegram") } if err := emailSendHTML("Auspex: Проблемы со службами", message); err != nil { log.Printf("❌ Ошибка отправки Email: %v", err) } else { log.Println("✅ Алерт успешно отправлен на Email") } if err := mattermostSend(message); err != nil { log.Printf("❌ Ошибка отправки в Mattermost: %v", err) } else { log.Println("✅ Алерт успешно отправлен в Mattermost") } } // ── Вспомогательные функции ────────────────────────────────────────────────── func scStateEmoji(state string) string { switch state { case "failed": return "💀" case "inactive": return "🔴" case "activating", "deactivating": return "🟡" default: return "❓" } } func scChangeArrow(prev, curr string) string { switch { case prev == "active" && curr != "active": return "❌" case prev != "active" && curr == "active": return "✅" default: return "🔄" } }