Compare commits

..
16 Commits
Author SHA1 Message Date
Maksim Totmin 6e9c5f20d4 docs: add routing.md — balancing modes and routing rules 2026-06-25 22:54:37 +07:00
Maksim Totmin 0fa9278eef docs: remove img.shields.io badges (blocked in RF) 2026-06-25 22:45:34 +07:00
Maksim Totmin f20d5aa0e2 docs: restructure documentation into docs/ directory
- Create docs/ with 7 focused files: architecture, api, scoring, agent,
  deployment, freeswitch, development
- Rewrite README.md as concise overview with links to docs/
- Remove AGENTS.md (content merged into docs/ and README)
2026-06-25 22:35:19 +07:00
Maksim Totmin 110289a093 feat: production-grade routing cache + stale detection fixes
Three-tier routing cache (ESL global -> file -> HTTP) eliminates HTTP
from call path for 2500-5000 concurrent calls. Lua reads cached route
in ~0.1us instead of blocking on api:execute('curl', ...).

Engine fixes:
- recalcBestLocked now skips stale nodes (was 5 min bug -> now ~10-15s)
- PickNodeForCall action_type='node' checks staleness for consistency
- onMetric callback pushes cache on every metric (no ticker delay)

Config:
- stale_threshold_sec default 20 -> 10 (industry standard)
- contrib/ included in Makefile deploy target
- route.lua paths updated for /opt/pulse-lets-go
2026-06-25 22:23:16 +07:00
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
Maksim Totmin 54d56690c6 feat: eviction trunk cleanup + gateway auto-update on re-registration 2026-06-25 21:01:58 +07:00
Maksim Totmin 938c9e9362 feat: weighted random routing + mode switching + EWMA smoothing + stale eviction
- Weighted random routing (math/rand/v2 per-goroutine ChaCha8)
- PUT /api/route/mode for runtime mode switch (best / weighted_random)
- EWMA score smoothing (configurable smoothing_factor, default 0.3)
- Periodic stale node eviction (5 min threshold, 60s interval)
- Non-blocking WS broadcast (per-client buffered channel + writePump)
- Background rate limiter cleanup goroutine
- In-memory trunk gateway cache (zero disk I/O in hot path)
- Configurable load_avg multiplier (default 50.0, backward compat)
- UI mode indicator + admin Switch button in dashboard
- 42 tests: weighted random, mode switching, EWMA, eviction, API integration
2026-06-25 20:42:46 +07:00
Maksim Totmin ff037f9c1e feat: balancer dashboard metrics + fix WebSocket Hijacker 2026-06-25 19:59:26 +07:00
Maksim Totmin 273e316af9 refactor: deploy model — all files under /opt/pulse-lets-go
Single prefix deployment: /opt/pulse-lets-go/ contains
bin/, data/, web/, log/ — no more split across
/usr/local/bin, /etc, /usr/local/share, /var/log.

* Makefile: install → deploy target with PREFIX=/opt/pulse-lets-go
* deploy/pulse-lets-go.service: all paths under /opt/pulse-lets-go,
  removed stale nats.service dependency
* main.go: default dataDir = parent of exeDir (PREFIX/bin → PREFIX/data)
* README: updated all paths, step 1 uses make deploy
2026-06-25 19:35:54 +07:00
Maksim Totmin 0d8a56e73f fix: build system — findWebDir order, auto-copy web static to bin
* findWebDir: CWD and project root checked before binary-adjacent path,
  so dev always serves fresh web/build/ over stale bin/web/build/
* build-go: auto-copies web/build/ to bin/web/ before Go compile
* build-prod: same auto-copy for production builds
* Proper refresh of frontend static on every make build
2026-06-25 19:30:49 +07:00
Maksim Totmin 78e53f42bf fix: SvelteKit UI reactivity with Svelte 5 runes
* / replaces legacy $: syntax
* Active menu highlight via .url.pathname (/stores)
* Logout navigates to /login via goto()
* Token/user footer updates reactively via
2026-06-25 19:30:42 +07:00
Maksim Totmin cc9da3ad7d refactor: common util package, ESL/AMI/security fixes, Prometheus metrics
Backend stability and security improvements:

