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)) }