Fix alerts and change make names for dashs

This commit is contained in:
Magnus Root 2026-06-04 12:06:59 +03:00
parent 8464a41b6e
commit a9a329b8ab
22 changed files with 60 additions and 15 deletions

0
.gitignore vendored Normal file → Executable file
View file

0
ARCHITECTURE.md Normal file → Executable file
View file

34
DATABASE_SCHEMA.md Normal file → Executable file
View file

@ -52,8 +52,8 @@ SELECT sid, domain_name, aliases FROM sp_info WHERE sid IS NOT NULL LIMIT 3;
|---|---|---|---| |---|---|---|---|
| `l7resourceid` | `text` | — | SID ресурса (FK к `sp_info.sid` / `manual_info.sid`) | | `l7resourceid` | `text` | — | SID ресурса (FK к `sp_info.sid` / `manual_info.sid`) |
| `client_title` | `text` | — | Название клиента — имя тенанта в дашборде | | `client_title` | `text` | — | Название клиента — имя тенанта в дашборде |
| `four_hundred` | `integer` | 20 | Порог 4xx ошибок в % для данного SID | | `four_hundred` | `integer` | 20 | Порог 4xx ошибок в % для данного SID. `100` = алерт не создавать |
| `five_hundred` | `integer` | 10 | Порог 5xx ошибок в % для данного SID | | `five_hundred` | `integer` | 10 | Порог 5xx ошибок в % для данного SID. `100` = алерт не создавать |
| `ptaf_fallback_code_alert_count` | `integer` | 1 | Порог WAF block алерта в штуках за минуту | | `ptaf_fallback_code_alert_count` | `integer` | 1 | Порог WAF block алерта в штуках за минуту |
**Пример:** **Пример:**
@ -85,6 +85,7 @@ FROM apps_settings LIMIT 5;
| `rps_commercial_limit` | `integer` | NULL | Коммерческий лимит RPS — синяя линия на графике. NULL → 100 | | `rps_commercial_limit` | `integer` | NULL | Коммерческий лимит RPS — синяя линия на графике. NULL → 100 |
| `waf_provider` | `text` | NULL | Тип провайдера WAF. NULL = клиент игнорируется | | `waf_provider` | `text` | NULL | Тип провайдера WAF. NULL = клиент игнорируется |
| `ptaf_fallback_code` | `text` | NULL | HTTP код блокировки PTAF (`418`, `403` и т.д.). `pass` или NULL = WAF block алерт не создавать | | `ptaf_fallback_code` | `text` | NULL | HTTP код блокировки PTAF (`418`, `403` и т.д.). `pass` или NULL = WAF block алерт не создавать |
| `parent_client_title` | `text` | NULL | Родительский клиент для группировки. Если задан — клиент объединяется с другими клиентами с тем же `parent_client_title` в одну секцию дашборда |
**Значения `waf_provider`:** **Значения `waf_provider`:**
@ -205,6 +206,31 @@ UPDATE client_info SET waf_provider = NULL WHERE client_title = 'Названи
> После смены `waf_provider` при следующем запуске генератора старые алерты будут удалены автоматически и созданы новые в нужной группе. > После смены `waf_provider` при следующем запуске генератора старые алерты будут удалены автоматически и созданы новые в нужной группе.
### Группировка клиентов
```sql
-- Объединить двух клиентов в одну секцию дашборда
UPDATE client_info SET parent_client_title = 'BigGroup'
WHERE client_title IN ('client_a', 'client_b');
-- Убрать группировку
UPDATE client_info SET parent_client_title = NULL
WHERE client_title = 'client_a';
```
> Клиенты с одинаковым `parent_client_title` объединяются в одну строку дашборда.
> Все их домены (SID) попадают в одну секцию. `RPSLimit` берётся максимальный по группе.
### Отключение алертов по ошибкам для конкретного SID
```sql
-- Отключить алерты по 4xx для конкретного домена (значение 100 = не создавать алерт)
UPDATE apps_settings SET four_hundred = 100 WHERE l7resourceid = 'SID12345';
-- Отключить алерты по 5xx для конкретного домена
UPDATE apps_settings SET five_hundred = 100 WHERE l7resourceid = 'SID12345';
```
### Настройка WAF block алерта ### Настройка WAF block алерта
```sql ```sql
@ -283,6 +309,8 @@ ORDER BY a.client_title;
| Новый SID в `sp_info`/`manual_info` + запись в `apps_settings` | Новый домен добавится в строку клиента | | Новый SID в `sp_info`/`manual_info` + запись в `apps_settings` | Новый домен добавится в строку клиента |
| Новый `client_title` + `waf_provider` в `client_info` | Новая строка клиента в соответствующем дашборде | | Новый `client_title` + `waf_provider` в `client_info` | Новая строка клиента в соответствующем дашборде |
| Изменение `waf_provider` (`spik``sp` или наоборот) | Клиент переносится в другой дашборд, старые алерты удаляются, создаются новые | | Изменение `waf_provider` (`spik``sp` или наоборот) | Клиент переносится в другой дашборд, старые алерты удаляются, создаются новые |
| Установка `parent_client_title` | Клиент объединяется с другими клиентами группы в одну секцию дашборда |
| `four_hundred = 100` или `five_hundred = 100` | Алерты по соответствующему коду для данного SID не создаются |
| Установка `waf_provider = NULL` | Клиент исчезает из всех дашбордов, алерты удаляются | | Установка `waf_provider = NULL` | Клиент исчезает из всех дашбордов, алерты удаляются |
| Изменение `rps_limit` | Пересоздание алерта RPS, обновление красной линии на графике | | Изменение `rps_limit` | Пересоздание алерта RPS, обновление красной линии на графике |
| Изменение `rps_commercial_limit` | Обновление синей линии на графике | | Изменение `rps_commercial_limit` | Обновление синей линии на графике |
@ -294,4 +322,4 @@ ORDER BY a.client_title;
| Удаление из `apps_settings` | Клиент/домен исчезает из дашборда, алерты удаляются автоматически | | Удаление из `apps_settings` | Клиент/домен исчезает из дашборда, алерты удаляются автоматически |
> Изменения в БД обнаруживаются через хэш (`alerts_hash` в `state.json`). При изменении любого поля — алерты пересоздаются автоматически при следующем запуске. > Изменения в БД обнаруживаются через хэш (`alerts_hash` в `state.json`). При изменении любого поля — алерты пересоздаются автоматически при следующем запуске.
> Устаревшие алерты удаляются автоматически через `deleteObsoleteAlerts()`. > Устаревшие алерты удаляются автоматически через `deleteObsoleteAlerts()`.

