# Pulse Lets Go [![Go](https://img.shields.io/badge/Go-1.26-blue?logo=go)](https://go.dev) [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![NATS](https://img.shields.io/badge/NATS-latest-27ae60?logo=nats)](https://nats.io) [![SvelteKit](https://img.shields.io/badge/SvelteKit-2-fb923c?logo=svelte)](https://kit.svelte.dev) **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.`. - **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. │ 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. Сборка ```bash git clone git@git.totmin.ru:en2zmax/pulse-lets-go.git cd pulse-lets-go make build ``` ### 3. Запуск ```bash # Терминал 1: NATS make nats # Терминал 2: балансировщик ./bin/pulse-lets-go # Терминал 3: эмулятор метрик (3 здоровые ноды) make emulate-normal ``` ### 4. Проверка ```bash # 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 ```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 `. ### 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=`) | ### Примеры запросов ```bash # 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. ```lua -- 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: ```xml ``` ### ESL (автоматическое управление шлюзами) При включении `esl.host` в конфиге балансировщик: - Подключается к FreeSWITCH ESL (`:8021`) - Синхронизирует ingress-транки как Sofia-шлюзы - Слушает события регистрации шлюзов - Передаёт статус gateway на WebSocket фронта --- ## Модели данных ### NATS-сообщение (PBX → 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 } ``` ### Транки | Тип | Назначение | |-----|-----------| | `ingress` | Входящий транк (откуда звонки приходят) | | `balance` | Транк назначения (привязан к Node) | | `fallback` | Резервный транк (когда все balance score < 0), ровно 1 | --- ## Деплой ### systemd ```bash make deploy # полный деплой в /opt/pulse-lets-go make install-systemd # устанавливает systemd unit ``` ```ini # 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` | `` | | 4 | **mod_lua загружен** | `fs_cli -x "show modules" \| grep 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**: ```bash fs_cli -x "load mod_curl" # Добавить в modules.conf.xml для персистентности: # ``` ### Порядок деплоя (8 шагов) **Шаг 1 — Деплой на сервер** ```bash 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 сервер** (если не установлен) ```bash ssh devadmin@10.101.60.81 sudo mkdir -p /opt/nats # Установить nats-server из репозитория или скопировать бинарник # Запуск: nats-server -p 4222 -D & ``` **Шаг 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/ ``` **Шаг 5 — Dialplan** (добавить `pulse_route` extension) ```xml ``` ```bash sudo fs_cli -x "reloadxml" ``` **Шаг 6 — Создание balance-транков для каждой PBX-ноды** ```bash # Получить список 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** ```bash # 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 — Проверка** ```bash 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 сломал звонки) ```bash # 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. ┌─────────────────────┐ │ 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 ```bash make build-agent # → bin/pulse-lets-go-agent (static binary, ~9 MB) ``` ### Configuration (agent.json) ```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 (расширяемость) ```go 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_.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 ```bash # 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 ```ini # /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 ```bash # На 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) ```