diff --git a/README.md b/README.md
index 7847805..d71804c 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Auspex
-Набор утилит мониторинга инфраструктуры.
+Набор утилит мониторинга инфраструктуры WAF.
## Утилиты
@@ -11,6 +11,7 @@
| `-p` | Проверка доступности портов из nginx upstream конфигов |
| `-m` | Проверка совпадений портов Angie ↔ Docker (+ проверка запуска контейнеров) |
| `-l` | Проверка обновления файлов PT AF access logs |
+| `-sv` | Проверка работоспособности служб |
## Структура проекта
@@ -30,7 +31,9 @@ auspex/
│ ├── portcheck/
│ │ └── portcheck.go # -p: проверка доступности портов
│ └── matchcheck/
-│ └── matchcheck.go # -m: совпадения Angie ↔ Docker
+│ | └── matchcheck.go # -m: совпадения Angie ↔ Docker
+│ └── servicecheck/
+│ └── servicecheck.go # -sv: проверка работоспособности служб
├── go.mod
├── Makefile
└── README.md
@@ -56,6 +59,7 @@ go build -o auspex
./auspex -m # Проверка Angie ↔ Docker (все клиенты)
./auspex -m client_name # Проверка конкретного клиента
./auspex -l # Проверка свежести логов
+./auspex -sw # Проверка работоспособности служб
./auspex -h # Справка
./auspex -v # Версия
```
@@ -92,7 +96,7 @@ go build -o auspex
| `AUSPEX_CERT_WARNING_DAYS` | Порог предупреждения (дни) | `15` |
| `AUSPEX_CERT_INFO_DAYS` | Порог информирования (дни) | `30` |
| `cfgDBUser` | БД юзер | `user` |
-| `cfgDBPassword` | БД пароль | `qJB` |
+| `cfgDBPassword` | БД пароль | `qJBrR` |
| `cfgDBName` | Название БД | `waf_info` |
| `cfgDBPort` | Порт БД | `5432` |
| `cfgPrimaryDBHost` | Основная БД | `10.100.10.8` |
diff --git a/config.go b/config.go
index d24ece2..bf08fb4 100644
--- a/config.go
+++ b/config.go
@@ -50,11 +50,12 @@ var (
// --- Пути ---
var (
- cfgPTAFBaseDir = "/home/install/ptaf"
- cfgAngieConfDir = "/etc/angie/http.d"
- cfgSSLDir = "/etc/ssl"
- cfgNginxConfigGlob = "/home/install/conf/ptaf-nginx/*/*/*/*.conf"
- cfgStateFilePath = "/tmp/port_checker_state.json"
+ cfgPTAFBaseDir = "/home/install/ptaf"
+ cfgAngieConfDir = "/etc/angie/http.d"
+ cfgSSLDir = "/etc/ssl"
+ cfgNginxConfigGlob = "/home/install/conf/ptaf-nginx/*/*/*/*.conf"
+ cfgStateFilePath = "/tmp/port_checker_state.json"
+ cfgServiceStateFilePath = "/tmp/auspex_service_state.json"
)
// --- Параметры проверки портов ---
@@ -73,6 +74,12 @@ var (
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 != "" {
cfgStateFilePath = v
}
+ if v, ok := env["SERVICE_STATE_FILE_PATH"]; ok && v != "" {
+ cfgServiceStateFilePath = v
+ }
if v, ok := env["LOG_DIR"]; ok && v != "" {
cfgLogDir = v
}
@@ -220,6 +230,19 @@ func applyConfig(env map[string]string) {
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(" ⚠️ База данных: не настроена")
}
+ // Services
+ if len(cfgServicesToCheck) > 0 {
+ fmt.Printf(" ✅ Службы для проверки: %s\n", strings.Join(cfgServicesToCheck, ", "))
+ } else {
+ fmt.Println(" ⚠️ Службы для проверки: не заданы (SERVICES_TO_CHECK)")
+ }
+
fmt.Println()
}
diff --git a/logcheck.go b/logcheck.go
index ce6e36b..1dba378 100644
--- a/logcheck.go
+++ b/logcheck.go
@@ -458,13 +458,12 @@ func lcSendAlerts(hostname string, staleFiles []lcLogFileStatus) {
message += fmt.Sprintf(
"%s %s\n"+
" 📄 Путь: %s\n"+
- " ⏱ Последняя запись: %d минут назад\n"+
- " 📅 Время: %s\n\n",
+ " ⏱ Последняя запись: %d минут назад в %s\n\n",
emoji,
filepath.Base(file.FilePath),
file.FilePath,
file.MinutesSince,
- file.LastModified.Format("2006-01-02 15:04:05"))
+ file.LastModified.Format("15:04:05 2006-01-02"))
}
message += "\n"
diff --git a/main.go b/main.go
index 6c54dd3..daa957c 100644
--- a/main.go
+++ b/main.go
@@ -7,7 +7,7 @@ import (
"strings"
)
-const appVersion = "1.0.0"
+const appVersion = "1.1.0"
// Цвета терминала (общие для всех модулей)
const (
@@ -79,6 +79,13 @@ func runByFlag(flag string, extraArgs []string) {
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":
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(" %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(" %s6%s) %s-sv%s Проверка состояния служб (systemd)\n", cGreen, cReset, cCyan, cReset)
fmt.Printf(" %s0%s) Выход\n\n", cGreen, 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)
@@ -141,11 +149,16 @@ func showMenu() {
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
}
return
+ case "6", "-sv", "sv":
+ if err := runServiceCheck(); err != nil {
+ fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
+ }
+ return
case "0", "q", "quit", "exit":
fmt.Println("Выход.")
return
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 Проверка совпадений портов Angie ↔ Docker [имя_клиента]\n", "-m")
fmt.Printf(" %-12s Проверка активности логов (PostgreSQL)\n", "-l")
+ fmt.Printf(" %-12s Проверка состояния служб systemd\n", "-sv")
fmt.Printf(" %-12s Показать версию\n", "-v")
fmt.Printf(" %-12s Показать эту справку\n\n", "-h")
fmt.Printf("Без аргументов запускается интерактивное меню.\n")
diff --git a/servicecheck.go b/servicecheck.go
new file mode 100644
index 0000000..cdd4db3
--- /dev/null
+++ b/servicecheck.go
@@ -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 = "⚠️ Изменения в состоянии служб"
+ 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")
+ }
+}
+
+// ── Вспомогательные функции ──────────────────────────────────────────────────
+
+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 "🔄"
+ }
+}