README.md fixes: - Architecture diagram now shows pulse-lets-go-agent layer - Features list: added agent, auto-trunk, WS gateway, user-01 protection - route.lua example: replaced fake curl.fetch() with real api:execute() - systemd unit: Wants=nats.service removed (NATS on separate host) - Gateway mapping: removed grep 'pulse-' filter (misses existing gateways) - Production deployment: added agent deployment step (between NATS and route.lua) - Makefile commands: added build-agent, build-siptest, siptest-*, emulate-* AGENTS.md fixes: - Structure: added missing cmd/emulator, cmd/siptest, cmd/pulse-lets-go-agent, esl/, ami/, contrib/ - NATS model: added sip_gateway field - config.json: added nats_user, nats_password, tls, rate_limit, log, esl blocks - Internal packages: added esl/ and ami/ - route.lua name: route_call → pulse_route (matches production)
8.8 KiB
8.8 KiB
pulse-lets-go — Telephone Load Balancer
Балансировщик телефонной нагрузки на базе FreeSWITCH + NATS.
Принимает метрики от нижестоящих АТС, вычисляет score готовности,
возвращает лучшую ноду для следующего вызова через /api/route.
Стек
| Компонент | Технология |
|---|---|
| Backend | Go (stdlib net/http или Chi, nats.go, golang-jwt, bcrypt) |
| Frontend | SvelteKit + Tailwind + shadcn-svelte |
| Auth | JWT access/refresh, bcrypt, роли: admin / viewer |
| Шина | NATS — pulse.metrics.<node_id> |
| Хранилище | JSON (data/config.json, trunks.json, users.json) + ASCII-лог метрик |
| Деплой | systemd + единый бинарник, без Docker |
Модели данных
NATS message (node → balancer, каждые 5 сек)
{
"node_id": "pbx-01",
"ts": 1719000000,
"status": "ok",
"active_calls": 42,
"max_calls": 250,
"idle_cpu": 35,
"load_avg": 0.85,
"call_failure_rate": 0.5,
"sip_gateway": "sip:pbx-01.lan:5060"
}
call_failure_rate: 0.0..100.0 (проценты).
In-memory node state (engine)
Все поля метрики + дополнительные:
disabled(bool) — ручное исключение админомdisabled_reason(string)
Scoring Engine
Lethal (score → -100, нода исключается)
status != "ok"
stale > stale_threshold_sec (по умолчанию 20 сек)
active_calls >= max_calls
idle_cpu < idle_cpu_min (по умолчанию 5)
disabled == true
call_failure_rate > call_failure_rate_lethal (по умолчанию 15.0)
Weighted score
call_score = min(100, max(0, 100 - (active_calls / max_calls * 100)))
load_score = min(100, max(0, 100 - (load_avg * 50)))
idle_score = min(100, max(0, idle_cpu))
fail_score = min(100, max(0, 100 - call_failure_rate))
score = call_score * 0.40 + load_score * 0.30 + idle_score * 0.20 + fail_score * 0.10
Веса и пороги конфигурируются в config.json.
Оптимизация
best_node_id + best_score + fallback_active кэшируются, обновляются при каждой метрике.
/api/route читает их за RLock, O(1).
HTTP API
Auth
POST /api/auth/login→ JWT- Все эндпоинты (кроме login и
/api/monitoring/*) требуютAuthorization: Bearer <jwt> /api/monitoring/*— API-key в заголовкеX-API-Key
Core
| Метод | Путь | Роль | Описание |
|---|---|---|---|
GET |
/api/route?caller_id=X&dest=Y&ingress_trunk=Z |
– | Выбор ноды для звонка (без JWT) |
Response (успех):
{ "node_id": "pbx-03", "score": 88, "sip_gateway": "sip:pbx03.lan:5060" }
Response (fallback — все ноды unhealthy):
{
"fallback": true,
"sip_gateway": "sip:operator.lan:5060",
"reason": "all_nodes_unhealthy",
"nodes": [...]
}
Response (нет нод вообще):
{ "error": "no_nodes_registered" }
Nodes
| Метод | Путь | Роль | Описание |
|---|---|---|---|
GET |
/api/nodes |
admin/viewer | Список нод со score |
GET |
/api/nodes/:id/metrics |
admin/viewer | История метрик (ring buffer) |
PUT |
/api/nodes/:id/toggle |
admin | {"disabled": true/false, "reason": "..."} |
Trunks
| Метод | Путь | Роль | Описание |
|---|---|---|---|
GET |
/api/trunks |
admin | Все транки |
GET |
/api/trunks?type=ingress|balance|fallback |
admin | Фильтр по типу |
POST |
/api/trunks |
admin | Создать |
PUT |
/api/trunks/:id |
admin | Обновить |
DELETE |
/api/trunks/:id |
admin | Удалить |
Users (admin only)
| Метод | Путь | Описание |
|---|---|---|
GET |
/api/users |
Список |
POST |
/api/users |
Создать |
PUT |
/api/users/:id |
Обновить |
DELETE |
/api/users/:id |
Удалить |
Monitoring (API-key, не JWT)
| Метод | Путь | Описание |
|---|---|---|
GET |
/api/monitoring/zabbix |
JSON для Zabbix LLD + items |
GET |
/api/monitoring/prometheus |
text/plain метрики |
WebSocket
| Метод | Путь | Описание |
|---|---|---|
WS |
/ws/metrics |
Real-time метрики на фронт (JWT в query param) |
Терминология транков
| Тип | Назначение | Сколько |
|---|---|---|
ingress |
Входящий транк (откуда звонки приходят) | 1..N |
balance |
Транк назначения (куда балансятся, привязан к Node) | 1..N |
fallback |
Резервный транк оператора (когда все balance score < 0) | ровно 1 |
Модель транка:
{
"id": "trk-001",
"name": "pbx-03 balance",
"type": "ingress|balance|fallback",
"node_id": "pbx-03",
"gateway": "sip:pbx03.lan:5060",
"codecs": ["PCMU", "PCMA"],
"context": "default",
"enabled": true,
"description": "",
"created_at": "...",
"updated_at": "..."
}
FreeSWITCH интеграция
FS использует Lua скрипт в dialplan → mod_curl дёргает /api/route → получает JSON → парсит sip_gateway → bridge.
<extension name="pulse_route">
<condition field="destination_number" expression="^.*$">
<action application="lua" data="route.lua"/>
</condition>
</extension>
route.lua: GET /api/route?... → если fallback: true идёт на fallback gateway, иначе на sip_gateway.
config.json
{
"nats_url": "nats://localhost:4222",
"nats_user": "",
"nats_password": "",
"listen_addr": ":8080",
"jwt_secret": "auto-generated-on-first-run",
"monitoring_api_key": "auto-generated-on-first-run",
"stale_threshold_sec": 20,
"scoring": {
"idle_cpu_min": 5,
"call_failure_rate_lethal": 15.0,
"weights": {
"call_score": 0.40,
"load_score": 0.30,
"idle_score": 0.20,
"fail_score": 0.10
}
},
"tls": {
"enabled": false,
"cert_file": "",
"key_file": ""
},
"rate_limit": {
"enabled": true,
"route_per_sec": 5000,
"api_per_sec": 100
},
"log": {
"level": "info",
"format": "text"
},
"esl": {
"host": "",
"port": 8021,
"password": "",
"password_env": "",
"sofia_profile": "external",
"gateway_prefix": "pulse"
}
}
Структура проекта
pulse-lets-go/
├── cmd/pulse-lets-go/main.go
├── cmd/pulse-lets-go-agent/
├── cmd/emulator/
├── cmd/siptest/
├── internal/
│ ├── api/ — handlers: auth, nodes, trunks, users, monitoring, ws
│ ├── engine/ — scorer + router (sync.RWMutex, best_node кэш)
│ ├── nats/ — subscriber, in-memory node store
│ ├── esl/ — FreeSWITCH ESL client (raw TCP)
│ ├── ami/ — Asterisk AMI client (raw TCP)
│ ├── config/ — manager: чтение/атомарная запись JSON
│ ├── models/ — все общие типы
│ └── log/ — ASCII-metrics логгер + ротация
├── contrib/ — route.lua, agent конфиги
├── data/ — JSON конфиги (gitignored)
├── web/ — SvelteKit + shadcn-svelte + Tailwind
│ ├── src/routes/
│ │ ├── +layout.svelte
│ │ ├── +page.svelte # дашборд
│ │ ├── login/+page.svelte
│ │ ├── trunks/+page.svelte
│ │ ├── nodes/+page.svelte
│ │ └── users/+page.svelte
│ ├── src/lib/
│ │ ├── components/
│ │ └── api.ts
│ ├── svelte.config.js
│ ├── tailwind.config.js
│ └── package.json
├── go.mod
├── Makefile
└── AGENTS.md
Порядок реализации
cmd/pulse-lets-go/main.go+ Go module + пустая структура internalinternal/models/— все типы данныхinternal/config/— чтение/запись JSON, атомарное сохранениеinternal/nats/— подписка, in-memory storeinternal/engine/— scorer + routerinternal/api/— все хендлерыinternal/log/— ASCII-логгер + ring buffer- Web-фронт (SvelteKit)
- Makefile + systemd unit
- Тесты + graceful shutdown