diff --git a/config.go b/config.go
index bf08fb4..ccdaa08 100644
--- a/config.go
+++ b/config.go
@@ -56,6 +56,7 @@ var (
cfgNginxConfigGlob = "/home/install/conf/ptaf-nginx/*/*/*/*.conf"
cfgStateFilePath = "/tmp/port_checker_state.json"
cfgServiceStateFilePath = "/tmp/auspex_service_state.json"
+ cfgDockerStateFilePath = "/tmp/auspex_docker_state.json"
)
// --- Параметры проверки портов ---
@@ -183,6 +184,9 @@ func applyConfig(env map[string]string) {
if v, ok := env["SERVICE_STATE_FILE_PATH"]; ok && v != "" {
cfgServiceStateFilePath = v
}
+ if v, ok := env["DOCKER_STATE_FILE_PATH"]; ok && v != "" {
+ cfgDockerStateFilePath = v
+ }
if v, ok := env["LOG_DIR"]; ok && v != "" {
cfgLogDir = v
}
diff --git a/dockercheck.go b/dockercheck.go
new file mode 100644
index 0000000..72fc039
--- /dev/null
+++ b/dockercheck.go
@@ -0,0 +1,499 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+)
+
+// ── Типы ─────────────────────────────────────────────────────────────────────
+
+type dcContainerInfo struct {
+ ID string
+ Name string
+ Status string // running, exited, dead, restarting, paused, created
+ Health string // healthy, unhealthy, starting, none
+ Image string
+}
+
+type dcStateFile struct {
+ Timestamp string `json:"timestamp"`
+ Containers map[string]dcContainerState `json:"containers"` // name → state
+}
+
+type dcContainerState struct {
+ Status string `json:"status"`
+ Health string `json:"health"`
+}
+
+type dcChangeKind string
+
+const (
+ dcKindStatusChanged dcChangeKind = "status_changed"
+ dcKindHealthChanged dcChangeKind = "health_changed"
+ dcKindDisappeared dcChangeKind = "disappeared"
+)
+
+type dcStateChange struct {
+ Name string
+ Kind dcChangeKind
+ PrevState dcContainerState
+ CurrState dcContainerState
+ Image string
+}
+
+func (ch dcStateChange) isProblem() bool {
+ switch ch.Kind {
+ case dcKindDisappeared:
+ return true
+ case dcKindStatusChanged:
+ return ch.CurrState.Status != "running"
+ case dcKindHealthChanged:
+ return ch.CurrState.Health == "unhealthy"
+ }
+ return false
+}
+
+func (ch dcStateChange) isRecovery() bool {
+ switch ch.Kind {
+ case dcKindStatusChanged:
+ return ch.CurrState.Status == "running" && ch.PrevState.Status != "running"
+ case dcKindHealthChanged:
+ return ch.CurrState.Health == "healthy" && ch.PrevState.Health == "unhealthy"
+ }
+ return false
+}
+
+// ── Точка входа ───────────────────────────────────────────────────────────────
+
+func runDockerCheck() error {
+ hostname, err := os.Hostname()
+ if err != nil {
+ hostname = "unknown"
+ log.Printf("⚠️ Не удалось получить hostname: %v", err)
+ }
+
+ log.Printf("🔍 DockerCheck запущен на хосте: %s", hostname)
+
+ // Проверяем доступность docker
+ if err := dcCheckDockerAvailable(); err != nil {
+ return fmt.Errorf("docker недоступен: %v", err)
+ }
+
+ // Получаем текущий список контейнеров
+ containers, err := dcGetContainers()
+ if err != nil {
+ return fmt.Errorf("ошибка получения списка контейнеров: %v", err)
+ }
+
+ if len(containers) == 0 {
+ log.Println("ℹ️ Запущенных контейнеров не найдено")
+ }
+
+ log.Printf("📦 Найдено контейнеров: %d", len(containers))
+
+ // Выводим текущее состояние
+ for _, c := range containers {
+ emoji := dcStatusEmoji(c.Status, c.Health)
+ if c.Health != "" && c.Health != "none" {
+ log.Printf("%s %-30s | %-12s | health: %s | %s", emoji, c.Name, c.Status, c.Health, c.Image)
+ } else {
+ log.Printf("%s %-30s | %-12s | %s", emoji, c.Name, c.Status, c.Image)
+ }
+ }
+
+ // Загружаем предыдущее состояние
+ prevState := dcLoadState()
+
+ // Строим map текущего состояния
+ current := make(map[string]dcContainerInfo, len(containers))
+ for _, c := range containers {
+ current[c.Name] = c
+ }
+
+ // Сравниваем
+ changes := dcCompareStates(prevState.Containers, current)
+
+ // Статистика
+ runningCount := 0
+ problemCount := 0
+ for _, c := range containers {
+ if c.Status == "running" && c.Health != "unhealthy" {
+ runningCount++
+ } else {
+ problemCount++
+ }
+ }
+
+ log.Println("\n📊 Итоги проверки контейнеров:")
+ log.Printf(" ✅ Работают нормально: %d", runningCount)
+ log.Printf(" ❌ Проблемных: %d", problemCount)
+ log.Printf(" 🔄 Изменений статуса: %d", len(changes))
+
+ if len(changes) > 0 {
+ log.Println("\n🔄 Изменения с последней проверки:")
+ log.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
+ for _, ch := range changes {
+ arrow := dcChangeArrow(ch)
+ switch ch.Kind {
+ case dcKindDisappeared:
+ log.Printf(" %s %-30s | исчез (был: %s)", arrow, ch.Name, ch.PrevState.Status)
+ case dcKindStatusChanged:
+ log.Printf(" %s %-30s | %s → %s", arrow, ch.Name, ch.PrevState.Status, ch.CurrState.Status)
+ case dcKindHealthChanged:
+ log.Printf(" %s %-30s | health: %s → %s", arrow, ch.Name, ch.PrevState.Health, ch.CurrState.Health)
+ }
+ }
+ log.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
+
+ if isTelegramConfigured() {
+ dcSendAlert(hostname, changes)
+ } else {
+ log.Println("\nℹ️ Telegram уведомления отключены (не настроены данные)")
+ }
+ } else {
+ if len(prevState.Containers) == 0 {
+ log.Println("\nℹ️ Первый запуск — состояние сохранено, уведомления при изменениях")
+ } else {
+ log.Println("\n✅ Изменений нет — уведомления не отправляются")
+ }
+ }
+
+ // Сохраняем текущее состояние
+ dcSaveState(current)
+
+ log.Println("=== Проверка контейнеров завершена ===")
+ return nil
+}
+
+// ── Получение контейнеров ────────────────────────────────────────────────────
+
+func dcCheckDockerAvailable() error {
+ err := exec.Command("docker", "info").Run()
+ if err != nil {
+ return fmt.Errorf("docker info завершился с ошибкой: %v", err)
+ }
+ return nil
+}
+
+func dcGetContainers() ([]dcContainerInfo, error) {
+ // Получаем все контейнеры (включая не-running) через Go-шаблон
+ // Поля: ID, Name, Status, Health, Image — разделитель |
+ out, err := exec.Command(
+ "docker", "ps", "-a",
+ "--format", "{{.ID}}|{{.Names}}|{{.Status}}|{{.Health}}|{{.Image}}",
+ ).Output()
+ if err != nil {
+ return nil, fmt.Errorf("docker ps завершился с ошибкой: %v", err)
+ }
+
+ var containers []dcContainerInfo
+ for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
+ if line == "" {
+ continue
+ }
+ parts := strings.SplitN(line, "|", 5)
+ if len(parts) != 5 {
+ continue
+ }
+
+ c := dcContainerInfo{
+ ID: strings.TrimSpace(parts[0]),
+ Name: strings.TrimSpace(parts[1]),
+ Image: strings.TrimSpace(parts[4]),
+ Health: strings.ToLower(strings.TrimSpace(parts[3])),
+ }
+
+ // Status из docker ps выглядит как "Up 2 hours" или "Exited (1) 3 minutes ago"
+ // Нормализуем до короткого статуса
+ c.Status = dcNormalizeStatus(parts[2])
+
+ containers = append(containers, c)
+ }
+
+ return containers, nil
+}
+
+// dcNormalizeStatus приводит вывод docker ps к короткому статусу
+func dcNormalizeStatus(raw string) string {
+ raw = strings.TrimSpace(raw)
+ lower := strings.ToLower(raw)
+ switch {
+ case strings.HasPrefix(lower, "up"):
+ return "running"
+ case strings.HasPrefix(lower, "exited"):
+ return "exited"
+ case strings.HasPrefix(lower, "restarting"):
+ return "restarting"
+ case strings.HasPrefix(lower, "dead"):
+ return "dead"
+ case strings.HasPrefix(lower, "paused"):
+ return "paused"
+ case strings.HasPrefix(lower, "created"):
+ return "created"
+ case strings.HasPrefix(lower, "removal"):
+ return "removing"
+ default:
+ return lower
+ }
+}
+
+// ── Сравнение состояний ──────────────────────────────────────────────────────
+
+func dcCompareStates(prev map[string]dcContainerState, curr map[string]dcContainerInfo) []dcStateChange {
+ var changes []dcStateChange
+
+ // Проверяем текущие контейнеры
+ for name, currInfo := range curr {
+ prevInfo, existed := prev[name]
+
+ if !existed {
+ // Новый контейнер — не алертим, просто добавится в state
+ continue
+ }
+
+ currState := dcContainerState{Status: currInfo.Status, Health: currInfo.Health}
+
+ // Изменение статуса
+ if prevInfo.Status != currState.Status {
+ changes = append(changes, dcStateChange{
+ Name: name,
+ Kind: dcKindStatusChanged,
+ PrevState: prevInfo,
+ CurrState: currState,
+ Image: currInfo.Image,
+ })
+ }
+
+ // Изменение health (независимо от статуса)
+ if prevInfo.Health != currState.Health &&
+ currState.Health != "" && currState.Health != "none" &&
+ prevInfo.Health != "" && prevInfo.Health != "none" {
+ changes = append(changes, dcStateChange{
+ Name: name,
+ Kind: dcKindHealthChanged,
+ PrevState: prevInfo,
+ CurrState: currState,
+ Image: currInfo.Image,
+ })
+ }
+ }
+
+ // Проверяем исчезнувшие контейнеры (были в state, нет сейчас)
+ for name, prevInfo := range prev {
+ if _, exists := curr[name]; !exists {
+ changes = append(changes, dcStateChange{
+ Name: name,
+ Kind: dcKindDisappeared,
+ PrevState: prevInfo,
+ CurrState: dcContainerState{},
+ Image: "",
+ })
+ }
+ }
+
+ return changes
+}
+
+// ── State-файл ────────────────────────────────────────────────────────────────
+
+func dcLoadState() dcStateFile {
+ data, err := os.ReadFile(cfgDockerStateFilePath)
+ if err != nil {
+ return dcStateFile{Containers: make(map[string]dcContainerState)}
+ }
+
+ var state dcStateFile
+ if err := json.Unmarshal(data, &state); err != nil {
+ log.Printf("⚠️ Не удалось прочитать state-файл docker: %v", err)
+ return dcStateFile{Containers: make(map[string]dcContainerState)}
+ }
+
+ return state
+}
+
+func dcSaveState(current map[string]dcContainerInfo) {
+ containers := make(map[string]dcContainerState, len(current))
+ for name, info := range current {
+ containers[name] = dcContainerState{
+ Status: info.Status,
+ Health: info.Health,
+ }
+ }
+
+ state := dcStateFile{
+ Timestamp: time.Now().Format("2006-01-02 15:04:05"),
+ Containers: containers,
+ }
+
+ data, err := json.MarshalIndent(state, "", " ")
+ if err != nil {
+ log.Printf("⚠️ Ошибка сериализации state-файла docker: %v", err)
+ return
+ }
+
+ if err := os.WriteFile(cfgDockerStateFilePath, data, 0644); err != nil {
+ log.Printf("⚠️ Ошибка записи state-файла %s: %v", cfgDockerStateFilePath, err)
+ }
+}
+
+// ── Отправка алерта ──────────────────────────────────────────────────────────
+
+func dcSendAlert(hostname string, changes []dcStateChange) {
+ var problems, recoveries []dcStateChange
+ for _, ch := range changes {
+ if ch.isRecovery() {
+ recoveries = append(recoveries, ch)
+ } else if ch.isProblem() {
+ problems = append(problems, ch)
+ }
+ }
+
+ // Если нет ни проблем, ни восстановлений — молчим
+ if len(problems) == 0 && len(recoveries) == 0 {
+ return
+ }
+
+ // Заголовок по наиболее критичному событию
+ var header string
+ switch {
+ case len(problems) > 0 && len(recoveries) > 0:
+ header = "⚠️ Изменения в состоянии контейнеров"
+ case len(problems) > 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(problems) > 0 {
+ message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
+ message += fmt.Sprintf("❌ Проблемы (%d):\n\n", len(problems))
+ for _, ch := range problems {
+ emoji := dcChangeEmoji(ch)
+ switch ch.Kind {
+ case dcKindDisappeared:
+ message += fmt.Sprintf(
+ "%s %s\n"+
+ " 📌 Контейнер исчез (был: %s)\n\n",
+ emoji, ch.Name, ch.PrevState.Status,
+ )
+ case dcKindStatusChanged:
+ message += fmt.Sprintf(
+ "%s %s\n"+
+ " 📌 %s → %s\n"+
+ " 🐳 %s\n"+
+ " 🔧 docker logs %s --tail 50\n\n",
+ emoji, ch.Name,
+ ch.PrevState.Status, ch.CurrState.Status,
+ ch.Image, ch.Name,
+ )
+ case dcKindHealthChanged:
+ message += fmt.Sprintf(
+ "%s %s\n"+
+ " 📌 health: %s → %s\n"+
+ " 🐳 %s\n"+
+ " 🔧 docker inspect %s\n\n",
+ emoji, ch.Name,
+ ch.PrevState.Health, ch.CurrState.Health,
+ ch.Image, ch.Name,
+ )
+ }
+ }
+ }
+
+ // Восстановленные контейнеры
+ if len(recoveries) > 0 {
+ message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
+ message += fmt.Sprintf("✅ Восстановились (%d):\n\n", len(recoveries))
+ for _, ch := range recoveries {
+ switch ch.Kind {
+ case dcKindStatusChanged:
+ message += fmt.Sprintf(
+ "✅ %s\n"+
+ " 📌 %s → %s\n\n",
+ ch.Name, ch.PrevState.Status, ch.CurrState.Status,
+ )
+ case dcKindHealthChanged:
+ message += fmt.Sprintf(
+ "✅ %s\n"+
+ " 📌 health: %s → %s\n\n",
+ ch.Name, ch.PrevState.Health, ch.CurrState.Health,
+ )
+ }
+ }
+ }
+
+ message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
+ message += "💡 docker ps -a | docker logs <имя> --tail 50"
+
+ if err := telegramSendHTML(message); err != nil {
+ log.Printf("❌ Ошибка отправки в Telegram: %v", err)
+ } else {
+ log.Println("✅ Алерт успешно отправлен в Telegram")
+ }
+}
+
+// ── Вспомогательные функции ──────────────────────────────────────────────────
+
+func dcStatusEmoji(status, health string) string {
+ if health == "unhealthy" {
+ return "🤒"
+ }
+ switch status {
+ case "running":
+ return "✅"
+ case "restarting":
+ return "🔄"
+ case "exited":
+ return "🔴"
+ case "dead":
+ return "💀"
+ case "paused":
+ return "⏸"
+ default:
+ return "❓"
+ }
+}
+
+func dcChangeEmoji(ch dcStateChange) string {
+ switch ch.Kind {
+ case dcKindDisappeared:
+ return "👻"
+ case dcKindHealthChanged:
+ return "🤒"
+ case dcKindStatusChanged:
+ switch ch.CurrState.Status {
+ case "restarting":
+ return "🔄"
+ case "exited":
+ return "🔴"
+ case "dead":
+ return "💀"
+ }
+ }
+ return "❓"
+}
+
+func dcChangeArrow(ch dcStateChange) string {
+ if ch.isRecovery() {
+ return "✅"
+ }
+ if ch.isProblem() {
+ return "❌"
+ }
+ return "🔄"
+}
diff --git a/main.go b/main.go
index daa957c..e3d7b41 100644
--- a/main.go
+++ b/main.go
@@ -7,7 +7,7 @@ import (
"strings"
)
-const appVersion = "1.1.0"
+const appVersion = "1.2.0"
// Цвета терминала (общие для всех модулей)
const (
@@ -86,6 +86,13 @@ func runByFlag(flag string, extraArgs []string) {
os.Exit(1)
}
+ case "-dc", "-d":
+ fmt.Printf("%s🐳 Auspex — Проверка контейнеров Docker%s\n\n", cCyan, cReset)
+ if err := runDockerCheck(); err != nil {
+ fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
+ os.Exit(1)
+ }
+
case "-v", "--version":
fmt.Printf("auspex v%s\n", appVersion)
@@ -109,9 +116,10 @@ func showMenu() {
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(" %s7%s) %s-dc%s Проверка контейнеров Docker\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 | auspex -sv%s\n\n", cYellow, cReset)
+ fmt.Printf("%sПример: auspex -ss | auspex -c | auspex -p | auspex -m [клиент] | auspex -l | auspex -sv | auspex -dc%s\n\n", cYellow, cReset)
reader := bufio.NewReader(os.Stdin)
@@ -154,11 +162,16 @@ func showMenu() {
fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
}
return
+ case "7", "-dc", "-d", "dc", "d":
+ if err := runDockerCheck(); err != nil {
+ fmt.Fprintf(os.Stderr, "Ошибка: %v\n", err)
+ }
+ return
case "0", "q", "quit", "exit":
fmt.Println("Выход.")
return
default:
- fmt.Printf("%sНеизвестная команда. Введите 1-6 или 0 для выхода.%s\n", cRed, cReset)
+ fmt.Printf("%sНеизвестная команда. Введите 1-7 или 0 для выхода.%s\n", cRed, cReset)
}
}
}
@@ -184,6 +197,7 @@ func printUsage() {
fmt.Printf(" %-12s Проверка совпадений портов Angie ↔ Docker [имя_клиента]\n", "-m")
fmt.Printf(" %-12s Проверка активности логов (PostgreSQL)\n", "-l")
fmt.Printf(" %-12s Проверка состояния служб systemd\n", "-sv")
+ fmt.Printf(" %-12s Проверка контейнеров Docker\n", "-dc")
fmt.Printf(" %-12s Показать версию\n", "-v")
fmt.Printf(" %-12s Показать эту справку\n\n", "-h")
fmt.Printf("Без аргументов запускается интерактивное меню.\n")