Update duplicate check

This commit is contained in:
Magnus Root 2026-06-10 18:54:34 +03:00
parent 96c07cdddd
commit 6d43c8bdc0
4 changed files with 129 additions and 41 deletions

View file

@ -2,7 +2,7 @@
## Обзор ## Обзор
API Sync — однопроходная утилита (не демон), запускаемая по cron. Поддерживает два режима запуска: основной и проверка дублей по ключу `--cd`. API Sync — однопроходная утилита (не демон), запускаемая по cron. Поддерживает два режима запуска: основной и проверка дублей/ошибок API по ключу `--cd`.
--- ---
@ -10,7 +10,7 @@ API Sync — однопроходная утилита (не демон), зап
```bash ```bash
./API_sync # основной запуск ./API_sync # основной запуск
./API_sync --cd # только проверка дублирующихся доменов/алиасов ./API_sync --cd # проверка дублирующихся доменов/алиасов + ошибок API
``` ```
Пример cron-конфигурации: Пример cron-конфигурации:
@ -19,7 +19,7 @@ API Sync — однопроходная утилита (не демон), зап
# Основной запуск каждые 15 минут # Основной запуск каждые 15 минут
*/15 * * * * install cd /home/install && /home/install/API_sync >> /var/log/API_sync.log 2>&1 */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 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) ├─ 4. Параллельная обработка SID (worker pool)
│ └─ processSID() для каждого SID │ └─ processSID() для каждого SID
│ ├─ GET /l7/resource/{sid}/global → whois │ ├─ GET /l7/resource/{sid}/global → whois
│ │ └─ ошибка → алерт 🔴 + следующий SID │ │ └─ ошибка → записывается в apiErrors
│ ├─ GET /l7/origin/global?l7ResourceId={sid} → origins │ ├─ GET /l7/origin/global?l7ResourceId={sid} → origins
│ │ └─ ошибка → алерт 🔴 + следующий SID │ │ └─ ошибка → записывается в apiErrors
│ ├─ GET /l7/alias/global?l7ResourceId={sid} → aliases │ ├─ GET /l7/alias/global?l7ResourceId={sid} → aliases
│ │ └─ ошибка → алерт 🔴 + следующий SID │ │ └─ ошибка → записывается в apiErrors
│ ├─ DNS резолв домена → проверка AntiDDOS │ ├─ DNS резолв домена → проверка AntiDDOS
│ └─ upsertAPIInfo() → сравнение + запись в API_info + алерты │ └─ 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() → алерт 🔴 если найдены дубли │ └─ checkDuplicateDomains() → алерт 🔴 если найдены дубли
└─ 7. Обновление WAF whitelist (updateWAFWhitelist) └─ 8. Обновление WAF whitelist (updateWAFWhitelist)
├─ git pull ├─ git pull
├─ Загрузка IP из API_info (auto-ресурсы) ├─ Загрузка IP из API_info (auto-ресурсы)
├─ Загрузка IP из manual_info (ручные ресурсы) ├─ Загрузка IP из manual_info (ручные ресурсы)
@ -71,7 +73,18 @@ main()
main() --cd main() --cd
├─ 1. Подключение к PostgreSQL ├─ 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 ### main.go — точка входа и worker pool
При запуске с ключом `--cd` выполняет только проверку дублей и завершается. В основном режиме инициализирует подключение к БД, собирает все SID и запускает пул горутин. Количество воркеров задаётся через `MAX_CONCURRENT_WORKERS` (по умолчанию 5). SID передаются воркерам через буферизированный канал. Все воркеры работают параллельно — DNS-резолв, запросы к API и запись в БД выполняются одновременно для разных SID. При запуске с ключом `--cd` параллельно проверяет доступность API для всех SID через `checkSIDAvailability()`, отправляет сводный алерт об ошибках и выполняет проверку дублей. В основном режиме запускает полный цикл синхронизации с пулом воркеров. Ошибки API собираются в общий map через mutex и отправляются одним алертом после завершения всех воркеров (если `CHECK_DUPLICATES=true`).
После завершения всех воркеров последовательно запускаются: очистка устаревших записей, проверка дублей (если не отключена через `CHECK_DUPLICATES=false`) и обновление whitelist.
### config.go — конфигурация ### config.go — конфигурация
@ -100,9 +111,11 @@ main() --cd
**`loadL7IDs()`** — загружает SID из `apps_settings` для конкретного клиента с `mode = 'auto'`. **`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`) для использования в алертах. Затем сравнивает новые данные со старыми и при наличии изменений формирует алерты: **`upsertAPIInfo()`** — в начале подтягивает из БД `client_title` (из `apps_settings`) и `waf_provider` (из `client_info`) для использования в алертах. Затем сравнивает новые данные со старыми и при наличии изменений формирует алерты:
- изменение домена → строка `Домен изменён: old -> new` + флаг необходимости обновления WAF - изменение домена → строка `Домен изменён: old -> new` + флаг необходимости обновления WAF
@ -178,7 +191,7 @@ main() --cd
| Изменение origins/aliases/домена | 🟡 | SID, домен, TENANT, WAF provider, детали изменений, предупреждение об обновлении WAF, время | | Изменение origins/aliases/домена | 🟡 | SID, домен, TENANT, WAF provider, детали изменений, предупреждение об обновлении WAF, время |
| Изменение WAF-настроек инстанса | ⚪ | SID, домен, TENANT, изменения waf_enabled/vendor/instance в формате `было -> стало`, время | | Изменение WAF-настроек инстанса | ⚪ | SID, домен, TENANT, изменения waf_enabled/vendor/instance в формате `было -> стало`, время |
| Удаление ресурса из мониторинга | 🗑 | SID, домен, предупреждение с указанием конкретного WAF-вендора | | Удаление ресурса из мониторинга | 🗑 | SID, домен, предупреждение с указанием конкретного WAF-вендора |
| Ошибка запроса к API ServicePipe | 🔴 | SID, endpoint, текст ошибки, время | | Ошибки запросов к API ServicePipe | 🔴 | Сводный список SID с ошибками и текстом ошибки, время |
| Дублирующиеся домены/алиасы | 🔴 | Список дублей с указанием SID, время проверки | | Дублирующиеся домены/алиасы | 🔴 | Список дублей с указанием SID, время проверки |
| Обновление whitelist | 🔄 | Добавленные/удалённые IP с привязкой к SID и домену, итоговое количество | | Обновление whitelist | 🔄 | Добавленные/удалённые IP с привязкой к SID и домену, итоговое количество |
@ -216,7 +229,7 @@ main() --cd
| `GIT_REPO_PATH` | — | Локальный путь к репозиторию | | `GIT_REPO_PATH` | — | Локальный путь к репозиторию |
| `WHITELIST_FILE` | whitelist_ptaf.txt | Имя файла whitelist | | `WHITELIST_FILE` | whitelist_ptaf.txt | Имя файла whitelist |
| `MAX_CONCURRENT_WORKERS` | 5 | Количество параллельных воркеров | | `MAX_CONCURRENT_WORKERS` | 5 | Количество параллельных воркеров |
| `CHECK_DUPLICATES` | true | Включить проверку дублей в основном режиме | | `CHECK_DUPLICATES` | true | Включить проверку дублей и сводный алерт ошибок API в основном режиме |
--- ---

