Maksim Totmin c713a4f142 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

175 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}