From 6d43c8bdc09d016c695091662a37058478659c8f Mon Sep 17 00:00:00 2001 From: Magnus Root Date: Wed, 10 Jun 2026 18:54:34 +0300 Subject: [PATCH] Update duplicate check --- ARCHITECTURE.md | 47 ++++++++++++++++++++++------------- README.md | 6 ++--- database.go | 65 ++++++++++++++++++++++++++++++++++--------------- main.go | 52 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 129 insertions(+), 41 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0eb0c0f..035e3f8 100755 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,7 +2,7 @@ ## Обзор -API Sync — однопроходная утилита (не демон), запускаемая по cron. Поддерживает два режима запуска: основной и проверка дублей по ключу `--cd`. +API Sync — однопроходная утилита (не демон), запускаемая по cron. Поддерживает два режима запуска: основной и проверка дублей/ошибок API по ключу `--cd`. --- @@ -10,7 +10,7 @@ API Sync — однопроходная утилита (не демон), зап ```bash ./API_sync # основной запуск -./API_sync --cd # только проверка дублирующихся доменов/алиасов +./API_sync --cd # проверка дублирующихся доменов/алиасов + ошибок API ``` Пример cron-конфигурации: @@ -19,7 +19,7 @@ API Sync — однопроходная утилита (не демон), зап # Основной запуск каждые 15 минут */15 * * * * install cd /home/install && /home/install/API_sync >> /var/log/API_sync.log 2>&1 -# Проверка дублей каждый час +# Проверка дублей и ошибок API каждый час 0 * * * * install cd /home/install && /home/install/API_sync --cd >> /var/log/API_sync.log 2>&1 ``` @@ -41,20 +41,22 @@ main() ├─ 4. Параллельная обработка SID (worker pool) │ └─ processSID() для каждого SID │ ├─ GET /l7/resource/{sid}/global → whois - │ │ └─ ошибка → алерт 🔴 + следующий SID + │ │ └─ ошибка → записывается в apiErrors │ ├─ GET /l7/origin/global?l7ResourceId={sid} → origins - │ │ └─ ошибка → алерт 🔴 + следующий SID + │ │ └─ ошибка → записывается в apiErrors │ ├─ GET /l7/alias/global?l7ResourceId={sid} → aliases - │ │ └─ ошибка → алерт 🔴 + следующий SID + │ │ └─ ошибка → записывается в apiErrors │ ├─ DNS резолв домена → проверка AntiDDOS │ └─ upsertAPIInfo() → сравнение + запись в API_info + алерты │ - ├─ 5. Очистка устаревших SID из API_info (cleanupRemovedSIDs) + ├─ 5. Отправка сводного алерта об ошибках API (если CHECK_DUPLICATES=true) │ - ├─ 6. Проверка дублирующихся доменов/алиасов (если CHECK_DUPLICATES=true) + ├─ 6. Очистка устаревших SID из API_info (cleanupRemovedSIDs) + │ + ├─ 7. Проверка дублирующихся доменов/алиасов (если CHECK_DUPLICATES=true) │ └─ checkDuplicateDomains() → алерт 🔴 если найдены дубли │ - └─ 7. Обновление WAF whitelist (updateWAFWhitelist) + └─ 8. Обновление WAF whitelist (updateWAFWhitelist) ├─ git pull ├─ Загрузка IP из API_info (auto-ресурсы) ├─ Загрузка IP из manual_info (ручные ресурсы) @@ -71,7 +73,18 @@ main() main() --cd │ ├─ 1. Подключение к PostgreSQL - └─ 2. checkDuplicateDomains() → алерт 🔴 если найдены дубли + │ + ├─ 2. Загрузка всех SID (apps_settings WHERE mode = 'auto') + │ + ├─ 3. Параллельная проверка доступности API (worker pool) + │ └─ checkSIDAvailability() для каждого SID + │ ├─ GET /l7/resource/{sid}/global + │ ├─ GET /l7/origin/global?l7ResourceId={sid} + │ └─ GET /l7/alias/global?l7ResourceId={sid} + │ + ├─ 4. Отправка сводного алерта об ошибках API (если есть) + │ + └─ 5. checkDuplicateDomains() → алерт 🔴 если найдены дубли ``` --- @@ -80,9 +93,7 @@ main() --cd ### main.go — точка входа и worker pool -При запуске с ключом `--cd` выполняет только проверку дублей и завершается. В основном режиме инициализирует подключение к БД, собирает все SID и запускает пул горутин. Количество воркеров задаётся через `MAX_CONCURRENT_WORKERS` (по умолчанию 5). SID передаются воркерам через буферизированный канал. Все воркеры работают параллельно — DNS-резолв, запросы к API и запись в БД выполняются одновременно для разных SID. - -После завершения всех воркеров последовательно запускаются: очистка устаревших записей, проверка дублей (если не отключена через `CHECK_DUPLICATES=false`) и обновление whitelist. +При запуске с ключом `--cd` параллельно проверяет доступность API для всех SID через `checkSIDAvailability()`, отправляет сводный алерт об ошибках и выполняет проверку дублей. В основном режиме запускает полный цикл синхронизации с пулом воркеров. Ошибки API собираются в общий map через mutex и отправляются одним алертом после завершения всех воркеров (если `CHECK_DUPLICATES=true`). ### config.go — конфигурация @@ -100,9 +111,11 @@ main() --cd **`loadL7IDs()`** — загружает SID из `apps_settings` для конкретного клиента с `mode = 'auto'`. -**`processSID()`** — оркестрирует обработку одного SID: делает три запроса к API, резолвит домен для проверки AntiDDOS, вызывает `upsertAPIInfo()`. При ошибке любого API-запроса отправляет алерт через `sendAPIErrorAlert()` с указанием SID, endpoint и текста ошибки. +**`processSID()`** — оркестрирует обработку одного SID: делает три запроса к API, резолвит домен для проверки AntiDDOS, вызывает `upsertAPIInfo()`. При ошибке API возвращает её наверх — воркер записывает в общий map ошибок. -**`sendAPIErrorAlert()`** — формирует и отправляет алерт 🔴 при ошибке запроса к API ServicePipe. Указывает SID, endpoint (`whois`, `origins`, `aliases`) и текст ошибки. +**`checkSIDAvailability()`** — делает только три API-запроса без записи в БД и без алертов. Используется в режиме `--cd` для быстрой проверки доступности API по каждому SID. + +**`sendAPIErrorsAlert()`** — формирует и отправляет один сводный алерт 🔴 со всеми SID у которых была ошибка API. SID отсортированы для стабильного вывода. **`upsertAPIInfo()`** — в начале подтягивает из БД `client_title` (из `apps_settings`) и `waf_provider` (из `client_info`) для использования в алертах. Затем сравнивает новые данные со старыми и при наличии изменений формирует алерты: - изменение домена → строка `Домен изменён: old -> new` + флаг необходимости обновления WAF @@ -178,7 +191,7 @@ main() --cd | Изменение origins/aliases/домена | 🟡 | SID, домен, TENANT, WAF provider, детали изменений, предупреждение об обновлении WAF, время | | Изменение WAF-настроек инстанса | ⚪ | SID, домен, TENANT, изменения waf_enabled/vendor/instance в формате `было -> стало`, время | | Удаление ресурса из мониторинга | 🗑 | SID, домен, предупреждение с указанием конкретного WAF-вендора | -| Ошибка запроса к API ServicePipe | 🔴 | SID, endpoint, текст ошибки, время | +| Ошибки запросов к API ServicePipe | 🔴 | Сводный список SID с ошибками и текстом ошибки, время | | Дублирующиеся домены/алиасы | 🔴 | Список дублей с указанием SID, время проверки | | Обновление whitelist | 🔄 | Добавленные/удалённые IP с привязкой к SID и домену, итоговое количество | @@ -216,7 +229,7 @@ main() --cd | `GIT_REPO_PATH` | — | Локальный путь к репозиторию | | `WHITELIST_FILE` | whitelist_ptaf.txt | Имя файла whitelist | | `MAX_CONCURRENT_WORKERS` | 5 | Количество параллельных воркеров | -| `CHECK_DUPLICATES` | true | Включить проверку дублей в основном режиме | +| `CHECK_DUPLICATES` | true | Включить проверку дублей и сводный алерт ошибок API в основном режиме | --- diff --git a/README.md b/README.md index 87f65c4..2e4eb34 100755 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ chmod 600 API_sync.env # Основной запуск ./API_sync -# Только проверка дублирующихся доменов/алиасов +# Проверка дублирующихся доменов/алиасов + ошибок API ./API_sync --cd ``` @@ -119,7 +119,7 @@ chmod 600 API_sync.env # Основной запуск каждые 15 минут */15 * * * * install cd /home/install && /home/install/API_sync >> /var/log/API_sync.log 2>&1 -# Проверка дублей каждый час +# Проверка дублей и ошибок API каждый час 0 * * * * install cd /home/install && /home/install/API_sync --cd >> /var/log/API_sync.log 2>&1 ``` @@ -141,7 +141,7 @@ chmod 600 API_sync.env ✅ **Очистка устаревших данных** — автоматическое удаление из API_info ресурсов, пропавших из apps_settings -✅ **Алерты об ошибках API** — уведомление при недоступности API API для конкретного SID +✅ **Сводный алерт ошибок API** — все недоступные SID собираются в одно сообщение (управляется `CHECK_DUPLICATES`) ✅ **Проверка дублей** — поиск совпадающих доменов/алиасов между разными SID, запускается в основном режиме (управляется `CHECK_DUPLICATES`) или отдельно через `--cd` diff --git a/database.go b/database.go index ab276f0..d66e4e9 100755 --- a/database.go +++ b/database.go @@ -63,22 +63,19 @@ func processSID(db *sql.DB, sid int64) error { // WHOIS var whois whoisResponse if err := apiGet(whoisURL+fmt.Sprint(sid)+"/global", &whois); err != nil { - sendAPIErrorAlert(sid, "whois", err) - return err + return fmt.Errorf("whois: %w", err) } // ORIGINS var originsResp apiList[originItem] if err := apiGet(originURL+fmt.Sprint(sid), &originsResp); err != nil { - sendAPIErrorAlert(sid, "origins", err) - return err + return fmt.Errorf("origins: %w", err) } // ALIASES var aliasResp apiList[aliasItem] if err := apiGet(aliasURL+fmt.Sprint(sid), &aliasResp); err != nil { - sendAPIErrorAlert(sid, "aliases", err) - return err + return fmt.Errorf("aliases: %w", err) } originsJSON, _ := json.Marshal(originsResp.Data.Result.Items) @@ -369,21 +366,49 @@ func upsertSPInfo( return nil } -func sendAPIErrorAlert(sid int64, endpoint string, err error) { +func checkSIDAvailability(sid int64) error { + var whois whoisResponse + if err := apiGet(whoisURL+fmt.Sprint(sid)+"/global", &whois); err != nil { + return fmt.Errorf("whois: %w", err) + } + + var originsResp apiList[originItem] + if err := apiGet(originURL+fmt.Sprint(sid), &originsResp); err != nil { + return fmt.Errorf("origins: %w", err) + } + + var aliasResp apiList[aliasItem] + if err := apiGet(aliasURL+fmt.Sprint(sid), &aliasResp); err != nil { + return fmt.Errorf("aliases: %w", err) + } + + return nil +} + +func sendAPIErrorsAlert(errors map[int64]string) { + if len(errors) == 0 { + return + } + + // Сортируем SID для стабильного вывода + sids := make([]int64, 0, len(errors)) + for sid := range errors { + sids = append(sids, sid) + } + sort.Slice(sids, func(i, j int) bool { return sids[i] < sids[j] }) + now := time.Now().In(time.FixedZone("UTC+3", 3*60*60)) - message := fmt.Sprintf( - "🔴 Ошибка запроса к API ServicePipe\n\n"+ - "SID: %d\n"+ - "Endpoint: %s\n"+ - "Ошибка: %s\n\n"+ - "Время: %s", - sid, - endpoint, - err.Error(), - now.Format("2006-01-02 15:04:05")+" UTC+3", - ) - if alertErr := sendAlert(message); alertErr != nil { - log.Printf("[API Error Alert] Failed to send alert for SID %d: %v", sid, alertErr) + var msg strings.Builder + msg.WriteString(fmt.Sprintf("🔴 Ошибки запросов к API ServicePipe (%d)\n\n", len(errors))) + + for _, sid := range sids { + msg.WriteString(fmt.Sprintf("SID %d: %s\n", sid, errors[sid])) + } + + msg.WriteString(fmt.Sprintf("\nВремя проверки: %s", now.Format("2006-01-02 15:04:05")+" UTC+3")) + + if err := sendAlert(msg.String()); err != nil { + log.Printf("[API Errors Alert] Failed to send alert: %v", err) } } diff --git a/main.go b/main.go index a0ad793..9b15b7c 100755 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( ) func main() { - // Режим --cd: только проверка дублей + // Режим --cd: только проверка дублей и ошибок API if len(os.Args) > 1 && os.Args[1] == "--cd" { log.Println("Running duplicate domain/alias check only") @@ -19,6 +19,46 @@ func main() { } defer db.Close() + // Проверяем ошибки API по всем SID из apps_settings + var allSIDs []int64 + clients, err := loadClients(db) + if err == nil { + for _, client := range clients { + ids, err := loadL7IDs(db, client) + if err == nil { + allSIDs = append(allSIDs, ids...) + } + } + } + + // Проверяем доступность API параллельно + sidChan := make(chan int64, len(allSIDs)) + var wg sync.WaitGroup + var mu sync.Mutex + apiErrors := make(map[int64]string) + + for i := 0; i < maxConcurrentWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for sid := range sidChan { + if err := checkSIDAvailability(sid); err != nil { + mu.Lock() + apiErrors[sid] = err.Error() + mu.Unlock() + } + } + }() + } + + for _, sid := range allSIDs { + sidChan <- sid + } + close(sidChan) + wg.Wait() + + sendAPIErrorsAlert(apiErrors) + if err := checkDuplicateDomains(db); err != nil { log.Printf("Duplicate check error: %v", err) } @@ -54,6 +94,8 @@ func main() { // Создаём канал для заданий и WaitGroup для ожидания sidChan := make(chan int64, len(allSIDs)) var wg sync.WaitGroup + var mu sync.Mutex + apiErrors := make(map[int64]string) // Запускаем воркеры for i := 0; i < maxConcurrentWorkers; i++ { @@ -64,6 +106,9 @@ func main() { log.Printf("[Worker %d] Processing SID %d", workerID, sid) if err := processSID(db, sid); err != nil { log.Printf("[Worker %d] SID %d error: %v", workerID, sid, err) + mu.Lock() + apiErrors[sid] = err.Error() + mu.Unlock() } } }(i) @@ -80,6 +125,11 @@ func main() { log.Println("Finish sync sp_info") + // Отправляем сводный алерт по ошибкам API + if checkDuplicates { + sendAPIErrorsAlert(apiErrors) + } + // Удаляем из sp_info ресурсы, которых больше нет в apps_settings log.Println("Start cleanup of removed SIDs") if err := cleanupRemovedSIDs(db, allSIDs); err != nil {