View file

@ -107,7 +107,7 @@ chmod 600 API_sync.env
# Основной запуск # Основной запуск
./API_sync ./API_sync
# Только проверка дублирующихся доменов/алиасов # Проверка дублирующихся доменов/алиасов + ошибок API
./API_sync --cd ./API_sync --cd
``` ```
@ -119,7 +119,7 @@ chmod 600 API_sync.env
# Основной запуск каждые 15 минут # Основной запуск каждые 15 минут
*/15 * * * * install cd /home/install && /home/install/API_sync >> /var/log/API_sync.log 2>&1 */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 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_info ресурсов, пропавших из apps_settings
**Алерты об ошибках API** — уведомление при недоступности API API для конкретного SID **Сводный алерт ошибок API** — все недоступные SID собираются в одно сообщение (управляется `CHECK_DUPLICATES`)
**Проверка дублей** — поиск совпадающих доменов/алиасов между разными SID, запускается в основном режиме (управляется `CHECK_DUPLICATES`) или отдельно через `--cd` **Проверка дублей** — поиск совпадающих доменов/алиасов между разными SID, запускается в основном режиме (управляется `CHECK_DUPLICATES`) или отдельно через `--cd`

View file

@ -63,22 +63,19 @@ func processSID(db *sql.DB, sid int64) error {
// WHOIS // WHOIS
var whois whoisResponse var whois whoisResponse
if err := apiGet(whoisURL+fmt.Sprint(sid)+"/global", &whois); err != nil { if err := apiGet(whoisURL+fmt.Sprint(sid)+"/global", &whois); err != nil {
sendAPIErrorAlert(sid, "whois", err) return fmt.Errorf("whois: %w", err)
return err
} }
// ORIGINS // ORIGINS
var originsResp apiList[originItem] var originsResp apiList[originItem]
if err := apiGet(originURL+fmt.Sprint(sid), &originsResp); err != nil { if err := apiGet(originURL+fmt.Sprint(sid), &originsResp); err != nil {
sendAPIErrorAlert(sid, "origins", err) return fmt.Errorf("origins: %w", err)
return err
} }
// ALIASES // ALIASES
var aliasResp apiList[aliasItem] var aliasResp apiList[aliasItem]
if err := apiGet(aliasURL+fmt.Sprint(sid), &aliasResp); err != nil { if err := apiGet(aliasURL+fmt.Sprint(sid), &aliasResp); err != nil {
sendAPIErrorAlert(sid, "aliases", err) return fmt.Errorf("aliases: %w", err)
return err
} }
originsJSON, _ := json.Marshal(originsResp.Data.Result.Items) originsJSON, _ := json.Marshal(originsResp.Data.Result.Items)
@ -369,21 +366,49 @@ func upsertSPInfo(
return nil 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)) now := time.Now().In(time.FixedZone("UTC+3", 3*60*60))
message := fmt.Sprintf( var msg strings.Builder
"🔴 <b>Ошибка запроса к API ServicePipe</b>\n\n"+ msg.WriteString(fmt.Sprintf("🔴 <b>Ошибки запросов к API ServicePipe (%d)</b>\n\n", len(errors)))
"<b>SID:</b> %d\n"+
"<b>Endpoint:</b> %s\n"+ for _, sid := range sids {
"<b>Ошибка:</b> %s\n\n"+ msg.WriteString(fmt.Sprintf("<b>SID %d:</b> %s\n", sid, errors[sid]))
"<b>Время:</b> %s", }
sid,
endpoint, msg.WriteString(fmt.Sprintf("\n<b>Время проверки:</b> %s", now.Format("2006-01-02 15:04:05")+" UTC+3"))
err.Error(),
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)
if alertErr := sendAlert(message); alertErr != nil {
log.Printf("[API Error Alert] Failed to send alert for SID %d: %v", sid, alertErr)
} }
} }