* internal/util/ — common RandomHex helper, removed 3 duplicates
* ESL: deduplicated readMessage (locked/unlocked), net.JoinHostPort for IPv6
* AMI: synchronous reconnect() in readEventsLoop, net.JoinHostPort for IPv6
* Auth: /api/auth/refresh accepts Authorization header only (no ?token=)
* decodeJSON: http.MaxBytesReader(1<<20) body limit
* Trunks: gatewayParams() uses configured ESL.GatewayPrefix
* Config: jwt_secret_env env-var fallback
* FSCollector: time.After → time.NewTimer with defer Stop
* Monitoring: Prometheus counters (route_requests, nodes_total/healthy, uptime)
* go fmt pass across all internal/ packages
2026-06-25 19:30:36 +07:00
Maksim Totmin e66ac27dd9 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)
2026-06-25 17:16:03 +07:00
Maksim Totmin 13c88c1586 fix: agent systemd unit remove nats dependency
Agent connects to NATS remotely, not locally. After=network only.
2026-06-25 16:59:39 +07:00
Maksim Totmin 95ac4164dd 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
2026-06-25 16:59:17 +07:00
Maksim Totmin 1e6f432230 feat: metrics agent for FreeSWITCH and Asterisk nodes
- pulse-lets-go-agent: collects metrics from PBX nodes via ESL (FreeSWITCH) or AMI (Asterisk)
- AMI client (raw TCP) for Asterisk Manager Interface
- Collector interface: collect, connect, subscribe hangup events, close
- FreeSWITCH collector: show channels, max_sessions, idle_cpu
- Asterisk collector: CoreShowChannels, CoreSettings
- System collector: /proc/loadavg, /proc/stat (CPU idle)
- Failure tracker: sliding window for call_failure_rate
- NATS publisher: pulse.metrics.<node_id> every 5 seconds
- Graceful shutdown, retry logic (5 failures → status=down)
- Template configs for UC nodes (Asterisk) and ses-sip (FreeSWITCH)
2026-06-25 16:59:10 +07:00
10 changed files with 46 additions and 248 deletions
+3 -3
View File
@@ -11,11 +11,11 @@
// { // {
// "node_id": "uc06", // "node_id": "uc06",
// "type": "asterisk", // "type": "asterisk",
// "nats_url": "nats://balancer.lan:4222", // "nats_url": "nats://10.101.60.81:4222",
// "sip_gateway": "sip:uc-pbx.lan:5060", // "sip_gateway": "sip:10.101.60.115:5060",
// "interval_sec": 5, // "interval_sec": 5,
// "max_calls": 150, // "max_calls": 150,
// "ami": { "host": "127.0.0.1", "port": 6154, "username": "admin", "password": "changeme" } // "ami": { "host": "127.0.0.1", "port": 6154, "username": "ctt", "password": "cttpass" }
// } // }
package agent package agent
+3 -3
View File
@@ -11,11 +11,11 @@
// { // {
// "node_id": "uc06", // "node_id": "uc06",
// "type": "asterisk", // "type": "asterisk",
// "nats_url": "nats://balancer.lan:4222", // "nats_url": "nats://10.101.60.81:4222",
// "sip_gateway": "sip:uc-pbx.lan:5060", // "sip_gateway": "sip:10.101.60.115:5060",
// "interval_sec": 5, // "interval_sec": 5,
// "max_calls": 150, // "max_calls": 150,
// "ami": { "host": "127.0.0.1", "port": 6154, "username": "admin", "password": "changeme" } // "ami": { "host": "127.0.0.1", "port": 6154, "username": "ctt", "password": "cttpass" }
// } // }
package main package main
+3 -3
View File
@@ -1,10 +1,10 @@
{ {
"node_id": "ses-pbx", "node_id": "ses-sip",
"type": "freeswitch", "type": "freeswitch",
"nats_url": "nats://balancer.lan:4222", "nats_url": "nats://10.101.60.81:4222",
"interval_sec": 5, "interval_sec": 5,
"max_calls": 250, "max_calls": 250,
"sip_gateway": "sip:ses-pbx.lan:5060", "sip_gateway": "sip:10.3.0.44:5060",
"sip_gateway_auto": false, "sip_gateway_auto": false,
"failure_window": 1000, "failure_window": 1000,
"esl": { "esl": {
+5 -5
View File
@@ -1,16 +1,16 @@
{ {
"node_id": "uc-pbx", "node_id": "uc06",
"type": "asterisk", "type": "asterisk",
"nats_url": "nats://balancer.lan:4222", "nats_url": "nats://10.101.60.81:4222",
"interval_sec": 5, "interval_sec": 5,
"max_calls": 150, "max_calls": 150,
"sip_gateway": "sip:uc-pbx.lan:5060", "sip_gateway": "sip:10.101.60.115:5060",
"sip_gateway_auto": false, "sip_gateway_auto": false,
"failure_window": 1000, "failure_window": 1000,
"ami": { "ami": {
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 6154, "port": 6154,
"username": "admin", "username": "ctt",
"password": "changeme" "password": "cttpass"
} }
} }
+9 -9
View File
@@ -48,24 +48,24 @@ make build-agent
```json ```json
{ {
"node_id": "pbx-01", "node_id": "uc06",
"type": "asterisk", "type": "asterisk",
"nats_url": "nats://balancer.host:4222", "nats_url": "nats://balancer.host:4222",
"nats_user": "", "nats_user": "",
"nats_password": "", "nats_password": "",
"interval_sec": 5, "interval_sec": 5,
"max_calls": 150, "max_calls": 150,
"sip_gateway": "sip:pbx-01.lan:5060", "sip_gateway": "sip:10.101.60.115:5060",
"sip_gateway_auto": false, "sip_gateway_auto": false,
"failure_window": 1000, "failure_window": 1000,
"esl": { "host": "127.0.0.1", "port": 8021, "password": "ClueCon" }, "esl": { "host": "127.0.0.1", "port": 8021, "password": "ClueCon" },
"ami": { "host": "127.0.0.1", "port": 6154, "username": "admin", "password": "changeme" } "ami": { "host": "127.0.0.1", "port": 6154, "username": "ctt", "password": "cttpass" }
} }
``` ```
| Field | Type | Default | Description | | Field | Type | Default | Description |
|-------|------|---------|-------------| |-------|------|---------|-------------|
| `node_id` | string | — | Unique node ID (pbx-01, pbx-02) | | `node_id` | string | — | Unique node ID (uc06, ses-sip) |
| `type` | string | — | `freeswitch` or `asterisk` | | `type` | string | — | `freeswitch` or `asterisk` |
| `nats_url` | string | — | NATS server URL | | `nats_url` | string | — | NATS server URL |
| `nats_user` | string | `""` | NATS username | | `nats_user` | string | `""` | NATS username |
@@ -113,8 +113,8 @@ New PBX type = new `collector_<type>.go` implementing 5 interface methods. No ex
| File | Target | Type | | File | Target | Type |
|------|--------|------| |------|--------|------|
| `contrib/agent-uc.json` | Asterisk UC nodes | asterisk | | `contrib/agent-uc.json` | Asterisk UC nodes (05, 06, 66-69) | asterisk |
| `contrib/agent-sessip.json` | FreeSWITCH ses-sip | freeswitch | | `contrib/agent-sessip.json` | FreeSWITCH ses-sip (10.3.0.44) | freeswitch |
## Deployment ## Deployment
@@ -123,13 +123,13 @@ New PBX type = new `collector_<type>.go` implementing 5 interface methods. No ex
make build-agent make build-agent
# 2. Copy to node # 2. Copy to node
scp bin/pulse-lets-go-agent admin@pbx-node:/usr/local/bin/ scp bin/pulse-lets-go-agent devadmin@NODE_IP:/usr/local/bin/
# 3. Create config # 3. Create config
scp contrib/agent-uc.json admin@pbx-node:/etc/pulse-lets-go-agent/agent.json scp contrib/agent-uc.json devadmin@NODE_IP:/etc/pulse-lets-go-agent/agent.json
# 4. Start # 4. Start
ssh admin@pbx-node "pulse-lets-go-agent -config /etc/pulse-lets-go-agent/agent.json" ssh devadmin@NODE_IP "pulse-lets-go-agent -config /etc/pulse-lets-go-agent/agent.json"
``` ```
## Systemd Unit ## Systemd Unit
+14 -188
View File
@@ -17,36 +17,9 @@ Roles: `admin` (full access), `viewer` (read-only nodes/metrics).
|--------|------|------|-------------| |--------|------|------|-------------|
| `GET` | `/api/route` | | Route a call. Query: `caller_id`, `dest`, `ingress_trunk` | | `GET` | `/api/route` | | Route a call. Query: `caller_id`, `dest`, `ingress_trunk` |
| `PUT` | `/api/route/mode` | admin | Set balancing mode: `{"mode":"best\|weighted_random"}` — см. [Routing](routing.md#balancing-modes) | | `PUT` | `/api/route/mode` | admin | Set balancing mode: `{"mode":"best\|weighted_random"}` — см. [Routing](routing.md#balancing-modes) |
| `GET` | `/api/health` | | Full health status with connections and stats | | `GET` | `/api/health` | | Stats: total nodes, healthy, requests, uptime |
| `GET` | `/api/health/live` | | Liveness probe always 200 `{"status":"alive"}` | | `GET` | `/api/health/live` | | Liveness probe (always 200) |
| `GET` | `/api/health/ready` | | Readiness — 200 `{"status":"ready"}` или 503 `{"reason":"nats_disconnected"}` | | `GET` | `/api/health/ready` | | Readiness probe (NATS + metrics) |
### Health Response
```json
{
"status": "ok",
"version": "1.0.0",
"connections": {
"nats": "connected|disconnected",
"esl": {
"status": "disabled|connected|disconnected",
"host": "127.0.0.1",
"uptime_seconds": 3600,
"reconnects": 0,
"gateway_ops": 42,
"events_recv": 15000
}
},
"stats": {
"total_nodes": 3,
"healthy_nodes": 2,
"route_requests": 5000,
"route_fallbacks": 10,
"uptime_seconds": 7200
}
}
```
### Route Response (success) ### Route Response (success)
@@ -56,13 +29,6 @@ Roles: `admin` (full access), `viewer` (read-only nodes/metrics).
`matched_rule` — имя сработавшего routing rule, либо `"none"` если применился глобальный scoring. `matched_rule` — имя сработавшего routing rule, либо `"none"` если применился глобальный scoring.
### Route Mode Response
`PUT /api/route/mode` возвращает текущий режим:
```json
{ "mode": "best" }
```
### Route Response (fallback — all nodes unhealthy) ### Route Response (fallback — all nodes unhealthy)
```json ```json
@@ -70,19 +36,14 @@ Roles: `admin` (full access), `viewer` (read-only nodes/metrics).
"fallback": true, "fallback": true,
"sip_gateway": "sip:operator.lan:5060", "sip_gateway": "sip:operator.lan:5060",
"reason": "all_nodes_unhealthy", "reason": "all_nodes_unhealthy",
"matched_rule": "my-pool-rule", "nodes": [...]
"nodes": [
{ "id": "pbx-01", "score": -100, "reason": "disabled", "status": "disabled" },
{ "id": "pbx-02", "score": -1, "reason": "stale", "status": "stale" },
{ "id": "pbx-03", "score": 70, "reason": "", "status": "healthy" }
]
} }
``` ```
### Route Response (no nodes registered) ### Route Response (no nodes registered)
```json ```json
{ "error": "no_nodes_registered", "matched_rule": "none" } { "error": "no_nodes_registered" }
``` ```
## Nodes ## Nodes
@@ -91,42 +52,7 @@ Roles: `admin` (full access), `viewer` (read-only nodes/metrics).
|--------|------|------|-------------| |--------|------|------|-------------|
| `GET` | `/api/nodes` | admin/viewer | List all nodes with scores and state | | `GET` | `/api/nodes` | admin/viewer | List all nodes with scores and state |
| `GET` | `/api/nodes/{id}/metrics` | admin/viewer | Metrics history (ring buffer, ~30 min) | | `GET` | `/api/nodes/{id}/metrics` | admin/viewer | Metrics history (ring buffer, ~30 min) |
| `PUT` | `/api/nodes/{id}/toggle` | admin | `{"disabled":true, "reason":"..."}` — returns updated `NodeInfo` | | `PUT` | `/api/nodes/{id}/toggle` | admin | `{"disabled":true, "reason":"..."}` |
### NodeInfo (GET /api/nodes)
```json
{
"node_id": "pbx-01",
"ts": 1719000000,
"status": "ok",
"active_calls": 42,
"max_calls": 250,
"idle_cpu": 35.0,
"load_avg": 0.85,
"call_failure_rate": 0.5,
"disabled": false,
"disabled_reason": "",
"score": 88.0,
"lethal_reason": "",
"is_stale": false,
"seconds_ago": 3.0
}
```
### Node Metrics (GET /api/nodes/{id}/metrics)
История метрик ноды из ring buffer (~30 мин, шаг 5 с):
```json
{
"node_id": "pbx-01",
"snapshots": [
{ "active_calls": 42, "load_avg": 0.85, "score": 88.0, ... }
],
"count": 360
}
```
## Trunks ## Trunks
@@ -162,32 +88,16 @@ Trunk types:
| `balance` | Destination trunk (bound to a Node) | 1..N | | `balance` | Destination trunk (bound to a Node) | 1..N |
| `fallback` | Operator fallback (when all balance score < 0) | exactly 1 | | `fallback` | Operator fallback (when all balance score < 0) | exactly 1 |
## Routing Rules
| Method | Path | Role | Description |
|--------|------|------|-------------|
| `GET` | `/api/routing/rules` | admin/viewer | Список правил с счётчиками hits |
| `POST` | `/api/routing/rules` | admin | Создать правило |
| `PUT` | `/api/routing/rules/{id}` | admin | Обновить правило |
| `DELETE` | `/api/routing/rules/{id}` | admin | Удалить правило |
| `PUT` | `/api/routing/rules/{id}/toggle` | admin | Вкл/выкл: `{"enabled":true}` |
| `PUT` | `/api/routing/rules/reorder` | admin | Новый порядок: `["rr-abc", "rr-def", ...]` |
Поля правила — см. [docs/routing.md](routing.md#routing-rules). Ответы содержат полную модель правила.
## Users (admin only) ## Users (admin only)
| Method | Path | Description | | Method | Path | Description |
|--------|------|-------------| |--------|------|-------------|
| `GET` | `/api/users` | List users (без password_hash) | | `GET` | `/api/users` | List users |
| `POST` | `/api/users` | `{"username":"...","password":"...","role":"admin\|viewer"}` | | `POST` | `/api/users` | Create user |
| `PUT` | `/api/users/{id}` | Partial update: `{"username":"...","role":"viewer"}` | | `PUT` | `/api/users/{id}` | Update user |
| `DELETE` | `/api/users/{id}` | Удалить (204 No Content) | | `DELETE` | `/api/users/{id}` | Delete user |
Защиты: Note: `user-01` (primary admin) cannot be deleted or demoted to viewer.
- `user-01` нельзя удалить или понизить до viewer
- Нельзя удалить последнего администратора
- GET-ответ исключает поле `password_hash`
## Monitoring (API-key auth) ## Monitoring (API-key auth)
@@ -195,67 +105,14 @@ Use `X-API-Key` header (not JWT).
| Method | Path | Description | | Method | Path | Description |
|--------|------|-------------| |--------|------|-------------|
| `GET` | `/api/monitoring/zabbix` | JSON для Zabbix LLD | | `GET` | `/api/monitoring/zabbix` | JSON for Zabbix LLD + items |
| `GET` | `/api/monitoring/prometheus` | `text/plain` метрики для Prometheus | | `GET` | `/api/monitoring/prometheus` | `text/plain` metrics for Prometheus |
### Zabbix Response
```json
{
"data": [
{
"{#NODE_ID}": "pbx-01",
"{#STATUS}": "ok",
"{#ACTIVE_CALLS}": 42,
"{#MAX_CALLS}": 250,
"{#IDLE_CPU}": 35.0,
"{#LOAD_AVG}": 0.85,
"{#CALL_FAILURE_RATE}": 0.5,
"{#SCORE}": 88.0,
"{#DISABLED}": false,
"{#IS_STALE}": false,
"{#SECONDS_AGO}": 3.0
}
],
"route_requests_total": 5000,
"route_fallbacks_total": 42,
"uptime_seconds": 7200
}
```
### Prometheus Metrics
| Метрика | Type | Labels |
|---------|------|--------|
| `pulse_route_requests_total` | counter | |
| `pulse_route_fallbacks_total` | counter | |
| `pulse_nodes_total` | gauge | |
| `pulse_nodes_healthy` | gauge | |
| `pulse_uptime_seconds` | gauge | |
| `pulse_node_active_calls` | gauge | `node` |
| `pulse_node_max_calls` | gauge | `node` |
| `pulse_node_idle_cpu` | gauge | `node` |
| `pulse_node_load_avg` | gauge | `node` |
| `pulse_node_call_failure_rate` | gauge | `node` |
| `pulse_node_score` | gauge | `node` |
| `pulse_node_seconds_ago` | gauge | `node` |
| `pulse_node_disabled` | gauge | `node` (0/1) |
| `pulse_node_stale` | gauge | `node` (0/1) |
| `pulse_node_status_ok` | gauge | `node` (0/1) |
## WebSocket ## WebSocket
| Method | Path | Description | | Method | Path | Description |
|--------|------|-------------| |--------|------|-------------|
| `WS` | `/ws/metrics` | Real-time metrics (JWT в query `?token=`) | | `WS` | `/ws/metrics` | Real-time metrics stream for frontend (JWT in query `?token=`) |
Типы сообщений (поле `type`):
| Тип | Payload | Описание |
|-----|---------|----------|
| `nodes_update` | `[]NodeInfo` | Обновление метрик всех нод (каждые 5 с) |
| `balancer_update` | `BalancerInfo` | Состояние балансировщика (uptime, mode, ESL, NATS) |
| `gateway_update` | `{"gateway","trunk_id","status"}` | Статус FS-gateway (up/down) |
## Examples ## Examples
@@ -278,37 +135,6 @@ curl -X PUT http://localhost:8080/api/nodes/pbx-01/toggle \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{"disabled":true,"reason":"maintenance"}' -d '{"disabled":true,"reason":"maintenance"}'
# Route mode (admin)
curl -X PUT http://localhost:8080/api/route/mode \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"mode":"weighted_random"}'
# → {"mode":"weighted_random"}
# Routing rules (admin)
curl -X POST http://localhost:8080/api/routing/rules \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name":"VIP pool",
"priority":10,
"match_caller_id":"^7495",
"action_type":"pool",
"action_pool_ids":["pbx-01","pbx-02"]
}'
# Toggle rule (admin)
curl -X PUT http://localhost:8080/api/routing/rules/rr-abc123/toggle \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"enabled":false}'
# Reorder rules (admin)
curl -X PUT http://localhost:8080/api/routing/rules/reorder \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '["rr-def456", "rr-abc123"]'
# Prometheus metrics (API-key) # Prometheus metrics (API-key)
curl -H "X-API-Key: $(jq -r '.monitoring_api_key' data/config.json)" \ curl -H "X-API-Key: $(jq -r '.monitoring_api_key' data/config.json)" \
http://localhost:8080/api/monitoring/prometheus http://localhost:8080/api/monitoring/prometheus
+7 -7
View File
@@ -60,14 +60,14 @@ WantedBy=multi-user.target
```bash ```bash
make deploy make deploy
scp -r /opt/pulse-lets-go admin@balancer.lan:/opt/ scp -r /opt/pulse-lets-go devadmin@10.101.60.81:/opt/
scp contrib/route.lua admin@balancer.lan:/tmp/ scp contrib/route.lua devadmin@10.101.60.81:/tmp/
``` ```
### Step 2 — NATS Server (if not installed) ### Step 2 — NATS Server (if not installed)
```bash ```bash
ssh admin@balancer.lan ssh devadmin@10.101.60.81
sudo mkdir -p /opt/nats sudo mkdir -p /opt/nats
# Install nats-server from repository or copy binary # Install nats-server from repository or copy binary
# Run: nats-server -p 4222 -D & # Run: nats-server -p 4222 -D &
@@ -79,11 +79,11 @@ Repeat for each UC/Asterisk node:
```bash ```bash
# Copy binary and config to each PBX: # Copy binary and config to each PBX:
scp bin/pulse-lets-go-agent admin@pbx-node:/usr/local/bin/ scp bin/pulse-lets-go-agent devadmin@PBX_IP:/usr/local/bin/
scp contrib/agent-uc.json admin@pbx-node:/etc/pulse-lets-go-agent/agent.json scp contrib/agent-uc.json devadmin@PBX_IP:/etc/pulse-lets-go-agent/agent.json
# Start: # Start:
ssh admin@pbx-node "systemctl start pulse-lets-go-agent" ssh devadmin@PBX_IP "systemctl start pulse-lets-go-agent"
# Check log: # Check log:
# journalctl -u pulse-lets-go-agent -f # journalctl -u pulse-lets-go-agent -f
@@ -121,7 +121,7 @@ Trunks are created automatically when an agent sends its first metric with `sip_
```bash ```bash
curl -X POST .../api/trunks \ curl -X POST .../api/trunks \
-d '{"name":"pbx-01","type":"balance","node_id":"pbx-01","gateway":"sip:pbx-01.lan:5060","enabled":true}' -d '{"name":"uc06","type":"balance","node_id":"uc06","gateway":"sip:10.101.60.115:5060","enabled":true}'
``` ```
### Step 7 — Start pulse-lets-go ### Step 7 — Start pulse-lets-go
-15
View File
@@ -90,21 +90,6 @@ Authorization: Bearer <admin-jwt>
Допустимые значения: `"best"`, `"weighted_random"`. API-эндпоинт сразу применяет режим и сохраняет в `config.json`. Допустимые значения: `"best"`, `"weighted_random"`. API-эндпоинт сразу применяет режим и сохраняет в `config.json`.
## Routing Rules API
Правила маршрутизации CRUD — `/api/routing/rules*`. Все эндпоинты требуют JWT (admin).
| Метод | Путь | Описание |
|-------|------|----------|
| `GET` | `/api/routing/rules` | Список всех правил (с hits) |
| `POST` | `/api/routing/rules` | Создать новое правило |
| `PUT` | `/api/routing/rules/{id}` | Обновить правило (частично) |
| `DELETE` | `/api/routing/rules/{id}` | Удалить правило |
| `PUT` | `/api/routing/rules/{id}/toggle` | Вкл/выкл: `{"enabled":true}` |
| `PUT` | `/api/routing/rules/reorder` | Новый порядок: `["rr-abc", "rr-def"]` — переназначает Priority |
Правила хранятся в `data/routes.json` и применяются в порядке `Priority` (first-match-wins).
## Ответ /api/route ## Ответ /api/route
В ответе маршрутизации появилось поле `matched_rule` — имя сработавшего правила (или `"none"` если правило не применилось): В ответе маршрутизации появилось поле `matched_rule` — имя сработавшего правила (или `"none"` если правило не применилось):
+1 -11
View File
@@ -29,13 +29,7 @@ func (a *API) handleZabbix(w http.ResponseWriter, r *http.Request) {
}) })
} }
stats := a.engine.GetHealthStats() writeJSON(w, http.StatusOK, models.ZabbixResponse{Data: data})
writeJSON(w, http.StatusOK, models.ZabbixResponse{
Data: data,
RouteRequests: stats.RouteRequests,
RouteFallbacks: stats.RouteFallbacks,
UptimeSeconds: stats.UptimeSeconds,
})
} }
// handlePrometheus — GET /api/monitoring/prometheus — text/plain метрики для Prometheus. // handlePrometheus — GET /api/monitoring/prometheus — text/plain метрики для Prometheus.
@@ -58,10 +52,6 @@ func (a *API) handlePrometheus(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(&sb, "# TYPE pulse_nodes_healthy gauge\n") fmt.Fprintf(&sb, "# TYPE pulse_nodes_healthy gauge\n")
fmt.Fprintf(&sb, "pulse_nodes_healthy %d\n", stats.HealthyNodes) fmt.Fprintf(&sb, "pulse_nodes_healthy %d\n", stats.HealthyNodes)
fmt.Fprintf(&sb, "# HELP pulse_route_fallbacks_total Total number of fallback route requests.\n")
fmt.Fprintf(&sb, "# TYPE pulse_route_fallbacks_total counter\n")
fmt.Fprintf(&sb, "pulse_route_fallbacks_total %d\n", stats.RouteFallbacks)
fmt.Fprintf(&sb, "# HELP pulse_uptime_seconds Uptime in seconds.\n") fmt.Fprintf(&sb, "# HELP pulse_uptime_seconds Uptime in seconds.\n")
fmt.Fprintf(&sb, "# TYPE pulse_uptime_seconds gauge\n") fmt.Fprintf(&sb, "# TYPE pulse_uptime_seconds gauge\n")
fmt.Fprintf(&sb, "pulse_uptime_seconds %d\n", stats.UptimeSeconds) fmt.Fprintf(&sb, "pulse_uptime_seconds %d\n", stats.UptimeSeconds)
+1 -4
View File
@@ -241,10 +241,7 @@ type ZabbixNode struct {
// ZabbixResponse — ответ на запрос Zabbix LLD. // ZabbixResponse — ответ на запрос Zabbix LLD.
type ZabbixResponse struct { type ZabbixResponse struct {
Data []ZabbixNode `json:"data"` Data []ZabbixNode `json:"data"`
RouteRequests int64 `json:"route_requests_total"`
RouteFallbacks int64 `json:"route_fallbacks_total"`
UptimeSeconds int64 `json:"uptime_seconds"`
} }
// --- Balancer --- // --- Balancer ---