0
LICENSE Normal file → Executable file
View file

0
README.md Normal file → Executable file
View file

4
alerts.go Normal file → Executable file
View file

@ -357,7 +357,7 @@ func generateAndSendAlerts(clients map[string]ClientData, tmpl AlertRulesTemplat
// Генерируем для response_status_code и upstream_status_code // Генерируем для response_status_code и upstream_status_code
for _, domain := range t.client.Domains { for _, domain := range t.client.Domains {
for _, statusField := range []string{"response_status_code", "upstream_status"} { for _, statusField := range []string{"response_status_code", "upstream_status"} {
if domain.Limit4xx > 0 { if domain.Limit4xx > 0 && domain.Limit4xx < 100 {
for _, rule := range buildErrorRateAlertRules(t.client, domain, 4, domain.Limit4xx, statusField, tmpl, config) { for _, rule := range buildErrorRateAlertRules(t.client, domain, 4, domain.Limit4xx, statusField, tmpl, config) {
rule["folderUID"] = folderUID rule["folderUID"] = folderUID
tasks = append(tasks, alertTask{ tasks = append(tasks, alertTask{
@ -366,7 +366,7 @@ func generateAndSendAlerts(clients map[string]ClientData, tmpl AlertRulesTemplat
}) })
} }
} }
if domain.Limit5xx > 0 { if domain.Limit5xx > 0 && domain.Limit5xx < 100 {
for _, rule := range buildErrorRateAlertRules(t.client, domain, 5, domain.Limit5xx, statusField, tmpl, config) { for _, rule := range buildErrorRateAlertRules(t.client, domain, 5, domain.Limit5xx, statusField, tmpl, config) {
rule["folderUID"] = folderUID rule["folderUID"] = folderUID
tasks = append(tasks, alertTask{ tasks = append(tasks, alertTask{

4
alerts_vl.go Normal file → Executable file
View file

@ -405,7 +405,7 @@ func generateAndSendAlertsVL(clients map[string]ClientData, tmpl AlertRulesTempl
// Генерируем для response_status_code и upstream_status_code // Генерируем для response_status_code и upstream_status_code
for _, domain := range client.Domains { for _, domain := range client.Domains {
for _, statusField := range []string{"response_status_code", "upstream_status"} { for _, statusField := range []string{"response_status_code", "upstream_status"} {
if domain.Limit4xx > 0 { if domain.Limit4xx > 0 && domain.Limit4xx < 100 {
for _, rule := range buildErrorRateAlertRulesVL(client, domain, 4, domain.Limit4xx, statusField, tmpl, config) { for _, rule := range buildErrorRateAlertRulesVL(client, domain, 4, domain.Limit4xx, statusField, tmpl, config) {
rule["folderUID"] = folderUID rule["folderUID"] = folderUID
tasks = append(tasks, alertTask{ tasks = append(tasks, alertTask{
@ -414,7 +414,7 @@ func generateAndSendAlertsVL(clients map[string]ClientData, tmpl AlertRulesTempl
}) })
} }
} }
if domain.Limit5xx > 0 { if domain.Limit5xx > 0 && domain.Limit5xx < 100 {
for _, rule := range buildErrorRateAlertRulesVL(client, domain, 5, domain.Limit5xx, statusField, tmpl, config) { for _, rule := range buildErrorRateAlertRulesVL(client, domain, 5, domain.Limit5xx, statusField, tmpl, config) {
rule["folderUID"] = folderUID rule["folderUID"] = folderUID
tasks = append(tasks, alertTask{ tasks = append(tasks, alertTask{

0
config.go Normal file → Executable file
View file

0
dashboard.go Normal file → Executable file
View file

0
dashboard_vl.go Normal file → Executable file
View file

33
database.go Normal file → Executable file
View file

@ -42,7 +42,8 @@ func fetchClientsData(db *sql.DB) (map[string]ClientData, error) {
COALESCE(a.five_hundred, 10) as limit_5xx, COALESCE(a.five_hundred, 10) as limit_5xx,
COALESCE(a.ptaf_fallback_code_alert_count, 1) as ptaf_fallback_code_alert_count, COALESCE(a.ptaf_fallback_code_alert_count, 1) as ptaf_fallback_code_alert_count,
COALESCE(MAX(ci.waf_provider) OVER (PARTITION BY a.client_title), '') as waf_provider, COALESCE(MAX(ci.waf_provider) OVER (PARTITION BY a.client_title), '') as waf_provider,
COALESCE(MAX(ci.ptaf_fallback_code) OVER (PARTITION BY a.client_title), '') as ptaf_fallback_code COALESCE(MAX(ci.ptaf_fallback_code) OVER (PARTITION BY a.client_title), '') as ptaf_fallback_code,
COALESCE(MAX(ci.parent_client_title) OVER (PARTITION BY a.client_title), '') as parent_client_title
FROM ( FROM (
SELECT sid, domain_name, aliases FROM sp_info WHERE sid IS NOT NULL SELECT sid, domain_name, aliases FROM sp_info WHERE sid IS NOT NULL
UNION UNION
@ -63,28 +64,44 @@ func fetchClientsData(db *sql.DB) (map[string]ClientData, error) {
clients := make(map[string]ClientData) clients := make(map[string]ClientData)
for rows.Next() { for rows.Next() {
var clientTitle, sid, domainName, aliasesJSON, wafProvider, ptafFallbackCode string var clientTitle, sid, domainName, aliasesJSON, wafProvider, ptafFallbackCode, parentClientTitle string
var rpsLimit, rpsCommercialLimit, limit4xx, limit5xx, ptafFallbackCodeAlertCount int var rpsLimit, rpsCommercialLimit, limit4xx, limit5xx, ptafFallbackCodeAlertCount int
if err := rows.Scan(&clientTitle, &sid, &domainName, &aliasesJSON, &rpsLimit, &rpsCommercialLimit, &limit4xx, &limit5xx, &ptafFallbackCodeAlertCount, &wafProvider, &ptafFallbackCode); err != nil { if err := rows.Scan(&clientTitle, &sid, &domainName, &aliasesJSON, &rpsLimit, &rpsCommercialLimit, &limit4xx, &limit5xx, &ptafFallbackCodeAlertCount, &wafProvider, &ptafFallbackCode, &parentClientTitle); err != nil {
return nil, fmt.Errorf("scan failed: %w", err) return nil, fmt.Errorf("scan failed: %w", err)
} }
// Распарсить JSON массив aliases // Распарсить JSON массив aliases
aliasesStr := parseAliasesJSON(aliasesJSON) aliasesStr := parseAliasesJSON(aliasesJSON)
if _, exists := clients[clientTitle]; !exists { // Если задан parent_client_title — группировать по нему
clients[clientTitle] = ClientData{ groupKey := clientTitle
ClientTitle: clientTitle, if parentClientTitle != "" {
groupKey = parentClientTitle
}
if _, exists := clients[groupKey]; !exists {
clients[groupKey] = ClientData{
ClientTitle: groupKey,
Domains: []DomainInfo{}, Domains: []DomainInfo{},
RPSLimit: rpsLimit, RPSLimit: rpsLimit,
RPSCommercialLimit: rpsCommercialLimit, RPSCommercialLimit: rpsCommercialLimit,
WafProvider: wafProvider, WafProvider: wafProvider,
PtafFallbackCode: ptafFallbackCode, PtafFallbackCode: ptafFallbackCode,
} }
} else {
// Обновляем лимиты — берём максимальные по группе
existing := clients[groupKey]
if rpsLimit > existing.RPSLimit {
existing.RPSLimit = rpsLimit
}
if rpsCommercialLimit > existing.RPSCommercialLimit {
existing.RPSCommercialLimit = rpsCommercialLimit
}
clients[groupKey] = existing
} }
client := clients[clientTitle] client := clients[groupKey]
client.Domains = append(client.Domains, DomainInfo{ client.Domains = append(client.Domains, DomainInfo{
SID: sid, SID: sid,
DomainName: domainName, DomainName: domainName,
@ -93,7 +110,7 @@ func fetchClientsData(db *sql.DB) (map[string]ClientData, error) {
Limit5xx: limit5xx, Limit5xx: limit5xx,
PtafFallbackCodeAlertCount: ptafFallbackCodeAlertCount, PtafFallbackCodeAlertCount: ptafFallbackCodeAlertCount,
}) })
clients[clientTitle] = client clients[groupKey] = client
} }
return clients, rows.Err() return clients, rows.Err()

0
env.go Normal file → Executable file
View file

0
git.go Normal file → Executable file
View file

0
go.mod Normal file → Executable file
View file

0
go.sum Normal file → Executable file
View file

0
grafana.go Normal file → Executable file
View file

0
main.go Normal file → Executable file
View file

0
panels.go Normal file → Executable file
View file

0
panels_vl.go Normal file → Executable file
View file

0
state.go Normal file → Executable file
View file

0
template.go Normal file → Executable file
View file

0
template_validator.go Normal file → Executable file
View file