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

32
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`) |
| `client_title` | `text` | — | Название клиента — имя тенанта в дашборде |
| `four_hundred` | `integer` | 20 | Порог 4xx ошибок в % для данного SID |
| `five_hundred` | `integer` | 10 | Порог 5xx ошибок в % для данного SID |
| `four_hundred` | `integer` | 20 | Порог 4xx ошибок в % для данного SID. `100` = алерт не создавать |
| `five_hundred` | `integer` | 10 | Порог 5xx ошибок в % для данного SID. `100` = алерт не создавать |
| `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 |
| `waf_provider` | `text` | NULL | Тип провайдера WAF. NULL = клиент игнорируется |
| `ptaf_fallback_code` | `text` | NULL | HTTP код блокировки PTAF (`418`, `403` и т.д.). `pass` или NULL = WAF block алерт не создавать |
| `parent_client_title` | `text` | NULL | Родительский клиент для группировки. Если задан — клиент объединяется с другими клиентами с тем же `parent_client_title` в одну секцию дашборда |
**Значения `waf_provider`:**
@ -205,6 +206,31 @@ UPDATE client_info SET waf_provider = NULL WHERE client_title = 'Названи
> После смены `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 алерта
```sql
@ -283,6 +309,8 @@ ORDER BY a.client_title;
| Новый SID в `sp_info`/`manual_info` + запись в `apps_settings` | Новый домен добавится в строку клиента |
| Новый `client_title` + `waf_provider` в `client_info` | Новая строка клиента в соответствующем дашборде |
| Изменение `waf_provider` (`spik``sp` или наоборот) | Клиент переносится в другой дашборд, старые алерты удаляются, создаются новые |
| Установка `parent_client_title` | Клиент объединяется с другими клиентами группы в одну секцию дашборда |
| `four_hundred = 100` или `five_hundred = 100` | Алерты по соответствующему коду для данного SID не создаются |
| Установка `waf_provider = NULL` | Клиент исчезает из всех дашбордов, алерты удаляются |
| Изменение `rps_limit` | Пересоздание алерта RPS, обновление красной линии на графике |
| Изменение `rps_commercial_limit` | Обновление синей линии на графике |

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
for _, domain := range t.client.Domains {
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) {
rule["folderUID"] = folderUID
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) {
rule["folderUID"] = folderUID
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
for _, domain := range client.Domains {
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) {
rule["folderUID"] = folderUID
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) {
rule["folderUID"] = folderUID
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.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.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 (
SELECT sid, domain_name, aliases FROM sp_info WHERE sid IS NOT NULL
UNION
@ -63,28 +64,44 @@ func fetchClientsData(db *sql.DB) (map[string]ClientData, error) {
clients := make(map[string]ClientData)
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
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)
}
// Распарсить JSON массив aliases
aliasesStr := parseAliasesJSON(aliasesJSON)
if _, exists := clients[clientTitle]; !exists {
clients[clientTitle] = ClientData{
ClientTitle: clientTitle,
// Если задан parent_client_title — группировать по нему
groupKey := clientTitle
if parentClientTitle != "" {
groupKey = parentClientTitle
}
if _, exists := clients[groupKey]; !exists {
clients[groupKey] = ClientData{
ClientTitle: groupKey,
Domains: []DomainInfo{},
RPSLimit: rpsLimit,
RPSCommercialLimit: rpsCommercialLimit,
WafProvider: wafProvider,
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{
SID: sid,
DomainName: domainName,
@ -93,7 +110,7 @@ func fetchClientsData(db *sql.DB) (map[string]ClientData, error) {
Limit5xx: limit5xx,
PtafFallbackCodeAlertCount: ptafFallbackCodeAlertCount,
})
clients[clientTitle] = client
clients[groupKey] = client
}
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