feat: CID-based routing rules engine with flexible match conditions

Routing rules:
- RouteRule model: 3 match fields (caller_id, dest, ingress_trunk regex),
  3 action types (node/pool/auto), time-based (days of week, time range)
- Engine router.go: CompiledRule with pre-compiled regex,
  PickNodeForCall with cascade (dead node → next rule), pickNodeFromPool
- Separate sync.RWMutex for Router — zero contention admin vs traffic
- Validate-before-save pattern: engine validates before JSON save
- Graceful degradation: corrupted rules skipped, no rules → PickNode()

Persistence:
- data/routing.json with atomic save (tmp→rename), config.Manager
- Auto-created empty on first start

API (admin only):
- GET/POST/PUT/DELETE /api/routing/rules
- PUT /api/routing/rules/:id/toggle
- PUT /api/routing/rules/reorder

Integration:
- /api/route now uses PickNodeForCall with caller_id/dest/ingress_trunk
  query params (already sent by route.lua)
- RouteResponse includes matched_rule field for debugging
- Main.go loads routing rules on startup with graceful fallback

Web UI:
- /routing page: table with priority arrows, inline edit, create form,
  regex test tool, toggle, delete, action badges, hits counter

Tests:
- 28 tests: matchRule, time matching, PickNodeForCall cascade,
  pool scoring, validation, hit counting, compilation
This commit is contained in:
Maksim Totmin
2026-06-25 21:51:37 +07:00
parent 54d56690c6
commit 54259e51b9
13 changed files with 1928 additions and 16 deletions
+11 -1
View File
@@ -63,7 +63,16 @@ func main() {
// 3. Engine
eng := engine.NewEngine(cfg)
// 3.1 Запускаем эвикцию stale-нод (очистка нод без метрик > 5 минут, проверка каждые 60 сек)
// 3.1 Загружаем routing rules из routing.json (graceful degradation: битые правила скипаются)
rules, err := cfgMgr.ReadRoutingRules()
if err != nil {
log.Printf("[main] ошибка чтения routing.json: %v (работаем без правил)", err)
} else if len(rules) > 0 {
eng.LoadRules(rules)
log.Printf("[main] загружено %d правил маршрутизации из routing.json", len(rules))
}
// 3.2 Запускаем эвикцию stale-нод (очистка нод без метрик > 5 минут, проверка каждые 60 сек)
eng.StartEvictionLoop(60*time.Second, 5*time.Minute)
// 4. ASCII логгер метрик
@@ -257,6 +266,7 @@ func main() {
log.Printf(" GET /api/nodes — список нод")
log.Printf(" GET /api/trunks — управление транками")
log.Printf(" GET /api/users — управление пользователями")
log.Printf(" GET /api/routing/rules — правила маршрутизации (CID/dest/trunk/time)")
log.Printf(" GET /api/monitoring/zabbix — Zabbix LLD + items")
log.Printf(" GET /api/monitoring/prometheus — Prometheus метрики")
log.Printf(" WS /ws/metrics — real-time метрики на фронт")