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
|
||||
|
||||
Набор утилит мониторинга инфраструктуры.
|
||||
Набор утилит мониторинга инфраструктуры 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` |
|
||||
|
|
|
|||
40
config.go
40
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -458,13 +458,12 @@ func lcSendAlerts(hostname string, staleFiles []lcLogFileStatus) {
|
|||
message += fmt.Sprintf(
|
||||
"%s <b>%s</b>\n"+
|
||||
" 📄 Путь: <code>%s</code>\n"+
|
||||
" ⏱ Последняя запись: <b>%d минут назад</b>\n"+
|
||||
" 📅 Время: %s\n\n",
|
||||
" ⏱ Последняя запись: <b>%d минут назад</b> в %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"
|
||||
|
|
|
|||
20
main.go
20
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")
|
||||
|
|
|
|||
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