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, "max_calls": 250,
"idle_cpu": 35, "idle_cpu": 35,
"load_avg": 0.85, "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. FS использует Lua скрипт в dialplan → `mod_curl` дёргает `/api/route` → получает JSON → парсит `sip_gateway` → bridge.
``` ```
<extension name="route_call"> <extension name="pulse_route">
<condition field="destination_number" expression="^.*$"> <condition field="destination_number" expression="^.*$">
<action application="lua" data="route.lua"/> <action application="lua" data="route.lua"/>
</condition> </condition>
@ -188,6 +189,8 @@ route.lua: GET `/api/route?...` → если `fallback: true` идёт на fall
```json ```json
{ {
"nats_url": "nats://localhost:4222", "nats_url": "nats://localhost:4222",
"nats_user": "",
"nats_password": "",
"listen_addr": ":8080", "listen_addr": ":8080",
"jwt_secret": "auto-generated-on-first-run", "jwt_secret": "auto-generated-on-first-run",
"monitoring_api_key": "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, "idle_score": 0.20,
"fail_score": 0.10 "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/ pulse-lets-go/
├── cmd/pulse-lets-go/main.go ├── cmd/pulse-lets-go/main.go
├── cmd/pulse-lets-go-agent/
├── cmd/emulator/
├── cmd/siptest/
├── internal/ ├── internal/
│ ├── api/ — handlers: auth, nodes, trunks, users, monitoring, ws │ ├── api/ — handlers: auth, nodes, trunks, users, monitoring, ws
│ ├── engine/ — scorer + router (sync.RWMutex, best_node кэш) │ ├── engine/ — scorer + router (sync.RWMutex, best_node кэш)
│ ├── nats/ — subscriber, in-memory node store │ ├── nats/ — subscriber, in-memory node store
│ ├── esl/ — FreeSWITCH ESL client (raw TCP)
│ ├── ami/ — Asterisk AMI client (raw TCP)
│ ├── config/ — manager: чтение/атомарная запись JSON │ ├── config/ — manager: чтение/атомарная запись JSON
│ ├── models/ — все общие типы │ ├── models/ — все общие типы
│ └── log/ — ASCII-metrics логгер + ротация │ └── log/ — ASCII-metrics логгер + ротация
├── contrib/ — route.lua, agent конфиги
├── data/ — JSON конфиги (gitignored) ├── data/ — JSON конфиги (gitignored)
├── web/ — SvelteKit + shadcn-svelte + Tailwind ├── web/ — SvelteKit + shadcn-svelte + Tailwind
│ ├── src/routes/ │ ├── src/routes/
@ -224,7 +255,8 @@ pulse-lets-go/
│ │ ├── +page.svelte # дашборд │ │ ├── +page.svelte # дашборд
│ │ ├── login/+page.svelte │ │ ├── login/+page.svelte
│ │ ├── trunks/+page.svelte │ │ ├── trunks/+page.svelte
│ │ └── nodes/+page.svelte │ │ ├── nodes/+page.svelte
│ │ └── users/+page.svelte
│ ├── src/lib/ │ ├── src/lib/
│ │ ├── components/ │ │ ├── components/
│ │ └── api.ts │ │ └── api.ts

106
README.md
View File

@ -13,13 +13,17 @@
## Features ## 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) с конфигурируемыми весами. - **Scoring engine** — lethal checks (disabled, stale, overload, CPU, failure rate) + weighted score (call, load, idle, fail) с конфигурируемыми весами.
- **NATS шина** — асинхронный сбор метрик через `pulse.metrics.<node_id>`. - **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 дашборд. - **WebSocket** — real-time трансляция метрик на SvelteKit дашборд.
- **FreeSWITCH ESL** — управление Sofia-шлюзами через raw TCP ESL. - **FreeSWITCH ESL** — управление Sofia-шлюзами через raw TCP ESL.
- **Monitoring** — Prometheus и Zabbix эндпоинты с отдельным API-key. - **Monitoring** — Prometheus и Zabbix эндпоинты с отдельным API-key.
- **SvelteKit UI** — дашборд, таблицы нод, транков, пользователей. - **SvelteKit UI** — дашборд, таблицы нод, транков, пользователей.
- **Защита пользователей** — первичный администратор (user-01) не может быть удалён или понижен до viewer.
- **Graceful shutdown** — корректное завершение с таймаутом 10 с. - **Graceful shutdown** — корректное завершение с таймаутом 10 с.
- **systemd ready** — unit с изоляцией (NoNewPrivileges, ProtectSystem, PrivateTmp). - **systemd ready** — unit с изоляцией (NoNewPrivileges, ProtectSystem, PrivateTmp).
@ -28,21 +32,29 @@
## Архитектура ## Архитектура
``` ```
┌────────────────┐ pulse.metrics.<id> ┌──────────────────┐ ┌─────────────────────┐ ESL/AMI ┌──────────────────────┐
│ PBX-01 │────── every 5s ────────────▶ │ │ │ PBX-01 │──────────▶│ pulse-lets-go-agent │
│ (FS node) │ │ NATS Server │ │ (FS or Asterisk) │ collect │ (on each PBX node) │
├────────────────┤ │ nats://:4222 │ ├─────────────────────┤ ├──────────────────────┤
│ PBX-02 │────── every 5s ────────────▶ │ │ │ PBX-02 │──────────▶│ pulse-lets-go-agent │
│ (FS node) │ └────────┬─────────┘ │ (FS or Asterisk) │ ├──────────────────────┤
├────────────────┤ │ ├─────────────────────┤ ├──────────────────────┤
│ PBX-03 │────── every 5s ────────────▶ │ │ PBX-N │──────────▶│ pulse-lets-go-agent │
│ (FS node) │ │ │ (FS or Asterisk) │ └─────────┬────────────┘
└────────────────┘ ▼ └─────────────────────┘ │ pulse.metrics.<id>
│ every 5s
┌──────────────┐
│ NATS Server │
│ nats://:4222 │
└──────┬───────┘
┌──────────────────────┐ ┌──────────────────────┐
│ pulse-lets-go │ │ pulse-lets-go │
│ │ │ │
│ ┌────────────────┐ │ │ ┌────────────────┐ │
┌───────▶│ NATS Sub │ │ │ NATS Sub │ │
│ │ (pulse.metrics)│ │ │ │ (pulse.metrics)│ │
│ └────────┬───────┘ │ │ └────────┬───────┘ │
│ ▼ │ │ ▼ │
@ -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. FreeSWITCH использует Lua-скрипт в dialplan. Скрипт дёргает `/api/route` через `mod_curl` и бриджится на полученный gateway.
```lua ```lua
-- contrib/route.lua (упрощённо) -- contrib/route.lua (сокращённо)
local url = string.format("%s/api/route?caller_id=%s&dest=%s&ingress_trunk=%s", api = freeswitch.API()
session:getVariable("balancer_url"), caller_id = session:getVariable("caller_id_number") or ""
session:getVariable("caller_id_number"), dest = session:getVariable("destination_number") or ""
session:getVariable("destination_number"), url = "http://localhost:8080/api/route?caller_id=" .. caller_id .. "&dest=" .. dest
session:getVariable("ingress_trunk"))
local response = curl.fetch({url = url, timeout = tonumber(route_timeout)}) raw = api:execute("curl", url)
local ok, result = cjson.decode(response.body) body = raw:match("\r?\n\r?\n(.+)") or raw
if result.fallback then local ok, route = pcall(cjson.decode, body)
session:bridge(result.sip_gateway) 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 else
session:bridge(result.sip_gateway) session:hangup("NORMAL_TEMPORARY_FAILURE")
end end
``` ```
@ -415,7 +431,7 @@ make install-systemd # устанавливает systemd unit
# deploy/pulse-lets-go.service # deploy/pulse-lets-go.service
[Unit] [Unit]
Description=Pulse Lets Go — Telephone Load Balancer Description=Pulse Lets Go — Telephone Load Balancer
Wants=nats.service After=network.target
[Service] [Service]
User=pulse User=pulse
@ -498,13 +514,28 @@ sudo mkdir -p /opt/nats
# Запуск: nats-server -p 4222 -D & # Запуск: 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 ```bash
sudo mkdir -p /etc/freeswitch/scripts sudo mkdir -p /etc/freeswitch/scripts
sudo cp /tmp/route.lua /etc/freeswitch/scripts/ sudo cp /tmp/route.lua /etc/freeswitch/scripts/
``` ```
**Шаг 4 — Dialplan** (добавить `pulse_route` extension) **Шаг 5 — Dialplan** (добавить `pulse_route` extension)
```xml ```xml
<!-- В /etc/freeswitch/dialplan/default.xml перед остальными extension --> <!-- В /etc/freeswitch/dialplan/default.xml перед остальными extension -->
<extension name="pulse_route"> <extension name="pulse_route">
@ -518,17 +549,18 @@ sudo cp /tmp/route.lua /etc/freeswitch/scripts/
sudo fs_cli -x "reloadxml" sudo fs_cli -x "reloadxml"
``` ```
**Шаг 5 — Маппинг существующих FS gateway в pulse-lets-go транки** **Шаг 6 — Создание balance-транков для каждой PBX-ноды**
```bash ```bash
# Получить список gateway из FS: # Получить список gateway из FS (или список нод из конфига):
echo "api sofia profile external gwlist" | nc 127.0.0.1 8021 | grep "pulse-" echo "api sofia profile external gwlist" | nc 127.0.0.1 8021
# → список имён gateway
# Создать balance-транки для каждого gateway через API: # Создать balance-транк для каждой UC-ноды:
# POST /api/trunks {"name":"uc-69 баланс","type":"balance","node_id":"uc-69","gateway":"sip:10.x.x.x:5060"} 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 ```bash
sudo mkdir -p /etc/pulse-lets-go /var/log/pulse-lets-go sudo mkdir -p /etc/pulse-lets-go /var/log/pulse-lets-go
# config.json создастся автоматически при первом запуске # 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 PULSE_DATA_DIR=/etc/pulse-lets-go ./pulse-lets-go
``` ```
**Шаг 7 — Проверка** **Шаг 8 — Проверка**
```bash ```bash
curl http://127.0.0.1:8080/api/health | jq '.connections' curl http://127.0.0.1:8080/api/health | jq '.connections'
# → {"nats":"connected","esl":"connected"} # → {"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 test-race` | Тесты с race detector |
| `make lint` | `go vet ./...` | | `make lint` | `go vet ./...` |
| `make fmt` | `go fmt ./...` | | `make fmt` | `go fmt ./...` |
| `make emulate-normal` | Эмуляция метрик (3 здоровые ноды) | | `make build-agent` | Сборка pulse-lets-go-agent |
| `make emulate-chaos` | Эмуляция случайных сбоев | | `make build-siptest` | Сборка SIP-тестера |
| `make emulate-*` | Эмуляция метрик (normal, overload, chaos, stale, ...) |
| `make siptest-*` | SIP-тестирование (uas, uac, full) |
### Стек ### Стек