52
main.go
View file

@ -9,7 +9,7 @@ import (
) )
func main() { func main() {
// Режим --cd: только проверка дублей // Режим --cd: только проверка дублей и ошибок API
if len(os.Args) > 1 && os.Args[1] == "--cd" { if len(os.Args) > 1 && os.Args[1] == "--cd" {
log.Println("Running duplicate domain/alias check only") log.Println("Running duplicate domain/alias check only")
@ -19,6 +19,46 @@ func main() {
} }
defer db.Close() 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 { if err := checkDuplicateDomains(db); err != nil {
log.Printf("Duplicate check error: %v", err) log.Printf("Duplicate check error: %v", err)
} }
@ -54,6 +94,8 @@ func main() {
// Создаём канал для заданий и WaitGroup для ожидания // Создаём канал для заданий и WaitGroup для ожидания
sidChan := make(chan int64, len(allSIDs)) sidChan := make(chan int64, len(allSIDs))
var wg sync.WaitGroup var wg sync.WaitGroup
var mu sync.Mutex
apiErrors := make(map[int64]string)
// Запускаем воркеры // Запускаем воркеры
for i := 0; i < maxConcurrentWorkers; i++ { for i := 0; i < maxConcurrentWorkers; i++ {
@ -64,6 +106,9 @@ func main() {
log.Printf("[Worker %d] Processing SID %d", workerID, sid) log.Printf("[Worker %d] Processing SID %d", workerID, sid)
if err := processSID(db, sid); err != nil { if err := processSID(db, sid); err != nil {
log.Printf("[Worker %d] SID %d error: %v", workerID, sid, err) log.Printf("[Worker %d] SID %d error: %v", workerID, sid, err)
mu.Lock()
apiErrors[sid] = err.Error()
mu.Unlock()
} }
} }
}(i) }(i)
@ -80,6 +125,11 @@ func main() {
log.Println("Finish sync sp_info") log.Println("Finish sync sp_info")
// Отправляем сводный алерт по ошибкам API
if checkDuplicates {
sendAPIErrorsAlert(apiErrors)
}
// Удаляем из sp_info ресурсы, которых больше нет в apps_settings // Удаляем из sp_info ресурсы, которых больше нет в apps_settings
log.Println("Start cleanup of removed SIDs") log.Println("Start cleanup of removed SIDs")
if err := cleanupRemovedSIDs(db, allSIDs); err != nil { if err := cleanupRemovedSIDs(db, allSIDs); err != nil {