pulse-lets-go/cmd/pulse-lets-go-agent/agent/collector_freeswitch.go
Maksim Totmin 40b8afb150 refactor: common util package, ESL/AMI/security fixes, Prometheus metrics
Backend stability and security improvements:

* internal/util/ — common RandomHex helper, removed 3 duplicates
* ESL: deduplicated readMessage (locked/unlocked), net.JoinHostPort for IPv6
* AMI: synchronous reconnect() in readEventsLoop, net.JoinHostPort for IPv6
* Auth: /api/auth/refresh accepts Authorization header only (no ?token=)
* decodeJSON: http.MaxBytesReader(1<<20) body limit
* Trunks: gatewayParams() uses configured ESL.GatewayPrefix
* Config: jwt_secret_env env-var fallback
* FSCollector: time.After → time.NewTimer with defer Stop
* Monitoring: Prometheus counters (route_requests, nodes_total/healthy, uptime)
* go fmt pass across all internal/ packages
2026-06-25 19:30:36 +07:00

138 lines
3.6 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()
// Ждём подключения с таймаутом
timer := time.NewTimer(100 * time.Millisecond)
defer timer.Stop()
for !fc.client.IsConnected() {
timer.Reset(100 * time.Millisecond)
select {
case <-ctx.Done():
return fmt.Errorf("esl connect timeout")
case <-timer.C:
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))
}