Metrics Agent (pulse-lets-go-agent)
The agent runs on each PBX node (FreeSWITCH or Asterisk), collects metrics, and publishes them to NATS every 5 seconds.
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) │
└─────────────────┘
NATS Message Format
{
"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,
"sip_gateway": "sip:pbx-01.lan:5060"
}
status: "ok" or any error string (triggers lethal)
call_failure_rate: 0.0..100.0 (percentage)
sip_gateway: optional, triggers auto-trunk creation on first metric
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: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" }
}
| Field |
Type |
Default |
Description |
node_id |
string |
— |
Unique node ID (uc06, ses-sip) |
type |
string |
— |
freeswitch or asterisk |
nats_url |
string |
— |
NATS server URL |
nats_user |
string |
"" |
NATS username |
nats_password |
string |
"" |
NATS password |
interval_sec |
int |
5 |
Metric publish interval |
max_calls |
int |
250 |
Node capacity (fallback) |
sip_gateway |
string |
"" |
SIP address for auto-trunk creation |
sip_gateway_auto |
bool |
false |
Auto-detect SIP from PBX |
failure_window |
int |
1000 |
Window size for call_failure_rate |
esl.* |
object |
— |
FreeSWITCH ESL settings (host, port, password) |
ami.* |
object |
— |
Asterisk AMI settings (host, port, username, password) |
Collectors
| Collector |
Source |
Metrics |
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
Sliding window of last N (default 1000) completed calls.
| Outcome |
FS (ESL) |
Asterisk (AMI) |
| Success |
CHANNEL_HANGUP + Hangup-Cause: NORMAL_CLEARING |
Hangup + Cause: 16 |
| Failure |
Any other Hangup-Cause |
Any other Cause |
Collector Interface (extensibility)
type Collector interface {
Type() string // "freeswitch" | "asterisk"
Connect() error // ESL auth / AMI login
Collect() (*PBXResult, error) // Collect metrics
ListenHangup(onHangup func(bool)) // Subscribe to hangup events
Close() // Close connection
}
New PBX type = new collector_<type>.go implementing 5 interface methods. No existing code changes needed.
Template Configs
| File |
Target |
Type |
contrib/agent-uc.json |
Asterisk UC nodes (05, 06, 66-69) |
asterisk |
contrib/agent-sessip.json |
FreeSWITCH ses-sip (ses-pbx.lan) |
freeswitch |
Deployment
# 1. Build
make build-agent
# 2. Copy to node
scp bin/pulse-lets-go-agent admin@NODE_IP:/usr/local/bin/
# 3. Create config
scp contrib/agent-uc.json admin@NODE_IP:/etc/pulse-lets-go-agent/agent.json
# 4. Start
ssh admin@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
# On PBX node — check agent log:
journalctl -u pulse-lets-go-agent -f
# Expected:
# [publisher] NATS connected to nats://host:4222
# [ami] connected to 127.0.0.1:6154 (for Asterisk)
# [agent] published metric: calls=0/150 load=0.28 cpu=99% status=ok
# On balancer — check node appeared:
curl http://localhost:8080/api/nodes | jq '.[] | {node_id, score, active_calls}'
Troubleshooting
| Problem |
Cause |
Solution |
nats: no servers available |
NATS not running or unreachable |
systemctl start nats-server |
ami auth rejected |
Wrong password or access |
Check /etc/asterisk/manager.conf |
esl connect timeout |
ESL not listening on port |
fs_cli -x "load mod_event_socket" |
calls=0/0 |
max_calls not obtained |
Set max_calls in agent.json |