485 lines
16 KiB
Go
485 lines
16 KiB
Go
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/*/*/<l7resourceid>*_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(
|
||
"🚨 <b>АЛЕРТ: Устаревшие логи!</b> 🚨\n\n"+
|
||
"🖥 <b>Хост:</b> <code>%s</code>\n"+
|
||
"⏰ <b>Время проверки:</b> %s\n"+
|
||
"📊 <b>Найдено устаревших файлов:</b> %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("👤 <b>Клиент:</b> %s\n", group.ClientTitle)
|
||
message += fmt.Sprintf("🆔 <b>L7 Resource ID:</b> <code>%s</code>\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 <b>%s</b>\n"+
|
||
" 📄 Путь: <code>%s</code>\n"+
|
||
" ⏱ Последняя запись: <b>%d минут назад</b> в %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 += "💡 <i>Порог оповещения: более " + fmt.Sprintf("%d", cfgLogStaleMinutes) + " минут без записей</i>"
|
||
|
||
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")
|
||
}
|
||
}
|