package main import ( "database/sql" "fmt" "log" "os" "path/filepath" "strings" "time" _ "github.com/lib/pq" ) // ── Типы ───────────────────────────────────────────────────────────────────── type lcInstanceInfo struct { Hostname string Instance string } type lcClientInfo struct { ClientTitle string Instance string } type lcAppInfo struct { L7ResourceID string ClientTitle string CheckWriteLogs bool } type lcLogFileStatus struct { FilePath string L7ResourceID string ClientTitle string LastModified time.Time MinutesSince int IsStale bool CheckWriteLogs bool } // ── Точка входа ────────────────────────────────────────────────────────────── func runLogCheck() error { hostname, err := os.Hostname() if err != nil { hostname = "unknown" log.Printf("⚠️ Не удалось получить hostname: %v", err) } log.Printf("🔍 LogCheck запущен на хосте: %s", hostname) log.Printf("📊 Подключение к базе данных...") // Подключаемся к БД (сначала primary, при ошибке - secondary) db, err := lcConnectDB() if err != nil { return fmt.Errorf("не удалось подключиться ни к одной БД: %v", err) } defer db.Close() log.Println("✅ Соединение с БД установлено") // Шаг 1: Получаем инстансы для текущего хоста instances, err := lcGetInstancesByHostname(db, hostname) if err != nil { return fmt.Errorf("ошибка получения инстансов: %v", err) } if len(instances) == 0 { log.Printf("⚠️ Для хоста %s не найдено инстансов в БД", hostname) return nil } log.Printf("📋 Найдено инстансов: %d", len(instances)) for _, inst := range instances { log.Printf(" - %s", inst.Instance) } // Шаг 2: Получаем клиентов для этих инстансов clients, err := lcGetClientsByInstances(db, instances) if err != nil { return fmt.Errorf("ошибка получения клиентов: %v", err) } if len(clients) == 0 { log.Println("⚠️ Клиенты не найдены для данных инстансов") return nil } log.Printf("👥 Найдено клиентов: %d", len(clients)) // Шаг 3: Получаем приложения (l7resourceid) для этих клиентов apps, err := lcGetAppsByClients(db, clients) if err != nil { return fmt.Errorf("ошибка получения приложений: %v", err) } if len(apps) == 0 { log.Println("⚠️ Приложения не найдены для данных клиентов") return nil } log.Printf("📦 Найдено приложений: %d", len(apps)) // Фильтруем только те, где check_write_logs = true activeApps := make([]lcAppInfo, 0) for _, app := range apps { if app.CheckWriteLogs { activeApps = append(activeApps, app) } } log.Printf("✓ Приложений с активной проверкой логов: %d", len(activeApps)) if len(activeApps) == 0 { log.Println("ℹ️ Нет приложений с включенной проверкой логов (check_write_logs = true)") return nil } // Шаг 4: Проверяем файлы логов log.Println("🔎 Проверка файлов логов...") staleFiles := make([]lcLogFileStatus, 0) okFiles := 0 notFoundFiles := 0 for _, app := range activeApps { statuses, err := lcCheckLogFile(app) if err != nil { log.Printf("⚠️ [%s] %s: %v", app.ClientTitle, app.L7ResourceID, err) notFoundFiles++ continue } // Обрабатываем все найденные файлы для данного приложения for _, status := range statuses { if status.IsStale { staleFiles = append(staleFiles, status) log.Printf("⚠️ [%s] %s (%s): лог устарел (%d мин)", status.ClientTitle, status.L7ResourceID, filepath.Base(status.FilePath), status.MinutesSince) } else { okFiles++ log.Printf("✓ [%s] %s (%s): актуален (%d мин)", status.ClientTitle, status.L7ResourceID, filepath.Base(status.FilePath), status.MinutesSince) } } } // Итоговая статистика log.Println("\n📊 Статистика проверки:") log.Printf(" ✅ Актуальных логов: %d", okFiles) log.Printf(" ⚠️ Устаревших логов: %d", len(staleFiles)) log.Printf(" ❌ Не найдено файлов: %d", notFoundFiles) log.Printf(" 📝 Всего проверено приложений: %d", len(activeApps)) // Шаг 5: Отправляем алерты если есть проблемы if len(staleFiles) > 0 { log.Printf("\n🚨 Найдено устаревших логов: %d", len(staleFiles)) log.Println("\n📋 Список устаревших логов:") log.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") // Группируем для вывода type consoleGroup struct { ClientTitle string L7ResourceID string Files []lcLogFileStatus } groupMap := make(map[string]*consoleGroup) for _, file := range staleFiles { key := file.ClientTitle + "::" + file.L7ResourceID if _, exists := groupMap[key]; !exists { groupMap[key] = &consoleGroup{ ClientTitle: file.ClientTitle, L7ResourceID: file.L7ResourceID, Files: make([]lcLogFileStatus, 0), } } groupMap[key].Files = append(groupMap[key].Files, file) } for _, group := range groupMap { log.Printf("\n👤 [%s] %s:", group.ClientTitle, group.L7ResourceID) for _, file := range group.Files { var emoji string if file.MinutesSince > 240 { emoji = "💀" // Более 4 часов } else if file.MinutesSince > 120 { emoji = "🔥" // Более 2 часов } else { emoji = "⚠️" // 61-120 минут } log.Printf(" %s %s: %d минут назад", emoji, filepath.Base(file.FilePath), file.MinutesSince) log.Printf(" 📄 %s", file.FilePath) } } log.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") if isTelegramConfigured() { lcSendAlerts(hostname, staleFiles) } else { log.Println("\nℹ️ Telegram уведомления отключены (не настроены данные)") log.Println("💡 Для включения уведомлений настройте cfgTelegramBotToken и cfgTelegramChatID в config.go") } } else { log.Println("\n✅ Все логи актуальны") } log.Println("=== Проверка завершена ===") return nil } // ── Подключение к БД ───────────────────────────────────────────────────────── func lcConnectDB() (*sql.DB, error) { // Пробуем primary primaryDSN := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", cfgPrimaryDBHost, cfgDBPort, cfgDBUser, cfgDBPassword, cfgDBName) db, err := sql.Open("postgres", primaryDSN) if err == nil { if err := db.Ping(); err == nil { log.Printf("✓ Подключено к primary БД: %s", cfgPrimaryDBHost) return db, nil } db.Close() } log.Printf("⚠️ Primary БД недоступна (%s), пробуем secondary...", cfgPrimaryDBHost) // Пробуем secondary secondaryDSN := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", cfgSecondaryDBHost, cfgDBPort, cfgDBUser, cfgDBPassword, cfgDBName) db, err = sql.Open("postgres", secondaryDSN) if err != nil { return nil, fmt.Errorf("не удалось открыть соединение с secondary: %v", err) } if err := db.Ping(); err != nil { db.Close() return nil, fmt.Errorf("secondary БД недоступна: %v", err) } log.Printf("✓ Подключено к secondary БД: %s", cfgSecondaryDBHost) return db, nil } // ── Запросы к БД ───────────────────────────────────────────────────────────── func lcGetInstancesByHostname(db *sql.DB, hostname string) ([]lcInstanceInfo, error) { query := `SELECT hostname, instance FROM instances_new WHERE hostname = $1` rows, err := db.Query(query, hostname) if err != nil { return nil, fmt.Errorf("ошибка выполнения запроса: %v", err) } defer rows.Close() var instances []lcInstanceInfo for rows.Next() { var inst lcInstanceInfo if err := rows.Scan(&inst.Hostname, &inst.Instance); err != nil { log.Printf("⚠️ Ошибка сканирования строки instances_new: %v", err) continue } instances = append(instances, inst) } return instances, rows.Err() } func lcGetClientsByInstances(db *sql.DB, instances []lcInstanceInfo) ([]lcClientInfo, error) { if len(instances) == 0 { return nil, nil } // Формируем список инстансов для IN (...) instanceNames := make([]interface{}, len(instances)) placeholders := make([]string, len(instances)) for i, inst := range instances { instanceNames[i] = inst.Instance placeholders[i] = fmt.Sprintf("$%d", i+1) } // Используем IN вместо ANY для совместимости query := fmt.Sprintf(`SELECT DISTINCT client_title, waf_instance FROM client_info WHERE waf_instance IN (%s)`, strings.Join(placeholders, ", ")) rows, err := db.Query(query, instanceNames...) if err != nil { return nil, fmt.Errorf("ошибка выполнения запроса: %v", err) } defer rows.Close() var clients []lcClientInfo for rows.Next() { var client lcClientInfo if err := rows.Scan(&client.ClientTitle, &client.Instance); err != nil { log.Printf("⚠️ Ошибка сканирования строки client_info: %v", err) continue } clients = append(clients, client) } return clients, rows.Err() } func lcGetAppsByClients(db *sql.DB, clients []lcClientInfo) ([]lcAppInfo, error) { if len(clients) == 0 { return nil, nil } clientTitles := make([]interface{}, len(clients)) placeholders := make([]string, len(clients)) for i, client := range clients { clientTitles[i] = client.ClientTitle placeholders[i] = fmt.Sprintf("$%d", i+1) } query := fmt.Sprintf(`SELECT l7resourceid, client_title, COALESCE(check_write_logs, false) FROM apps_settings WHERE client_title IN (%s)`, strings.Join(placeholders, ", ")) rows, err := db.Query(query, clientTitles...) if err != nil { return nil, fmt.Errorf("ошибка выполнения запроса: %v", err) } defer rows.Close() var apps []lcAppInfo for rows.Next() { var app lcAppInfo if err := rows.Scan(&app.L7ResourceID, &app.ClientTitle, &app.CheckWriteLogs); err != nil { log.Printf("⚠️ Ошибка сканирования строки apps_settings: %v", err) continue } apps = append(apps, app) } return apps, rows.Err() } // ── Проверка файлов логов ──────────────────────────────────────────────────── func lcCheckLogFile(app lcAppInfo) ([]lcLogFileStatus, error) { // Ищем файл вида: /var/log/ptaf_nginx/*/*/*_access.log // Учитываем что может быть: 10307_nationallottery.ru_access.log или 10307_access.log pattern := filepath.Join(cfgLogDir, "*", "*", app.L7ResourceID+"*_access.log") matches, err := filepath.Glob(pattern) if err != nil { return nil, fmt.Errorf("ошибка поиска файла: %v", err) } if len(matches) == 0 { return nil, fmt.Errorf("файл лога не найден") } // Если найдено несколько файлов, проверяем все var results []lcLogFileStatus if len(matches) > 1 { log.Printf("ℹ️ Найдено несколько файлов для %s (%d шт):", app.L7ResourceID, len(matches)) } for _, logFile := range matches { // Получаем информацию о файле fileInfo, err := os.Stat(logFile) if err != nil { log.Printf("⚠️ Ошибка получения информации о файле %s: %v", logFile, err) continue } lastModified := fileInfo.ModTime() timeSince := time.Since(lastModified) minutesSince := int(timeSince.Minutes()) status := lcLogFileStatus{ FilePath: logFile, L7ResourceID: app.L7ResourceID, ClientTitle: app.ClientTitle, LastModified: lastModified, MinutesSince: minutesSince, IsStale: minutesSince > cfgLogStaleMinutes, CheckWriteLogs: app.CheckWriteLogs, } results = append(results, status) if len(matches) > 1 { log.Printf(" - %s (изменен: %s, %d мин назад)", filepath.Base(logFile), fileInfo.ModTime().Format("2006-01-02 15:04:05"), minutesSince) } } return results, nil } // ── Отправка алертов ───────────────────────────────────────────────────────── func lcSendAlerts(hostname string, staleFiles []lcLogFileStatus) { if len(staleFiles) == 0 { return } // Группируем по клиентам и l7resourceid для удобства type fileGroup struct { ClientTitle string L7ResourceID string Files []lcLogFileStatus } groupMap := make(map[string]*fileGroup) for _, file := range staleFiles { key := file.ClientTitle + "::" + file.L7ResourceID if _, exists := groupMap[key]; !exists { groupMap[key] = &fileGroup{ ClientTitle: file.ClientTitle, L7ResourceID: file.L7ResourceID, Files: make([]lcLogFileStatus, 0), } } groupMap[key].Files = append(groupMap[key].Files, file) } // Формируем HTML сообщение message := fmt.Sprintf( "🚨 АЛЕРТ: Устаревшие логи! 🚨\n\n"+ "🖥 Хост: %s\n"+ "⏰ Время проверки: %s\n"+ "📊 Найдено устаревших файлов: %d\n\n", hostname, time.Now().Format("2006-01-02 15:04:05"), len(staleFiles)) message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n" // Выводим по группам (клиент + l7resourceid) for _, group := range groupMap { message += fmt.Sprintf("👤 Клиент: %s\n", group.ClientTitle) message += fmt.Sprintf("🆔 L7 Resource ID: %s\n\n", group.L7ResourceID) for _, file := range group.Files { var emoji string if file.MinutesSince > 240 { emoji = "💀" // Более 4 часов } else if file.MinutesSince > 120 { emoji = "🔥" // Более 2 часов } else { emoji = "⚠️" // 61-120 минут } message += fmt.Sprintf( "%s %s\n"+ " 📄 Путь: %s\n"+ " ⏱ Последняя запись: %d минут назад в %s\n\n", emoji, filepath.Base(file.FilePath), file.FilePath, file.MinutesSince, file.LastModified.Format("15:04:05 2006-01-02")) } message += "\n" } message += "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" message += "💡 Порог оповещения: более " + fmt.Sprintf("%d", cfgLogStaleMinutes) + " минут без записей" if err := telegramSendHTML(message); err != nil { log.Printf("❌ Ошибка отправки в Telegram: %v", err) } else { log.Println("✅ Алерт успешно отправлен в Telegram") } if err := emailSendHTML("Auspex: Устаревшие логи", message); err != nil { log.Printf("❌ Ошибка отправки Email: %v", err) } else { log.Println("✅ Алерт успешно отправлен на Email") } }