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 "🔄"
}
}