auspex/logcheck.go
2026-03-24 12:51:59 +03:00

485 lines
16 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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")
}
}