Maksim Totmin 54259e51b9 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
2026-06-25 21:51:37 +07:00
2026-06-25 14:27:41 +07:00
2026-06-25 14:27:41 +07:00
2026-06-25 14:27:41 +07:00
2026-06-25 14:27:41 +07:00

Pulse Lets Go

Go License NATS SvelteKit

pulse-lets-go — балансировщик телефонной нагрузки на базе FreeSWITCH + NATS.

Агенты PBX отправляют метрики в NATS каждые 5 секунд. Балансировщик вычисляет weighted score готовности каждой ноды, кэширует лучшую и отдаёт её FreeSWITCH через /api/route для следующего звонка.


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>.
  • 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).

Архитектура

┌─────────────────────┐  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
                                     │  └────────────────┘  │
                                     └──────────────────────┘

Quick Start

1. Требования

  • Go 1.26+
  • Node.js 22+
  • NATS Server (автоматически качается make nats)

2. Сборка

git clone git@git.totmin.ru:en2zmax/pulse-lets-go.git
cd pulse-lets-go
make build

3. Запуск

# Терминал 1: NATS
make nats

# Терминал 2: балансировщик
./bin/pulse-lets-go

# Терминал 3: эмулятор метрик (3 здоровые ноды)
make emulate-normal

4. Проверка

# Route (без авторизации)
curl 'http://localhost:8080/api/route?caller_id=123&dest=456&ingress_trunk=trk-001'

# Login
curl -X POST http://localhost:8080/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"admin"}'

Конфигурация

При первом запуске config.json создаётся автоматически в директории data/ (или $PULSE_DATA_DIR). JWT-секрет и API-key генерируются случайно.

config.json

