- 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)
175 lines
4.7 KiB
Go
175 lines
4.7 KiB
Go
package agent
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"os/signal"
|
||
"sync/atomic"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/pulse-lets-go/internal/models"
|
||
)
|
||
|
||
const maxConsecutiveFailures = 5
|
||
|
||
// Agent — главный цикл сбора метрик и публикации в NATS.
|
||
type Agent struct {
|
||
cfg *Config
|
||
publisher *Publisher
|
||
collector Collector
|
||
failtrack *FailureTracker
|
||
pubCount atomic.Int64
|
||
}
|
||
|
||
// NewAgent создаёт агента на основе конфигурации.
|
||
func NewAgent(cfg *Config) (*Agent, error) {
|
||
if err := cfg.Validate(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Publisher
|
||
pub, err := NewPublisher(cfg.NatsURL, cfg.NatsUser, cfg.NatsPassword, cfg.NodeID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Collector — в зависимости от типа PBX
|
||
var col Collector
|
||
switch cfg.Type {
|
||
case "freeswitch":
|
||
col = NewFSCollector(cfg.ESL.Host, cfg.ESL.Port, cfg.ESL.Password)
|
||
case "asterisk":
|
||
col = NewAsteriskCollector(cfg.AMI.Host, cfg.AMI.Port, cfg.AMI.Username, cfg.AMI.Password)
|
||
default:
|
||
pub.Close()
|
||
return nil, fmt.Errorf("неподдерживаемый тип PBX: %s", cfg.Type)
|
||
}
|
||
|
||
if err := col.Connect(); err != nil {
|
||
pub.Close()
|
||
return nil, fmt.Errorf("подключение к %s: %w", cfg.Type, err)
|
||
}
|
||
|
||
// Failure rate tracker
|
||
ft := NewFailureTracker(cfg.FailureWindow)
|
||
|
||
// Подписка на hangup-события
|
||
col.ListenHangups(func(success bool) {
|
||
ft.Record(success)
|
||
})
|
||
|
||
return &Agent{
|
||
cfg: cfg,
|
||
publisher: pub,
|
||
collector: col,
|
||
failtrack: ft,
|
||
}, nil
|
||
}
|
||
|
||
// Start запускает главный цикл агента: сбор метрик → публикация.
|
||
// Блокирует до получения сигнала завершения (SIGINT, SIGTERM).
|
||
func (a *Agent) Start() {
|
||
log.Printf("[agent] запущен: node=%s, type=%s, interval=%ds",
|
||
a.cfg.NodeID, a.cfg.Type, a.cfg.IntervalSec)
|
||
|
||
ticker := time.NewTicker(time.Duration(a.cfg.IntervalSec) * time.Second)
|
||
defer ticker.Stop()
|
||
|
||
quit := make(chan os.Signal, 1)
|
||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||
|
||
// Счётчик последовательных ошибок сбора
|
||
consecutiveFailures := 0
|
||
|
||
// Первый сбор сразу
|
||
a.collectAndPublish(&consecutiveFailures)
|
||
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
a.collectAndPublish(&consecutiveFailures)
|
||
case <-quit:
|
||
log.Println("[agent] получен сигнал завершения")
|
||
a.Shutdown()
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
// collectAndPublish выполняет один цикл: сбор метрик → публикация.
|
||
func (a *Agent) collectAndPublish(failures *int) {
|
||
// 1. Системные метрики (CPU, load)
|
||
sys := CollectSystem()
|
||
|
||
// 2. Метрики PBX (calls, max_calls, статус)
|
||
pbx, err := a.collector.Collect()
|
||
if err != nil {
|
||
*failures++
|
||
log.Printf("[agent] ошибка сбора метрик (попытка %d): %v", *failures, err)
|
||
|
||
if *failures >= maxConsecutiveFailures {
|
||
// После N неудач — считаем ноду "down"
|
||
pbx = &PBXResult{
|
||
Status: "down",
|
||
}
|
||
} else {
|
||
// Пробуем ещё раз через секунду
|
||
time.Sleep(1 * time.Second)
|
||
pbx, err = a.collector.Collect()
|
||
if err != nil {
|
||
return // пропускаем этот тик
|
||
}
|
||
*failures = 0
|
||
}
|
||
} else {
|
||
*failures = 0
|
||
}
|
||
|
||
// 3. Failure rate
|
||
failRate := a.failtrack.Rate()
|
||
|
||
// 4. SIP gateway
|
||
sipGW := a.cfg.GetSIPGateway("")
|
||
|
||
// 5. Формируем метрику
|
||
metric := ToMetric(a.cfg.NodeID, pbx, sys, failRate, sipGW, a.cfg.MaxCalls)
|
||
|
||
// 6. Публикуем
|
||
if err := a.publisher.Publish(metric); err != nil {
|
||
log.Printf("[agent] ошибка публикации: %v", err)
|
||
return
|
||
}
|
||
|
||
a.pubCount.Add(1)
|
||
log.Printf("[agent] опубликована метрика: calls=%d/%d load=%.2f fail=%.1f%% cpu=%.0f%% status=%s",
|
||
metric.ActiveCalls, metric.MaxCalls, metric.LoadAvg, metric.CallFailureRate, metric.IdleCPU, metric.Status)
|
||
}
|
||
|
||
// Shutdown gracefully shuts down the agent.
|
||
func (a *Agent) Shutdown() {
|
||
log.Printf("[agent] остановка, всего опубликовано метрик: %d", a.pubCount.Load())
|
||
if a.collector != nil {
|
||
a.collector.Close()
|
||
}
|
||
if a.publisher != nil {
|
||
a.publisher.Close()
|
||
}
|
||
}
|
||
|
||
// CollectAndPublish экспортированный метод для ручного вызова (тесты, дебаг).
|
||
func (a *Agent) CollectAndPublish() *models.NodeMetric {
|
||
sys := CollectSystem()
|
||
pbx, err := a.collector.Collect()
|
||
if err != nil {
|
||
pbx = &PBXResult{Status: "down"}
|
||
}
|
||
failRate := a.failtrack.Rate()
|
||
sipGW := a.cfg.GetSIPGateway("")
|
||
metric := ToMetric(a.cfg.NodeID, pbx, sys, failRate, sipGW, a.cfg.MaxCalls)
|
||
a.publisher.Publish(metric)
|
||
a.pubCount.Add(1)
|
||
return metric
|
||
}
|