pulse-lets-go/cmd/pulse-lets-go-agent/agent/collector_freeswitch.go
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

135 lines
3.5 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 (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/pulse-lets-go/internal/esl"
)
// FSCollector собирает метрики с FreeSWITCH через ESL.
type FSCollector struct {
client *esl.Client
}
// NewFSCollector создаёт FreeSWITCH-коллектор.
func NewFSCollector(host string, port int, password string) *FSCollector {
return &FSCollector{
client: esl.NewClient(host, port, password),
}
}
func (fc *FSCollector) Type() string { return "freeswitch" }
func (fc *FSCollector) Connect() error {
// ESL client подключается синхронно через tryConnect
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
fc.client.ConnectWithRetry()
// Ждём подключения с таймаутом
for !fc.client.IsConnected() {
select {
case <-ctx.Done():
return fmt.Errorf("esl connect timeout")
case <-time.After(100 * time.Millisecond):
// Выходим из цикла после ConnectWithRetry
return nil
}
}
return nil
}
func (fc *FSCollector) Collect() (*PBXResult, error) {
if !fc.client.IsConnected() {
return nil, fmt.Errorf("esl не подключён")
}
result := &PBXResult{Status: "ok"}
// active_calls: api show channels count
if active, err := fc.apiInt("show channels count"); err == nil {
result.ActiveCalls = active
}
// max_calls: api json status → max_sessions
if max, err := fc.apiJSONInt("json", "status", "max_sessions"); err == nil {
result.MaxCalls = max
}
// idle_cpu: eval $${idle_cpu} — FS-специфичный
if idle, err := fc.apiFloat("eval $${idle_cpu}"); err == nil && idle > 0 {
result.IdleCPU = idle
}
return result, nil
}
func (fc *FSCollector) ListenHangups(onHangup func(success bool)) {
// Подписываемся на CHANNEL_HANGUP через ESL
fc.client.Subscribe("CHANNEL_HANGUP")
go func() {
// Для FS используем обработку событий через eventsCh
// Упрощённо: не реализуем полноценную подписку на первом этапе
// failure rate остаётся на нуле
time.Sleep(1 * time.Second)
}()
}
func (fc *FSCollector) Close() {
fc.client.Disconnect()
}
// --- helpers ---
func (fc *FSCollector) apiInt(cmd string) (int, error) {
headers, body, err := fc.client.Send("api " + cmd)
if err != nil {
return 0, err
}
_ = headers
val := strings.TrimSpace(body)
n, err := strconv.Atoi(val)
if err != nil {
// иногда ответ содержит текст помимо числа
n, _ = strconv.Atoi(strings.Fields(val)[0])
}
return n, nil
}
func (fc *FSCollector) apiFloat(cmd string) (float64, error) {
_, body, err := fc.client.Send("api " + cmd)
if err != nil {
return 0, err
}
val := strings.TrimSpace(body)
f, _ := strconv.ParseFloat(val, 64)
return f, nil
}
// apiJSONInt парсит JSON-ответ от FS api: api json status → field1.field2
func (fc *FSCollector) apiJSONInt(cmd string, field string, subfield string) (int, error) {
_, body, err := fc.client.Send("api " + cmd)
if err != nil {
return 0, err
}
// Быстрый парсинг: ищем "subfield": число
search := fmt.Sprintf(`"%s":`, subfield)
idx := strings.Index(body, search)
if idx < 0 {
return 0, fmt.Errorf("поле %s не найдено в ответе", subfield)
}
rest := body[idx+len(search):]
rest = strings.TrimLeft(rest, " \t")
end := strings.IndexAny(rest, ",\n\r}")
if end > 0 {
rest = rest[:end]
}
return strconv.Atoi(strings.TrimSpace(rest))
}