{
  "nats_url": "nats://localhost:4222",
  "nats_user": "",
  "nats_password": "",
  "listen_addr": ":8080",
  "jwt_secret": "aabbccdd...32-байта-hex",
  "monitoring_api_key": "11223344...32-байта-hex",
  "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"
  }
}
Поле Дефолт Описание
nats_url nats://localhost:4222 URL NATS сервера
nats_user "" Имя пользователя NATS (если нужна аутентификация)
nats_password "" Пароль NATS
listen_addr :8080 Адрес HTTP-сервера
jwt_secret auto Секрет для подписи JWT (32 байта hex)
monitoring_api_key auto API-ключ для /api/monitoring/*
stale_threshold_sec 20 Через сколько секунд без метрик нода считается stale
scoring.idle_cpu_min 5 Минимальный idle CPU. Ниже — lethal
scoring.call_failure_rate_lethal 15.0 Максимальный failure rate. Выше — lethal
scoring.weights.* 0.40/0.30/0.20/0.10 Веса компонентов скора (сумма = 1.0)
tls.enabled false Включить HTTPS
rate_limit.enabled true Включить rate limiting
rate_limit.route_per_sec 5000 Лимит /api/route на IP
rate_limit.api_per_sec 100 Лимит остальных /api/* на IP
log.level info Уровень лога: debug, info, warn, error
log.format text Формат: text или json
esl.host "" Хост FreeSWITCH ESL. Пусто — ESL отключён
esl.port 8021 Порт ESL
esl.password "" Пароль ESL (ClueCon)
esl.password_env "" Или имя переменной окружения с паролем

Scoring Engine

Lethal-условия (score = -100)

Если хотя бы одно истинно — нода исключается из роутинга:

Условие Порог
status != "ok" Любой статус кроме ok
Stale > stale_threshold_sec (20 с)
active_calls >= max_calls 100% заполнение
idle_cpu < idle_cpu_min < 5%
call_failure_rate > call_failure_rate_lethal > 15%
disabled == true Ручное отключение админом

Weighted score (0..100)

call_score = clamp(100 - (active_calls / max_calls * 100), 0, 100)
load_score = clamp(100 - (load_avg * 50), 0, 100)
idle_score = clamp(idle_cpu, 0, 100)
fail_score = clamp(100 - call_failure_rate, 0, 100)

score = call_score × 0.40 +
        load_score × 0.30 +
        idle_score × 0.20 +
        fail_score × 0.10

Пример

Нода pbx-01: active_calls=42, max_calls=250, load_avg=0.85, idle_cpu=35, fail_rate=0.5

call_score = clamp(100 - (42/250)*100, 0, 100) = 83.2
load_score = clamp(100 - 0.85*50, 0, 100)      = 57.5
idle_score = clamp(35, 0, 100)                   = 35.0
fail_score = clamp(100 - 0.5, 0, 100)            = 99.5

score = 83.2×0.40 + 57.5×0.30 + 35.0×0.20 + 99.5×0.10 = 70.1

Кэширование

best_node_id + best_score + fallback_active пересчитываются при каждой метрике. /api/route читает их за RLock — O(1).


API Reference

Auth

Метод Путь Роль Описание
POST /api/auth/login Вход: {"username":"admin","password":"admin"} → access_token + refresh_token
POST /api/auth/refresh Обновление access-токена через refresh_token

Все остальные эндпоинты (кроме /api/route, /api/health/*, /api/monitoring/*) требуют Authorization: Bearer <jwt>.

Core

Метод Путь Роль Описание
GET /api/route Выбор ноды для звонка. Query: caller_id, dest, ingress_trunk
GET /api/health Статистика: всего нод, здоровых, запросов, uptime
GET /api/health/live Liveness probe (всегда 200)
GET /api/health/ready Readiness probe (NATS + метрики)

Nodes

Метод Путь Роль Описание
GET /api/nodes admin/viewer Список всех нод со score, состоянием
GET /api/nodes/{id}/metrics admin/viewer История метрик (ring buffer, ~30 мин)
PUT /api/nodes/{id}/toggle admin {"disabled":true, "reason":"..."}

Trunks

Метод Путь Роль Описание
GET /api/trunks admin Все транки. Query: type=ingress|balance|fallback
POST /api/trunks admin Создать транк
PUT /api/trunks/{id} admin Обновить транк
DELETE /api/trunks/{id} admin Удалить транк

Users

Метод Путь Роль Описание
GET /api/users admin Список пользователей
POST /api/users admin Создать пользователя
PUT /api/users/{id} admin Обновить пользователя
DELETE /api/users/{id} admin Удалить пользователя

Monitoring (API-key, не JWT)

Метод Путь Описание
GET /api/monitoring/zabbix JSON для Zabbix LLD
GET /api/monitoring/prometheus text/plain метрики для Prometheus

WebSocket

Метод Путь Описание
WS /ws/metrics Real-time метрики на фронт (JWT в query ?token=)

Примеры запросов

# Route (no auth)
curl 'http://localhost:8080/api/route?caller_id=74951234567&dest=123&ingress_trunk=trk-001'
# → {"node_id":"pbx-03","score":88,"sip_gateway":"sip:pbx03.lan:5060"}

# Fallback (все ноды unhealthy)
# → {"fallback":true,"sip_gateway":"sip:operator.lan:5060","reason":"all_nodes_unhealthy","nodes":[...]}

# Login
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"admin"}' | jq -r '.access_token')

# Nodes with JWT
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/nodes

# Toggle node (admin only)
curl -X PUT http://localhost:8080/api/nodes/pbx-01/toggle \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"disabled":true,"reason":"maintenance"}'

# Prometheus metrics (API-key)
curl -H "X-API-Key: $(jq -r '.monitoring_api_key' data/config.json)" \
  http://localhost:8080/api/monitoring/prometheus

FreeSWITCH Интеграция

Dialplan (route.lua)

FreeSWITCH использует Lua-скрипт в dialplan. Скрипт дёргает /api/route через mod_curl и бриджится на полученный gateway.

-- 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

raw = api:execute("curl", url)
body = raw:match("\r?\n\r?\n(.+)") or raw

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:hangup("NORMAL_TEMPORARY_FAILURE")
end

Конфигурация в dialplan:

<extension name="route_call">
  <condition field="destination_number" expression="^.*$">
    <action application="set" data="balancer_url=http://balancer.lan:8080"/>
    <action application="set" data="ingress_trunk=trk-001"/>
    <action application="lua" data="route.lua"/>
  </condition>
</extension>

ESL (автоматическое управление шлюзами)

При включении esl.host в конфиге балансировщик:

  • Подключается к FreeSWITCH ESL (:8021)
  • Синхронизирует ingress-транки как Sofia-шлюзы
  • Слушает события регистрации шлюзов
  • Передаёт статус gateway на WebSocket фронта

Модели данных

NATS-сообщение (PBX → 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
}

Транки

Тип Назначение
ingress Входящий транк (откуда звонки приходят)
balance Транк назначения (привязан к Node)
fallback Резервный транк (когда все balance score < 0), ровно 1

Деплой

systemd

make deploy      # полный деплой в /opt/pulse-lets-go
make install-systemd  # устанавливает systemd unit
# deploy/pulse-lets-go.service
[Unit]
Description=Pulse Lets Go — Telephone Load Balancer
After=network.target

[Service]
User=pulse
Group=pulse
ExecStart=/opt/pulse-lets-go/bin/pulse-lets-go
WorkingDirectory=/opt/pulse-lets-go
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/pulse-lets-go/data /opt/pulse-lets-go/log

[Install]
WantedBy=multi-user.target

Структура директорий

/opt/pulse-lets-go/
├── bin/pulse-lets-go       # Бинарник
├── data/
│   ├── config.json         # Конфигурация
│   ├── trunks.json         # Транки
│   └── users.json          # Пользователи
├── web/                    # SvelteKit статика
└── log/
    └── metrics.YYYY-MM-DD.log  # ASCII-лог метрик

Security

  • NoNewPrivileges=true — запрет повышения привилегий
  • ProtectSystem=strict — read-only системные директории
  • ProtectHome=yes — изоляция home
  • PrivateTmp=true — изолированный /tmp
  • JWT-секрет и API-key генерируются случайно при первом запуске

Production Deployment

Предусловия (перед деплоем pulse-lets-go)

Проверка Команда Ожидание
1 ESL запущен nc -z 127.0.0.1 8021 && echo OK OK
2 ESL пароль echo "auth ClueCon" | nc 127.0.0.1 8021 +OK accepted
3 mod_curl загружен fs_cli -x "show modules" | grep mod_curl <load module="mod_curl"/>
4 mod_lua загружен fs_cli -x "show modules" | grep mod_lua <load module="mod_lua"/>
5 NATS установлен which nats-server путь к бинарнику
6 Права на ESL ACL в event_socket.conf.xml разрешает 127.0.0.1 localhost или 127.0.0.1 в ACL

Загрузка mod_curl без рестарта FreeSWITCH

Если mod_curl не загружен — звонки работать не будут. Загружается без остановки FS:

fs_cli -x "load mod_curl"
# Добавить в modules.conf.xml для персистентности:
# <load module="mod_curl"/>

Порядок деплоя (8 шагов)

Шаг 1 — Деплой на сервер

make deploy
scp -r /opt/pulse-lets-go devadmin@10.101.60.81:/opt/
scp contrib/route.lua devadmin@10.101.60.81:/tmp/

Шаг 2 — NATS сервер (если не установлен)

ssh devadmin@10.101.60.81
sudo mkdir -p /opt/nats
# Установить nats-server из репозитория или скопировать бинарник
# Запуск: nats-server -p 4222 -D &

Шаг 3 — Деплой agent на PBX-ноды (повторить для каждой UC/Asterisk ноды)

# Скопировать бинарник и конфиг на каждую 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

sudo mkdir -p /etc/freeswitch/scripts
sudo cp /tmp/route.lua /etc/freeswitch/scripts/

Шаг 5 — Dialplan (добавить pulse_route extension)

<!-- В /etc/freeswitch/dialplan/default.xml перед остальными extension -->
<extension name="pulse_route">
  <condition field="destination_number" expression="^(.*)$">
    <action application="set" data="balancer_url=http://127.0.0.1:8080"/>
    <action application="lua" data="route.lua"/>
  </condition>
</extension>
sudo fs_cli -x "reloadxml"

Шаг 6 — Создание balance-транков для каждой PBX-ноды

# Получить список gateway из FS (или список нод из конфига):
echo "api sofia profile external gwlist" | nc 127.0.0.1 8021

# Создать 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).

Шаг 7 — Запуск pulse-lets-go

# config.json создастся автоматически при первом запуске в /opt/pulse-lets-go/data/
# Добавить esl блок в config.json:
# "esl": {"host": "127.0.0.1", "port": 8021, "password": "ClueCon"}
/opt/pulse-lets-go/bin/pulse-lets-go

Шаг 8 — Проверка

curl http://127.0.0.1:8080/api/health | jq '.connections'
# → {"nats":"connected","esl":"connected"}
curl 'http://127.0.0.1:8080/api/route?caller_id=123&dest=456'
# → {"node_id":"pbx-03","score":88,"sip_gateway":...}

Rollback (если route.lua сломал звонки)

# 1. Убрать pulse_route из dialplan
sudo sed -i '/pulse_route/,/<\/extension>/d' /etc/freeswitch/dialplan/default.xml
sudo fs_cli -x "reloadxml"

# 2. Остановить pulse-lets-go
sudo systemctl stop pulse-lets-go

# Звонки продолжают идти по старому dialplan без балансировщика.

Права доступа (production)

Операция Требуемые права
Чтение FS конфигов root или freeswitch
fs_cli -x команды root или freeswitch
ESL (порт 8021) ACL в event_socket.conf.xml
Установка ПО root
Запись в /etc/freeswitch/scripts/ root
reloadxml root или freeswitch

Версии (протестировано)

Компонент Версия
FreeSWITCH 1.10.12+
NATS Server 2.10+
Go 1.26
ОС Linux (CentOS 7+, AlmaLinux, Arch)

Metrics Agent (pulse-lets-go-agent)

Architecture

┌─────────────────────┐     pulse.metrics.<id>     ┌─────────────────────┐
│  pulse-lets-go-agent│──────────────────────────▶  │    NATS Server      │
│  (on each PBX node) │     every 5 seconds        │    (central)        │
├─────────────────────┤                            └────────┬────────────┘
│  FreeSWITCH (ESL)   │                                     │
│  Asterisk (AMI)     │                                     ▼
│  System (/proc)     │                             ┌─────────────────┐
└─────────────────────┘                             │ pulse-lets-go    │
                                                    │ (scoring engine) │
                                                    └─────────────────┘

The agent runs on each PBX node (FreeSWITCH or Asterisk), collects metrics, and publishes them to NATS at a regular interval (default 5 seconds). The pulse-lets-go balancer subscribes to all metrics and uses them for scoring and routing decisions.

Installation

make build-agent
# → bin/pulse-lets-go-agent (static binary, ~9 MB)

Configuration (agent.json)

{
  "node_id": "uc06",
  "type": "asterisk",
  "nats_url": "nats://balancer.host:4222",
  "nats_user": "",
  "nats_password": "",
  "interval_sec": 5,
  "max_calls": 150,
  "sip_gateway": "sip:10.101.60.115:5060",
  "sip_gateway_auto": false,
  "failure_window": 1000,
  "esl": { "host": "127.0.0.1", "port": 8021, "password": "ClueCon" },
  "ami": { "host": "127.0.0.1", "port": 6154, "username": "ctt", "password": "cttpass" }
}
Поле Тип Default Описание
node_id string Уникальный ID ноды (uc06, ses-sip)
type string freeswitch или asterisk
nats_url string URL NATS-сервера
nats_user string "" Пользователь NATS
nats_password string "" Пароль NATS
interval_sec int 5 Интервал публикации метрик
max_calls int 250 Ёмкость ноды (fallback)
sip_gateway string "" SIP-адрес для auto-создания транка
sip_gateway_auto bool false Авто-определение SIP из PBX
failure_window int 1000 Размер окна для call_failure_rate
esl.* object FreeSWITCH ESL настройки (host, port, password)
ami.* object Asterisk AMI настройки (host, port, username, password)

Collectors

Collector Источник Метрики PBX
freeswitch ESL: show channels count, json status, eval $${idle_cpu} active_calls, max_calls, idle_cpu
asterisk AMI: CoreShowChannels, CoreSettings, Hangup events active_calls, max_calls, call_failure_rate
system /proc/loadavg, /proc/stat load_avg, idle_cpu (fallback)

Call Failure Rate: скользящее окно последних N (1000) завершённых звонков.

Исход FS (ESL) Asterisk (AMI)
Успех CHANNEL_HANGUP + Hangup-Cause: NORMAL_CLEARING Hangup + Cause: 16
Отказ Любой другой Hangup-Cause Любой другой Cause

Collector Interface (расширяемость)

type Collector interface {
    Type() string                     // "freeswitch" | "asterisk"
    Connect() error                   // ESL auth / AMI login
    Collect() (*PBXResult, error)     // Сбор метрик
    ListenHangup(onHangup func(bool)) // Подписка на hangup события
    Close()                           // Закрытие соединения
}

Новый тип ноды (omnichannel, другой PBX) = новый файл collector_<type>.go, реализующий 5 методов интерфейса. Ничего в существующем коде менять не нужно.

Template Configs

Файл Назначение Тип
contrib/agent-uc.json Asterisk UC-ноды (05, 06, 66-69) asterisk
contrib/agent-sessip.json FreeSWITCH ses-sip (10.3.0.44) freeswitch

Deployment

# 1. Собрать
make build-agent

# 2. Скопировать на ноду
scp bin/pulse-lets-go-agent devadmin@NODE_IP:/usr/local/bin/

# 3. Создать конфиг
scp contrib/agent-uc.json devadmin@NODE_IP:/etc/pulse-lets-go-agent/agent.json

# 4. Запустить
ssh devadmin@NODE_IP "pulse-lets-go-agent -config /etc/pulse-lets-go-agent/agent.json"

Systemd Unit

# /etc/systemd/system/pulse-lets-go-agent.service
[Unit]
Description=Pulse Lets Go Agent — PBX metrics collector
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/pulse-lets-go-agent -config /etc/pulse-lets-go-agent/agent.json
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Verification

# На PBX-ноде — проверить лог агента:
journalctl -u pulse-lets-go-agent -f
# Ожидаем:
#   [publisher] NATS подключён к nats://host:4222
#   [ami] подключён к 127.0.0.1:6154        (для Asterisk)
#   [agent] опубликована метрика: calls=0/150 load=0.28 cpu=99% status=ok

# На балансировщике — проверить что нода появилась:
curl http://localhost:8080/api/nodes | jq '.[] | {node_id, score, active_calls}'

Troubleshooting

Проблема Причина Решение
nats: no servers available NATS не запущен или недоступен systemctl start nats-server
ami auth rejected Неверный пароль или доступ Проверить /etc/asterisk/manager.conf
esl connect timeout ESL не слушает на порту fs_cli -x "load mod_event_socket"
calls=0/0 max_calls не удалось получить Указать max_calls в agent.json

Разработка

Команды

Команда Описание
make build Полная сборка (Go + SvelteKit)
make dev Go backend + SvelteKit dev server (hot-reload)
make test Go тесты с coverage
make test-race Тесты с race detector
make lint go vet ./...
make fmt go fmt ./...
make build-agent Сборка pulse-lets-go-agent
make build-siptest Сборка SIP-тестера
make emulate-* Эмуляция метрик (normal, overload, chaos, stale, ...)
make siptest-* SIP-тестирование (uas, uac, full)

Стек

Компонент Технология
Backend Go 1.26, stdlib net/http + http.ServeMux
Frontend SvelteKit 2 + Tailwind CSS 4 + Lucide
Auth JWT (golang-jwt/v5, HS256) + bcrypt
Message bus NATS (nats.go)
Storage JSON-файлы (data/)
ESL Raw TCP (без внешних библиотек)

Структура проекта

pulse-lets-go/
├── cmd/
│   ├── pulse-lets-go/       # Точка входа (main.go)
│   ├── pulse-lets-go-agent/ # Агент сбора метрик (FS / Asterisk)
│   ├── emulator/            # Эмулятор метрик для тестирования
│   └── siptest/             # SIP-тестер для e2e проверок
├── internal/
│   ├── api/                 # HTTP-хендлеры (auth, nodes, trunks, users, monitoring, ws, route)
│   ├── config/              # JSON-конфиги (atomic save, thread-safe)
│   ├── engine/              # Scoring engine + router (sync.RWMutex, best-node cache)
│   ├── esl/                 # FreeSWITCH ESL client (raw TCP)
│   ├── ami/                 # Asterisk AMI client (raw TCP)
│   ├── log/                 # ASCII-логгер метрик с ротацией
│   ├── models/              # Все типы данных (NodeMetric, NodeState, Trunk, User, ...)
│   └── nats/                # NATS subscriber
├── contrib/                 # route.lua, agent-*.json (FreeSWITCH dialplan + агент)
├── deploy/                  # systemd unit
├── web/                     # SvelteKit фронтенд
└── data/                    # JSON-файлы времени выполнения (gitignored)
Description
Балансировщик телефонной нагрузки на базе FreeSWITCH + NATS
Readme 12 MiB
Languages
Go 74.2%
Svelte 18.5%
Lua 2.7%
TypeScript 2.4%
Makefile 1.8%
Other 0.4%