Service check
This commit is contained in:
parent
7b7bb7c7c8
commit
ff1a3528c0
5 changed files with 403 additions and 14 deletions
10
README.md
10
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Auspex
|
# Auspex
|
||||||
|
|
||||||
Набор утилит мониторинга инфраструктуры.
|
Набор утилит мониторинга инфраструктуры WAF.
|
||||||
|
|
||||||
## Утилиты
|
## Утилиты
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
| `-p` | Проверка доступности портов из nginx upstream конфигов |
|
| `-p` | Проверка доступности портов из nginx upstream конфигов |
|
||||||
| `-m` | Проверка совпадений портов Angie ↔ Docker (+ проверка запуска контейнеров) |
|
| `-m` | Проверка совпадений портов Angie ↔ Docker (+ проверка запуска контейнеров) |
|
||||||
| `-l` | Проверка обновления файлов PT AF access logs |
|
| `-l` | Проверка обновления файлов PT AF access logs |
|
||||||
|
| `-sv` | Проверка работоспособности служб |
|
||||||
|
|
||||||
## Структура проекта
|
## Структура проекта
|
||||||
|
|
||||||
|
|
@ -30,7 +31,9 @@ auspex/
|
||||||
│ ├── portcheck/
|
│ ├── portcheck/
|
||||||
│ │ └── portcheck.go # -p: проверка доступности портов
|
│ │ └── portcheck.go # -p: проверка доступности портов
|
||||||
│ └── matchcheck/
|
│ └── matchcheck/
|
||||||
│ └── matchcheck.go # -m: совпадения Angie ↔ Docker
|
│ | └── matchcheck.go # -m: совпадения Angie ↔ Docker
|
||||||
|
│ └── servicecheck/
|
||||||
|
│ └── servicecheck.go # -sv: проверка работоспособности служб
|
||||||
├── go.mod
|
├── go.mod
|
||||||
├── Makefile
|
├── Makefile
|
||||||
└── README.md
|
└── README.md
|
||||||
|
|
@ -56,6 +59,7 @@ go build -o auspex
|
||||||
./auspex -m # Проверка Angie ↔ Docker (все клиенты)
|
./auspex -m # Проверка Angie ↔ Docker (все клиенты)
|
||||||
./auspex -m client_name # Проверка конкретного клиента
|
./auspex -m client_name # Проверка конкретного клиента
|
||||||
./auspex -l # Проверка свежести логов
|
./auspex -l # Проверка свежести логов
|
||||||
|
./auspex -sw # Проверка работоспособности служб
|
||||||
./auspex -h # Справка
|
./auspex -h # Справка
|
||||||
./auspex -v # Версия
|
./auspex -v # Версия
|
||||||
```
|
```
|
||||||
|
|
@ -92,7 +96,7 @@ go build -o auspex
|
||||||
| `AUSPEX_CERT_WARNING_DAYS` | Порог предупреждения (дни) | `15` |
|
| `AUSPEX_CERT_WARNING_DAYS` | Порог предупреждения (дни) | `15` |
|
||||||
| `AUSPEX_CERT_INFO_DAYS` | Порог информирования (дни) | `30` |
|
| `AUSPEX_CERT_INFO_DAYS` | Порог информирования (дни) | `30` |
|
||||||
| `cfgDBUser` | БД юзер | `user` |
|
| `cfgDBUser` | БД юзер | `user` |
|
||||||
| `cfgDBPassword` | БД пароль | `qJB` |
|
| `cfgDBPassword` | БД пароль | `qJBrR` |
|
||||||
| `cfgDBName` | Название БД | `waf_info` |
|
| `cfgDBName` | Название БД | `waf_info` |
|
||||||
| `cfgDBPort` | Порт БД | `5432` |
|
| `cfgDBPort` | Порт БД | `5432` |
|
||||||
| `cfgPrimaryDBHost` | Основная БД | `10.100.10.8` |
|
| `cfgPrimaryDBHost` | Основная БД | `10.100.10.8` |
|
||||||
|
|
|
||||||
40
config.go
40
config.go
|
|
@ -50,11 +50,12 @@ var (
|
||||||
|
|
||||||
// --- Пути ---
|
// --- Пути ---
|
||||||
var (
|
var (
|
||||||
cfgPTAFBaseDir = "/home/install/ptaf"
|
cfgPTAFBaseDir = "/home/install/ptaf"
|
||||||
cfgAngieConfDir = "/etc/angie/http.d"
|
cfgAngieConfDir = "/etc/angie/http.d"
|
||||||
cfgSSLDir = "/etc/ssl"
|
cfgSSLDir = "/etc/ssl"
|
||||||
cfgNginxConfigGlob = "/home/install/conf/ptaf-nginx/*/*/*/*.conf"
|
cfgNginxConfigGlob = "/home/install/conf/ptaf-nginx/*/*/*/*.conf"
|
||||||
cfgStateFilePath = "/tmp/port_checker_state.json"
|
cfgStateFilePath = "/tmp/port_checker_state.json"
|
||||||
|
cfgServiceStateFilePath = "/tmp/auspex_service_state.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Параметры проверки портов ---
|
// --- Параметры проверки портов ---
|
||||||
|
|
@ -73,6 +74,12 @@ var (
|
||||||
cfgLogStaleMinutes = 61 // Порог в минутах для определения устаревшего лога
|
cfgLogStaleMinutes = 61 // Порог в минутах для определения устаревшего лога
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// --- Service Check ---
|
||||||
|
// Список служб systemd для проверки на текущем хосте.
|
||||||
|
// Задаётся через SERVICES_TO_CHECK в auspex.env в виде списка через запятую.
|
||||||
|
// Пример: SERVICES_TO_CHECK=angie,docker
|
||||||
|
var cfgServicesToCheck = []string{}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Загрузка конфигурации из файла
|
// Загрузка конфигурации из файла
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -173,6 +180,9 @@ func applyConfig(env map[string]string) {
|
||||||
if v, ok := env["STATE_FILE_PATH"]; ok && v != "" {
|
if v, ok := env["STATE_FILE_PATH"]; ok && v != "" {
|
||||||
cfgStateFilePath = v
|
cfgStateFilePath = v
|
||||||
}
|
}
|
||||||
|
if v, ok := env["SERVICE_STATE_FILE_PATH"]; ok && v != "" {
|
||||||
|
cfgServiceStateFilePath = v
|
||||||
|
}
|
||||||
if v, ok := env["LOG_DIR"]; ok && v != "" {
|
if v, ok := env["LOG_DIR"]; ok && v != "" {
|
||||||
cfgLogDir = v
|
cfgLogDir = v
|
||||||
}
|
}
|
||||||
|
|
@ -220,6 +230,19 @@ func applyConfig(env map[string]string) {
|
||||||
cfgMaxWorkers = workers
|
cfgMaxWorkers = workers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Services to check
|
||||||
|
if v, ok := env["SERVICES_TO_CHECK"]; ok && v != "" {
|
||||||
|
parts := strings.Split(v, ",")
|
||||||
|
services := make([]string, 0, len(parts))
|
||||||
|
for _, s := range parts {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s != "" {
|
||||||
|
services = append(services, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfgServicesToCheck = services
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -251,5 +274,12 @@ func printConfigStatus() {
|
||||||
fmt.Println(" ⚠️ База данных: не настроена")
|
fmt.Println(" ⚠️ База данных: не настроена")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Services
|
||||||
|
if len(cfgServicesToCheck) > 0 {
|
||||||
|
fmt.Printf(" ✅ Службы для проверки: %s\n", strings.Join(cfgServicesToCheck, ", "))
|
||||||
|
} else {
|
||||||
|
fmt.Println(" ⚠️ Службы для проверки: не заданы (SERVICES_TO_CHECK)")
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -458,13 +458,12 @@ func lcSendAlerts(hostname string, staleFiles []lcLogFileStatus) {
|
||||||
message += fmt.Sprintf(
|
message += fmt.Sprintf(
|
||||||
"%s <b>%s</b>\n"+
|
"%s <b>%s</b>\n"+
|
||||||
" 📄 Путь: <code>%s</code>\n"+
|
" 📄 Путь: <code>%s</code>\n"+
|
||||||
" ⏱ Последняя запись: <b>%d минут назад</b>\n"+
|
" ⏱ Последняя запись: <b>%d минут назад</b> в %s\n\n",
|
||||||
" 📅 Время: %s\n\n",
|
|
||||||
emoji,
|
emoji,
|
||||||
filepath.Base(file.FilePath),
|
filepath.Base(file.FilePath),
|
||||||
file.FilePath,
|
file.FilePath,
|
||||||
file.MinutesSince,
|
file.MinutesSince,
|
||||||
file.LastModified.Format("2006-01-02 15:04:05"))
|
file.LastModified.Format("15:04:05 2006-01-02"))
|
||||||
}
|
}
|
||||||
|
|
||||||
message += "\n"
|
message += "\n"
|
||||||
|
|
|
||||||
20
main.go
20
main.go
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const appVersion = "1.0.0"
|
const appVersion = "1.1.0"
|
||||||
|
|
||||||
// Цвета терминала (общие для всех модулей)
|
// Цвета терминала (общие для всех модулей)
|
||||||
const (
|
const (
|
||||||
|
|
@ -79,6 +79,13 @@ func runByFlag(flag string, extraArgs []string) {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "-sv":
|
||||||
|
fmt.Printf("%s🔧 Auspex — Проверка состояния служб%s\n\n", cCyan, cReset)
|
||||||
|
if err := runServiceCheck(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
case "-v", "--version":
|
case "-v", "--version":
|
||||||
fmt.Printf("auspex v%s\n", appVersion)
|
fmt.Printf("auspex v%s\n", appVersion)
|
||||||
|
|
||||||
|
|
@ -101,9 +108,10 @@ func showMenu() {
|
||||||
fmt.Printf(" %s3%s) %s-p %s Проверка доступности портов (nginx upstream)\n", cGreen, cReset, cCyan, cReset)
|
fmt.Printf(" %s3%s) %s-p %s Проверка доступности портов (nginx upstream)\n", cGreen, cReset, cCyan, cReset)
|
||||||
fmt.Printf(" %s4%s) %s-m %s Проверка совпадений портов Angie ↔ Docker\n", cGreen, cReset, cCyan, cReset)
|
fmt.Printf(" %s4%s) %s-m %s Проверка совпадений портов Angie ↔ Docker\n", cGreen, cReset, cCyan, cReset)
|
||||||
fmt.Printf(" %s5%s) %s-l %s Проверка активности логов (PostgreSQL)\n", cGreen, cReset, cCyan, cReset)
|
fmt.Printf(" %s5%s) %s-l %s Проверка активности логов (PostgreSQL)\n", cGreen, cReset, cCyan, cReset)
|
||||||
|
fmt.Printf(" %s6%s) %s-sv%s Проверка состояния служб (systemd)\n", cGreen, cReset, cCyan, cReset)
|
||||||
fmt.Printf(" %s0%s) Выход\n\n", cGreen, cReset)
|
fmt.Printf(" %s0%s) Выход\n\n", cGreen, cReset)
|
||||||
fmt.Printf("%sДля быстрого вызова: auspex <ключ>%s\n", cYellow, cReset)
|
fmt.Printf("%sДля быстрого вызова: auspex <ключ>%s\n", cYellow, cReset)
|
||||||
fmt.Printf("%sПример: auspex -ss | auspex -c | auspex -p | auspex -m [клиент] | auspex -l%s\n\n", cYellow, cReset)
|
fmt.Printf("%sПример: auspex -ss | auspex -c | auspex -p | auspex -m [клиент] | auspex -l | auspex -sv%s\n\n", cYellow, cReset)
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
|
@ -141,11 +149,16 @@ func showMenu() {
|
||||||
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
case "6", "-sv", "sv":
|
||||||
|
if err := runServiceCheck(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
case "0", "q", "quit", "exit":
|
case "0", "q", "quit", "exit":
|
||||||
fmt.Println("Выход.")
|
fmt.Println("Выход.")
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
fmt.Printf("%sНеизвестная команда. Введите 1-5 или 0 для выхода.%s\n", cRed, cReset)
|
fmt.Printf("%sНеизвестная команда. Введите 1-6 или 0 для выхода.%s\n", cRed, cReset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,6 +183,7 @@ func printUsage() {
|
||||||
fmt.Printf(" %-12s Проверка доступности портов (nginx upstream)\n", "-p")
|
fmt.Printf(" %-12s Проверка доступности портов (nginx upstream)\n", "-p")
|
||||||
fmt.Printf(" %-12s Проверка совпадений портов Angie ↔ Docker [имя_клиента]\n", "-m")
|
fmt.Printf(" %-12s Проверка совпадений портов Angie ↔ Docker [имя_клиента]\n", "-m")
|
||||||
fmt.Printf(" %-12s Проверка активности логов (PostgreSQL)\n", "-l")
|
fmt.Printf(" %-12s Проверка активности логов (PostgreSQL)\n", "-l")
|
||||||
|
fmt.Printf(" %-12s Проверка состояния служб systemd\n", "-sv")
|
||||||
fmt.Printf(" %-12s Показать версию\n", "-v")
|
fmt.Printf(" %-12s Показать версию\n", "-v")
|
||||||
fmt.Printf(" %-12s Показать эту справку\n\n", "-h")
|
fmt.Printf(" %-12s Показать эту справку\n\n", "-h")
|
||||||
fmt.Printf("Без аргументов запускается интерактивное меню.\n")
|
fmt.Printf("Без аргументов запускается интерактивное меню.\n")
|
||||||
|
|
|
||||||
342
servicecheck.go
Normal file
342
servicecheck.go
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
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 <служба> -n 50 --no-pager</i>"
|
||||||
|
|
||||||
|
if err := telegramSendHTML(message); err != nil {
|
||||||
|
log.Printf("❌ Ошибка отправки в Telegram: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("✅ Алерт успешно отправлен в Telegram")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Вспомогательные функции ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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 "🔄"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue