Service check

This commit is contained in:
Magnus Root 2026-02-26 12:04:00 +03:00
parent 7b7bb7c7c8
commit ff1a3528c0
5 changed files with 403 additions and 14 deletions

View file

@ -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` |

View file

@ -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()
} }

View file

@ -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
View file

@ -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
View 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 &lt;служба&gt; -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 "🔄"
}
}