auspex/servicecheck.go
2026-03-24 12:51:59 +03:00

347 lines
11 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = "⚠️ <b>Изменения в состоянии служб</b>"
case len(fallen) > 0:
header = "🚨 <b>АЛЕРТ: Службы упали!</b>"
default:
header = "✅ <b>Службы восстановлены</b>"
}
message := fmt.Sprintf(
"%s\n\n"+
"🖥 <b>Хост:</b> <code>%s</code>\n"+
"⏰ <b>Время:</b> %s\n\n",
header,
hostname,
time.Now().Format("2006-01-02 15:04:05"),
)
// Упавшие службы
if len(fallen) > 0 {
message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
message += fmt.Sprintf("❌ <b>Упали (%d):</b>\n\n", len(fallen))
for _, ch := range fallen {
emoji := scStateEmoji(ch.CurrState)
message += fmt.Sprintf(
"%s <b>%s</b>\n"+
" 📌 <code>%s</code> → <code>%s</code> / <code>%s</code>\n"+
" 🔧 <code>systemctl status %s</code>\n\n",
emoji, ch.Name,
ch.PrevState, ch.CurrState, ch.CurrInfo.SubState,
ch.Name,
)
}
}
// Восстановленные службы
if len(recovered) > 0 {
message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
message += fmt.Sprintf("✅ <b>Восстановились (%d):</b>\n\n", len(recovered))
for _, ch := range recovered {
message += fmt.Sprintf(
"✅ <b>%s</b>\n"+
" 📌 <code>%s</code> → <code>%s</code>\n\n",
ch.Name,
ch.PrevState, ch.CurrState,
)
}
}
message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
message += "💡 <i>journalctl -u &lt;служба&gt; -n 50 --no-pager</i>"
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")
}
}
// ── Вспомогательные функции ──────────────────────────────────────────────────
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 "🔄"
}
}