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)
283 lines
8.8 KiB
Markdown
283 lines
8.8 KiB
Markdown
# 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 сек)
|
||
|
||
```json
|
||
{
|
||
"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 (успех):
|
||
```json
|
||
{ "node_id": "pbx-03", "score": 88, "sip_gateway": "sip:pbx03.lan:5060" }
|
||
```
|
||
|
||
Response (fallback — все ноды unhealthy):
|
||
```json
|
||
{
|
||
"fallback": true,
|
||
"sip_gateway": "sip:operator.lan:5060",
|
||
"reason": "all_nodes_unhealthy",
|
||
"nodes": [...]
|
||
}
|
||
```
|
||
|
||
Response (нет нод вообще):
|
||
```json
|
||
{ "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 |
|
||
|
||
Модель транка:
|
||
```json
|
||
{
|
||
"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
|
||
|
||
```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
|
||
```
|
||
|
||
## Порядок реализации
|
||
|
||
1. `cmd/pulse-lets-go/main.go` + Go module + пустая структура internal
|
||
2. `internal/models/` — все типы данных
|
||
3. `internal/config/` — чтение/запись JSON, атомарное сохранение
|
||
4. `internal/nats/` — подписка, in-memory store
|
||
5. `internal/engine/` — scorer + router
|
||
6. `internal/api/` — все хендлеры
|
||
7. `internal/log/` — ASCII-логгер + ring buffer
|
||
8. Web-фронт (SvelteKit)
|
||
9. Makefile + systemd unit
|
||
10. Тесты + graceful shutdown
|