docs: fix 12 documentation inaccuracies in README.md and AGENTS.md

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)
This commit is contained in:
Maksim Totmin 2026-06-25 17:16:03 +07:00
parent 13c88c1586
commit e66ac27dd9
2 changed files with 134 additions and 68 deletions

View File

@ -28,7 +28,8 @@
"max_calls": 250,
"idle_cpu": 35,
"load_avg": 0.85,
"call_failure_rate": 0.5
"call_failure_rate": 0.5,
"sip_gateway": "sip:pbx-01.lan:5060"
}
```
@ -174,7 +175,7 @@ Response (нет нод вообще):
FS использует Lua скрипт в dialplan → `mod_curl` дёргает `/api/route` → получает JSON → парсит `sip_gateway` → bridge.
```
<extension name="route_call">
<extension name="pulse_route">
<condition field="destination_number" expression="^.*$">
<action application="lua" data="route.lua"/>
</condition>
@ -188,6 +189,8 @@ route.lua: GET `/api/route?...` → если `fallback: true` идёт на fall
```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",
@ -201,6 +204,28 @@ route.lua: GET `/api/route?...` → если `fallback: true` идёт на fall
"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"
}
}
```
@ -210,13 +235,19 @@ route.lua: GET `/api/route?...` → если `fallback: true` идёт на fall
```
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/
@ -224,7 +255,8 @@ pulse-lets-go/
│ │ ├── +page.svelte # дашборд
│ │ ├── login/+page.svelte
│ │ ├── trunks/+page.svelte
│ │ └── nodes/+page.svelte
│ │ ├── nodes/+page.svelte
│ │ └── users/+page.svelte
│ ├── src/lib/
│ │ ├── components/
│ │ └── api.ts

164
README.md
View File

@ -13,13 +13,17 @@
## Features
- **pulse-lets-go-agent** — агент сбора метрик для FreeSWITCH (ESL) и Asterisk (AMI). Работает на каждой PBX-ноде.
- **Scoring engine** — lethal checks (disabled, stale, overload, CPU, failure rate) + weighted score (call, load, idle, fail) с конфигурируемыми весами.
- **NATS шина** — асинхронный сбор метрик через `pulse.metrics.<node_id>`.
- **REST API** — JWT (access + refresh), роли admin/viewer, rate limiting, CORS.
- **Auto-создание транков** — при получении первой метрики от новой ноды с `sip_gateway` создаётся balance-транк автоматически.
- **Gateway health WS** — статус FS-gateway (registered/unregistered) транслируется на фронт через WebSocket.
- **REST API** — JWT (access + refresh), роли admin/viewer, rate limiting, CORS, health endpoints.
- **WebSocket** — real-time трансляция метрик на SvelteKit дашборд.
- **FreeSWITCH ESL** — управление Sofia-шлюзами через raw TCP ESL.
- **Monitoring** — Prometheus и Zabbix эндпоинты с отдельным API-key.
- **SvelteKit UI** — дашборд, таблицы нод, транков, пользователей.
- **Защита пользователей** — первичный администратор (user-01) не может быть удалён или понижен до viewer.
- **Graceful shutdown** — корректное завершение с таймаутом 10 с.
- **systemd ready** — unit с изоляцией (NoNewPrivileges, ProtectSystem, PrivateTmp).
@ -28,46 +32,54 @@
## Архитектура
```
┌────────────────┐ pulse.metrics.<id> ┌──────────────────┐
│ PBX-01 │────── every 5s ────────────▶ │ │
│ (FS node) │ │ NATS Server │
├────────────────┤ │ nats://:4222 │
│ PBX-02 │────── every 5s ────────────▶ │ │
│ (FS node) │ └────────┬─────────┘
├────────────────┤ │
│ PBX-03 │────── every 5s ────────────▶ │
│ (FS node) │ │
└────────────────┘ ▼
┌──────────────────────┐
│ pulse-lets-go │
│ │
│ ┌────────────────┐ │
┌───────▶│ NATS Sub │ │
│ │ (pulse.metrics)│ │
│ └────────┬───────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Engine │ │
│ │ (scoring) │──│──▶ /api/route
│ └────────┬───────┘ │ ↓
│ ▼ │ FreeSWITCH bridge
│ ┌────────────────┐ │ sip:pbx-03:5060
│ │ REST API │──│──▶ /api/nodes
│ │ (JWT auth) │ │ /api/trunks
│ └────────┬───────┘ │ /api/users
│ ▼ │
│ ┌────────────────┐ │
│ │ WebSocket │──│──▶ SvelteKit UI
│ └────────────────┘ │ (ws://:8080/ws/metrics)
│ ┌────────────────┐ │
│ │ ESL Client │──│──▶ FreeSWITCH ESL
│ │ (gateways) │ │ :8021
│ └────────────────┘ │
│ ┌────────────────┐ │
│ │ Monitoring │──│──▶ /api/monitoring/prometheus
│ │ (API-key) │ │ /api/monitoring/zabbix
│ └────────────────┘ │
└──────────────────────┘
┌─────────────────────┐ ESL/AMI ┌──────────────────────┐
│ PBX-01 │──────────▶│ pulse-lets-go-agent │
│ (FS or Asterisk) │ collect │ (on each PBX node) │
├─────────────────────┤ ├──────────────────────┤
│ PBX-02 │──────────▶│ pulse-lets-go-agent │
│ (FS or Asterisk) │ ├──────────────────────┤
├─────────────────────┤ ├──────────────────────┤
│ PBX-N │──────────▶│ pulse-lets-go-agent │
│ (FS or Asterisk) │ └─────────┬────────────┘
└─────────────────────┘ │ pulse.metrics.<id>
│ every 5s
┌──────────────┐
│ NATS Server │
│ nats://:4222 │
└──────┬───────┘
┌──────────────────────┐
│ pulse-lets-go │
│ │
│ ┌────────────────┐ │
│ │ NATS Sub │ │
│ │ (pulse.metrics)│ │
│ └────────┬───────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Engine │ │
│ │ (scoring) │──│──▶ /api/route
│ └────────┬───────┘ │ ↓
│ ▼ │ FreeSWITCH bridge
│ ┌────────────────┐ │ sip:pbx-03:5060
│ │ REST API │──│──▶ /api/nodes
│ │ (JWT auth) │ │ /api/trunks
│ └────────┬───────┘ │ /api/users
│ ▼ │
│ ┌────────────────┐ │
│ │ WebSocket │──│──▶ SvelteKit UI
│ └────────────────┘ │ (ws://:8080/ws/metrics)
│ ┌────────────────┐ │
│ │ ESL Client │──│──▶ FreeSWITCH ESL
│ │ (gateways) │ │ :8021
│ └────────────────┘ │
│ ┌────────────────┐ │
│ │ Monitoring │──│──▶ /api/monitoring/prometheus
│ │ (API-key) │ │ /api/monitoring/zabbix
│ └────────────────┘ │
└──────────────────────┘
```
---
@ -336,20 +348,24 @@ curl -H "X-API-Key: $(jq -r '.monitoring_api_key' data/config.json)" \
FreeSWITCH использует Lua-скрипт в dialplan. Скрипт дёргает `/api/route` через `mod_curl` и бриджится на полученный gateway.
```lua
-- contrib/route.lua (упрощённо)
local url = string.format("%s/api/route?caller_id=%s&dest=%s&ingress_trunk=%s",
session:getVariable("balancer_url"),
session:getVariable("caller_id_number"),
session:getVariable("destination_number"),
session:getVariable("ingress_trunk"))
-- contrib/route.lua (сокращённо)
api = freeswitch.API()
caller_id = session:getVariable("caller_id_number") or ""
dest = session:getVariable("destination_number") or ""
url = "http://localhost:8080/api/route?caller_id=" .. caller_id .. "&dest=" .. dest
local response = curl.fetch({url = url, timeout = tonumber(route_timeout)})
local ok, result = cjson.decode(response.body)
raw = api:execute("curl", url)
body = raw:match("\r?\n\r?\n(.+)") or raw
if result.fallback then
session:bridge(result.sip_gateway)
local ok, route = pcall(cjson.decode, body)
if not ok then session:hangup("NORMAL_TEMPORARY_FAILURE") return end
if route.fallback then
session:bridge(route.sip_gateway) -- оператор
elseif route.sip_gateway then
session:bridge(route.sip_gateway) -- лучшая нода
else
session:bridge(result.sip_gateway)
session:hangup("NORMAL_TEMPORARY_FAILURE")
end
```
@ -415,7 +431,7 @@ make install-systemd # устанавливает systemd unit
# deploy/pulse-lets-go.service
[Unit]
Description=Pulse Lets Go — Telephone Load Balancer
Wants=nats.service
After=network.target
[Service]
User=pulse
@ -498,13 +514,28 @@ sudo mkdir -p /opt/nats
# Запуск: nats-server -p 4222 -D &
```
**Шаг 3 — route.lua в FS scripts**
**Шаг 3 — Деплой agent на PBX-ноды** (повторить для каждой UC/Asterisk ноды)
```bash
# Скопировать бинарник и конфиг на каждую PBX:
scp bin/pulse-lets-go-agent devadmin@PBX_IP:/usr/local/bin/
scp contrib/agent-uc.json devadmin@PBX_IP:/etc/pulse-lets-go-agent/agent.json
# Запустить:
ssh devadmin@PBX_IP "systemctl start pulse-lets-go-agent"
# Проверить лог:
# journalctl -u pulse-lets-go-agent -f
# → [ami] подключён к 127.0.0.1:6154
# → [agent] опубликована метрика: calls=0/150 load=0.28 cpu=99% status=ok
```
**Шаг 4 — route.lua в FS scripts**
```bash
sudo mkdir -p /etc/freeswitch/scripts
sudo cp /tmp/route.lua /etc/freeswitch/scripts/
```
**Шаг 4 — Dialplan** (добавить `pulse_route` extension)
**Шаг 5 — Dialplan** (добавить `pulse_route` extension)
```xml
<!-- В /etc/freeswitch/dialplan/default.xml перед остальными extension -->
<extension name="pulse_route">
@ -518,17 +549,18 @@ sudo cp /tmp/route.lua /etc/freeswitch/scripts/
sudo fs_cli -x "reloadxml"
```
**Шаг 5 — Маппинг существующих FS gateway в pulse-lets-go транки**
**Шаг 6 — Создание balance-транков для каждой PBX-ноды**
```bash
# Получить список gateway из FS:
echo "api sofia profile external gwlist" | nc 127.0.0.1 8021 | grep "pulse-"
# → список имён gateway
# Получить список gateway из FS (или список нод из конфига):
echo "api sofia profile external gwlist" | nc 127.0.0.1 8021
# Создать balance-транки для каждого gateway через API:
# POST /api/trunks {"name":"uc-69 баланс","type":"balance","node_id":"uc-69","gateway":"sip:10.x.x.x:5060"}
# Создать balance-транк для каждой UC-ноды:
curl -X POST .../api/trunks
-d '{"name":"uc06","type":"balance","node_id":"uc06","gateway":"sip:10.101.60.115:5060","enabled":true}'
```
Транки также создаются автоматически при получении первой метрики от агента (если в метрике передан `sip_gateway`).
**Шаг 6 — Запуск pulse-lets-go**
**Шаг 7 — Запуск pulse-lets-go**
```bash
sudo mkdir -p /etc/pulse-lets-go /var/log/pulse-lets-go
# config.json создастся автоматически при первом запуске
@ -537,7 +569,7 @@ sudo mkdir -p /etc/pulse-lets-go /var/log/pulse-lets-go
PULSE_DATA_DIR=/etc/pulse-lets-go ./pulse-lets-go
```
**Шаг 7 — Проверка**
**Шаг 8 — Проверка**
```bash
curl http://127.0.0.1:8080/api/health | jq '.connections'
# → {"nats":"connected","esl":"connected"}
@ -751,8 +783,10 @@ curl http://localhost:8080/api/nodes | jq '.[] | {node_id, score, active_calls}'
| `make test-race` | Тесты с race detector |
| `make lint` | `go vet ./...` |
| `make fmt` | `go fmt ./...` |
| `make emulate-normal` | Эмуляция метрик (3 здоровые ноды) |
| `make emulate-chaos` | Эмуляция случайных сбоев |
| `make build-agent` | Сборка pulse-lets-go-agent |
| `make build-siptest` | Сборка SIP-тестера |
| `make emulate-*` | Эмуляция метрик (normal, overload, chaos, stale, ...) |
| `make siptest-*` | SIP-тестирование (uas, uac, full) |
### Стек