chore: emulator sip_gateway, Makefile targets, full README documentation
- Emulator: add sip_gateway field to all scenarios (auto-trunk creation) - Makefile: build-agent, build-siptest, emulate-*, siptest-* targets - README: full documentation with agent section, production deployment - Project structure update with all components
This commit is contained in:
parent
c713a4f142
commit
fd949ddaed
37
Makefile
37
Makefile
@ -8,7 +8,8 @@ DATA_DIR := data
|
|||||||
|
|
||||||
.PHONY: all build build-go build-web clean run test install \
|
.PHONY: all build build-go build-web clean run test install \
|
||||||
emulate emulate-normal emulate-overload emulate-cpu-low emulate-fail-high \
|
emulate emulate-normal emulate-overload emulate-cpu-low emulate-fail-high \
|
||||||
emulate-node-down emulate-stale emulate-chaos build-emulator
|
emulate-node-down emulate-stale emulate-chaos build-emulator \
|
||||||
|
build-siptest siptest-uas siptest-uac siptest-full
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@ -100,6 +101,40 @@ emulate-stale: nats build-emulator
|
|||||||
emulate-chaos: nats build-emulator
|
emulate-chaos: nats build-emulator
|
||||||
./$(BINDIR)/emulator --scenario chaos
|
./$(BINDIR)/emulator --scenario chaos
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Агент сбора метрик (pulse-lets-go-agent)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
build-agent:
|
||||||
|
@mkdir -p $(BINDIR)
|
||||||
|
go build -o $(BINDIR)/pulse-lets-go-agent ./cmd/pulse-lets-go-agent
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# SIP-тестер (e2e проверка с FreeSWITCH)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
build-siptest:
|
||||||
|
@mkdir -p $(BINDIR)
|
||||||
|
go build -o $(BINDIR)/siptest ./cmd/siptest
|
||||||
|
|
||||||
|
# Запуск UAS (PBX-нода, отвечает 200 OK на INVITE)
|
||||||
|
siptest-uas: build-siptest
|
||||||
|
./$(BINDIR)/siptest -mode uas
|
||||||
|
|
||||||
|
# Отправка тестового звонка (10 CPS, 100 вызовов)
|
||||||
|
siptest-uac: build-siptest
|
||||||
|
./$(BINDIR)/siptest -mode uac -r 10 -l 100 -m 100
|
||||||
|
|
||||||
|
# Полный e2e тест: UAS + UAC
|
||||||
|
siptest-full: build-siptest
|
||||||
|
@echo "→ Запуск UAS (PBX-нода) на порту 5090..."
|
||||||
|
./$(BINDIR)/siptest -mode uas &
|
||||||
|
@sleep 1
|
||||||
|
@echo "→ Запуск UAC (оператор)..."
|
||||||
|
./$(BINDIR)/siptest -mode uac -r 10 -l 100 -m 200
|
||||||
|
@echo "→ Остановка UAS..."
|
||||||
|
pkill siptest 2>/dev/null || true
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# NATS и запуск
|
# NATS и запуск
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
790
README.md
Normal file
790
README.md
Normal file
@ -0,0 +1,790 @@
|
|||||||
|
# Pulse Lets Go
|
||||||
|
|
||||||
|
[](https://go.dev)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://nats.io)
|
||||||
|
[](https://kit.svelte.dev)
|
||||||
|
|
||||||
|
**pulse-lets-go** — балансировщик телефонной нагрузки на базе FreeSWITCH + NATS.
|
||||||
|
|
||||||
|
Агенты PBX отправляют метрики в NATS каждые 5 секунд. Балансировщик вычисляет weighted score готовности каждой ноды, кэширует лучшую и отдаёт её FreeSWITCH через `/api/route` для следующего звонка.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **WebSocket** — real-time трансляция метрик на SvelteKit дашборд.
|
||||||
|
- **FreeSWITCH ESL** — управление Sofia-шлюзами через raw TCP ESL.
|
||||||
|
- **Monitoring** — Prometheus и Zabbix эндпоинты с отдельным API-key.
|
||||||
|
- **SvelteKit UI** — дашборд, таблицы нод, транков, пользователей.
|
||||||
|
- **Graceful shutdown** — корректное завершение с таймаутом 10 с.
|
||||||
|
- **systemd ready** — unit с изоляцией (NoNewPrivileges, ProtectSystem, PrivateTmp).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────┐ 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
|
||||||
|
│ └────────────────┘ │
|
||||||
|
└──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 <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=`) |
|
||||||
|
|
||||||
|
### Примеры запросов
|
||||||
|
|
||||||
|
```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 (упрощённо)
|
||||||
|
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"))
|
||||||
|
|
||||||
|
local response = curl.fetch({url = url, timeout = tonumber(route_timeout)})
|
||||||
|
local ok, result = cjson.decode(response.body)
|
||||||
|
|
||||||
|
if result.fallback then
|
||||||
|
session:bridge(result.sip_gateway)
|
||||||
|
else
|
||||||
|
session:bridge(result.sip_gateway)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Конфигурация в dialplan:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<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 с)
|
||||||
|
|
||||||
|
```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 install # копирует бинарник + web-статику
|
||||||
|
make install-systemd # устанавливает systemd unit
|
||||||
|
```
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# deploy/pulse-lets-go.service
|
||||||
|
[Unit]
|
||||||
|
Description=Pulse Lets Go — Telephone Load Balancer
|
||||||
|
Wants=nats.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=pulse
|
||||||
|
Group=pulse
|
||||||
|
WorkingDirectory=/usr/local/share/pulse-lets-go
|
||||||
|
Environment=PULSE_DATA_DIR=/etc/pulse-lets-go
|
||||||
|
ExecStart=/usr/local/bin/pulse-lets-go
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=yes
|
||||||
|
ReadWritePaths=/etc/pulse-lets-go /var/log/pulse-lets-go
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### Структура директорий
|
||||||
|
|
||||||
|
```
|
||||||
|
/usr/local/share/pulse-lets-go/
|
||||||
|
├── web/build/ # SvelteKit статика
|
||||||
|
/etc/pulse-lets-go/
|
||||||
|
├── config.json # Конфигурация
|
||||||
|
├── trunks.json # Транки
|
||||||
|
├── users.json # Пользователи
|
||||||
|
/var/log/pulse-lets-go/
|
||||||
|
└── 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**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fs_cli -x "load mod_curl"
|
||||||
|
# Добавить в modules.conf.xml для персистентности:
|
||||||
|
# <load module="mod_curl"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Порядок деплоя (7 шагов)
|
||||||
|
|
||||||
|
**Шаг 1 — Скопировать бинарник и конфиги**
|
||||||
|
```bash
|
||||||
|
make build-prod
|
||||||
|
scp bin/pulse-lets-go admin@balancer.lan:/usr/local/bin/
|
||||||
|
scp -r contrib/route.lua admin@balancer.lan:/tmp/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 2 — NATS сервер** (если не установлен)
|
||||||
|
```bash
|
||||||
|
ssh admin@balancer.lan
|
||||||
|
sudo mkdir -p /opt/nats
|
||||||
|
# Установить nats-server из репозитория или скопировать бинарник
|
||||||
|
# Запуск: nats-server -p 4222 -D &
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 3 — route.lua в FS scripts**
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /etc/freeswitch/scripts
|
||||||
|
sudo cp /tmp/route.lua /etc/freeswitch/scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 4 — Dialplan** (добавить `pulse_route` extension)
|
||||||
|
```xml
|
||||||
|
<!-- В /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>
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
sudo fs_cli -x "reloadxml"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 5 — Маппинг существующих FS gateway в pulse-lets-go транки**
|
||||||
|
```bash
|
||||||
|
# Получить список gateway из FS:
|
||||||
|
echo "api sofia profile external gwlist" | nc 127.0.0.1 8021 | grep "pulse-"
|
||||||
|
# → список имён gateway
|
||||||
|
|
||||||
|
# Создать balance-транки для каждого gateway через API:
|
||||||
|
# POST /api/trunks {"name":"uc-69 баланс","type":"balance","node_id":"uc-69","gateway":"sip:10.x.x.x:5060"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 6 — Запуск pulse-lets-go**
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /etc/pulse-lets-go /var/log/pulse-lets-go
|
||||||
|
# config.json создастся автоматически при первом запуске
|
||||||
|
# Добавить esl блок в config.json:
|
||||||
|
# "esl": {"host": "127.0.0.1", "port": 8021, "password": "ClueCon"}
|
||||||
|
PULSE_DATA_DIR=/etc/pulse-lets-go ./pulse-lets-go
|
||||||
|
```
|
||||||
|
|
||||||
|
**Шаг 7 — Проверка**
|
||||||
|
```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.<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
|
||||||
|
|
||||||
|
```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:uc-pbx.lan: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": "admin", "password": "changeme" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Поле | Тип | 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_<type>.go`, реализующий 5 методов интерфейса. Ничего в существующем коде менять не нужно.
|
||||||
|
|
||||||
|
### Template Configs
|
||||||
|
|
||||||
|
| Файл | Назначение | Тип |
|
||||||
|
|------|-----------|-----|
|
||||||
|
| `contrib/agent-uc.json` | Asterisk UC-ноды (05, 06, 66-69) | asterisk |
|
||||||
|
| `contrib/agent-sessip.json` | FreeSWITCH ses-sip (ses-pbx.lan) | freeswitch |
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Собрать
|
||||||
|
make build-agent
|
||||||
|
|
||||||
|
# 2. Скопировать на ноду
|
||||||
|
scp bin/pulse-lets-go-agent admin@NODE_IP:/usr/local/bin/
|
||||||
|
|
||||||
|
# 3. Создать конфиг
|
||||||
|
scp contrib/agent-uc.json admin@NODE_IP:/etc/pulse-lets-go-agent/agent.json
|
||||||
|
|
||||||
|
# 4. Запустить
|
||||||
|
ssh admin@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 nats.service
|
||||||
|
Wants=nats.service
|
||||||
|
|
||||||
|
[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 emulate-normal` | Эмуляция метрик (3 здоровые ноды) |
|
||||||
|
| `make emulate-chaos` | Эмуляция случайных сбоев |
|
||||||
|
|
||||||
|
### Стек
|
||||||
|
|
||||||
|
| Компонент | Технология |
|
||||||
|
|-----------|-----------|
|
||||||
|
| 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)
|
||||||
|
```
|
||||||
@ -37,6 +37,7 @@ type nodeMetric struct {
|
|||||||
IdleCPU float64 `json:"idle_cpu"`
|
IdleCPU float64 `json:"idle_cpu"`
|
||||||
LoadAvg float64 `json:"load_avg"`
|
LoadAvg float64 `json:"load_avg"`
|
||||||
CallFailureRate float64 `json:"call_failure_rate"`
|
CallFailureRate float64 `json:"call_failure_rate"`
|
||||||
|
SIPGateway string `json:"sip_gateway,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Спецификация значения (фиксированное или случайный диапазон) ---
|
// --- Спецификация значения (фиксированное или случайный диапазон) ---
|
||||||
@ -63,13 +64,14 @@ func (v valueSpec) get() float64 {
|
|||||||
// --- Конфигурация ноды в эмуляторе ---
|
// --- Конфигурация ноды в эмуляторе ---
|
||||||
|
|
||||||
type nodeSpec struct {
|
type nodeSpec struct {
|
||||||
nodeID string
|
nodeID string
|
||||||
status valueSpec // строка: ok / down
|
status valueSpec // строка: ok / down
|
||||||
calls valueSpec
|
calls valueSpec
|
||||||
maxCalls float64 // фиксированное
|
maxCalls float64 // фиксированное
|
||||||
idleCPU valueSpec
|
idleCPU valueSpec
|
||||||
loadAvg valueSpec
|
loadAvg valueSpec
|
||||||
failRate valueSpec
|
failRate valueSpec
|
||||||
|
sipGateway string // SIP-адрес для авто-создания транка
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *nodeSpec) generate() nodeMetric {
|
func (ns *nodeSpec) generate() nodeMetric {
|
||||||
@ -82,6 +84,7 @@ func (ns *nodeSpec) generate() nodeMetric {
|
|||||||
IdleCPU: ns.idleCPU.get(),
|
IdleCPU: ns.idleCPU.get(),
|
||||||
LoadAvg: ns.loadAvg.get(),
|
LoadAvg: ns.loadAvg.get(),
|
||||||
CallFailureRate: ns.failRate.get(),
|
CallFailureRate: ns.failRate.get(),
|
||||||
|
SIPGateway: ns.sipGateway,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,16 +103,19 @@ func scenarioNormal() []nodeSpec {
|
|||||||
nodeID: "pbx-01", status: fixed(1),
|
nodeID: "pbx-01", status: fixed(1),
|
||||||
calls: rrange(10, 100), maxCalls: 250,
|
calls: rrange(10, 100), maxCalls: 250,
|
||||||
idleCPU: rrange(40, 95), loadAvg: rrange(0.1, 1.0), failRate: rrange(0, 2),
|
idleCPU: rrange(40, 95), loadAvg: rrange(0.1, 1.0), failRate: rrange(0, 2),
|
||||||
|
sipGateway: "sip:pbx-01.lan:5060",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nodeID: "pbx-02", status: fixed(1),
|
nodeID: "pbx-02", status: fixed(1),
|
||||||
calls: rrange(50, 200), maxCalls: 250,
|
calls: rrange(50, 200), maxCalls: 250,
|
||||||
idleCPU: rrange(15, 45), loadAvg: rrange(0.5, 2.0), failRate: rrange(1, 8),
|
idleCPU: rrange(15, 45), loadAvg: rrange(0.5, 2.0), failRate: rrange(1, 8),
|
||||||
|
sipGateway: "sip:pbx-02.lan:5060",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nodeID: "pbx-03", status: fixed(1),
|
nodeID: "pbx-03", status: fixed(1),
|
||||||
calls: rrange(5, 60), maxCalls: 250,
|
calls: rrange(5, 60), maxCalls: 250,
|
||||||
idleCPU: rrange(60, 90), loadAvg: rrange(0.05, 0.5), failRate: rrange(0, 1),
|
idleCPU: rrange(60, 90), loadAvg: rrange(0.05, 0.5), failRate: rrange(0, 1),
|
||||||
|
sipGateway: "sip:pbx-03.lan:5060",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -327,6 +333,6 @@ func publish(nc *natsgo.Conn, m nodeMetric) {
|
|||||||
log.Printf("[emulator] ошибка публикации %s: %v", m.NodeID, err)
|
log.Printf("[emulator] ошибка публикации %s: %v", m.NodeID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("[emulator] → %s: calls=%d/%d idle=%.0f%% load=%.2f fail=%.1f%% status=%s",
|
log.Printf("[emulator] → %s: calls=%d/%d idle=%.0f%% load=%.2f fail=%.1f%% status=%s gw=%s",
|
||||||
m.NodeID, m.ActiveCalls, m.MaxCalls, m.IdleCPU, m.LoadAvg, m.CallFailureRate, m.Status)
|
m.NodeID, m.ActiveCalls, m.MaxCalls, m.IdleCPU, m.LoadAvg, m.CallFailureRate, m.Status, m.SIPGateway)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user