- 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)
108 lines
3.0 KiB
Go
108 lines
3.0 KiB
Go
package agent
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/pulse-lets-go/internal/ami"
|
||
)
|
||
|
||
// AsteriskCollector собирает метрики с Asterisk через AMI.
|
||
type AsteriskCollector struct {
|
||
client *ami.Client
|
||
}
|
||
|
||
// NewAsteriskCollector создаёт Asterisk-коллектор.
|
||
func NewAsteriskCollector(host string, port int, username, password string) *AsteriskCollector {
|
||
return &AsteriskCollector{
|
||
client: ami.NewClient(host, port, username, password),
|
||
}
|
||
}
|
||
|
||
func (ac *AsteriskCollector) Type() string { return "asterisk" }
|
||
|
||
func (ac *AsteriskCollector) Connect() error {
|
||
return ac.client.Connect()
|
||
}
|
||
|
||
func (ac *AsteriskCollector) Collect() (*PBXResult, error) {
|
||
if !ac.client.IsConnected() {
|
||
return nil, fmt.Errorf("ami не подключён")
|
||
}
|
||
|
||
result := &PBXResult{Status: "ok"}
|
||
|
||
// active_calls: CoreShowChannels определяет количество активных каналов
|
||
if active, err := ac.coreShowChannelsCount(); err == nil {
|
||
result.ActiveCalls = active
|
||
}
|
||
|
||
// max_calls: CoreSettings → CoreMaxCalls
|
||
if max, err := ac.coreSettingsInt("CoreMaxCalls"); err == nil {
|
||
result.MaxCalls = max
|
||
}
|
||
|
||
// idle_cpu: Asterisk не даёт FS-шного idle_cpu, используем системный
|
||
result.IdleCPU = 0
|
||
|
||
return result, nil
|
||
}
|
||
|
||
func (ac *AsteriskCollector) ListenHangups(onHangup func(success bool)) {
|
||
// Подписываемся на события Hangup через AMI
|
||
ac.client.Subscribe("call")
|
||
|
||
go func() {
|
||
for evt := range ac.client.Events() {
|
||
if evt.Headers["Event"] == "Hangup" {
|
||
cause := evt.Headers["Cause"]
|
||
// Считаем успешным только NORMAL_CLEARING (код 16)
|
||
success := cause == "16" || strings.Contains(cause, "NORMAL_CLEARING")
|
||
onHangup(success)
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
func (ac *AsteriskCollector) Close() {
|
||
ac.client.Disconnect()
|
||
}
|
||
|
||
// --- helpers ---
|
||
|
||
// coreShowChannelsCount получает количество активных каналов.
|
||
func (ac *AsteriskCollector) coreShowChannelsCount() (int, error) {
|
||
cmd := "Action: Command\r\nCommand: core show channels count\r\n"
|
||
headers, body, err := ac.client.SendCommandBody(cmd)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
_ = headers
|
||
|
||
// Ответ выглядит как: "1 active call" или "5 active calls"
|
||
body = strings.TrimSpace(body)
|
||
fields := strings.Fields(body)
|
||
if len(fields) >= 3 {
|
||
// Первое слово — число
|
||
n, err := strconv.Atoi(fields[0])
|
||
if err == nil {
|
||
return n, nil
|
||
}
|
||
}
|
||
return 0, fmt.Errorf("не удалось распарсить количество каналов из: %s", body)
|
||
}
|
||
|
||
// coreSettingsInt получает целочисленное значение из CoreSettings.
|
||
func (ac *AsteriskCollector) coreSettingsInt(key string) (int, error) {
|
||
headers, _, err := ac.client.SendCommandBody("Action: CoreSettings\r\n")
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
val := headers[key]
|
||
if val == "" {
|
||
return 0, fmt.Errorf("поле %s не найдено", key)
|
||
}
|
||
return strconv.Atoi(strings.TrimSpace(val))
|
||
}
|