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
125 lines
3.9 KiB
Go
125 lines
3.9 KiB
Go
// Package agent реализует агент сбора метрик для pulse-lets-go.
|
||
// Агент работает на каждой PBX-ноде (FreeSWITCH или Asterisk),
|
||
// собирает метрики и публикует их в NATS каждые 5 секунд.
|
||
//
|
||
// Поддерживаемые типы PBX:
|
||
// - "freeswitch" — сбор через ESL (Event Socket Library)
|
||
// - "asterisk" — сбор через AMI (Asterisk Manager Interface)
|
||
//
|
||
// Пример agent.json:
|
||
//
|
||
// {
|
||
// "node_id": "uc06",
|
||
// "type": "asterisk",
|
||
// "nats_url": "nats://balancer.lan:4222",
|
||
// "sip_gateway": "sip:uc-pbx.lan:5060",
|
||
// "interval_sec": 5,
|
||
// "max_calls": 150,
|
||
// "ami": { "host": "127.0.0.1", "port": 6154, "username": "admin", "password": "changeme" }
|
||
// }
|
||
package agent
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
)
|
||
|
||
// Config — конфигурация агента (agent.json).
|
||
type Config struct {
|
||
NodeID string `json:"node_id"` // уникальный ID ноды (uc06, ses-sip)
|
||
Type string `json:"type"` // "freeswitch" или "asterisk"
|
||
NatsURL string `json:"nats_url"` // NATS URL
|
||
NatsUser string `json:"nats_user,omitempty"`
|
||
NatsPassword string `json:"nats_password,omitempty"`
|
||
IntervalSec int `json:"interval_sec"` // интервал сбора (default: 5)
|
||
MaxCalls int `json:"max_calls"` // ёмкость ноды (default: 250)
|
||
SIPGateway string `json:"sip_gateway"` // SIP-адрес для auto-транка
|
||
SIPGatewayAuto bool `json:"sip_gateway_auto"` // авто-определить из PBX
|
||
FailureWindow int `json:"failure_window"` // окно для call_failure_rate (default: 1000)
|
||
ESL ESLCfg `json:"esl,omitempty"`
|
||
AMI AMICfg `json:"ami,omitempty"`
|
||
}
|
||
|
||
// ESLCfg — настройки подключения к FreeSWITCH ESL.
|
||
type ESLCfg struct {
|
||
Host string `json:"host"`
|
||
Port int `json:"port"`
|
||
Password string `json:"password"`
|
||
}
|
||
|
||
// AMICfg — настройки подключения к Asterisk AMI.
|
||
type AMICfg struct {
|
||
Host string `json:"host"`
|
||
Port int `json:"port"`
|
||
Username string `json:"username"`
|
||
Password string `json:"password"`
|
||
}
|
||
|
||
// LoadConfig загружает конфигурацию агента из JSON-файла.
|
||
func LoadConfig(path string) (*Config, error) {
|
||
data, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("чтение %s: %w", path, err)
|
||
}
|
||
|
||
var cfg Config
|
||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||
return nil, fmt.Errorf("парсинг %s: %w", path, err)
|
||
}
|
||
|
||
// Defaults
|
||
if cfg.IntervalSec <= 0 {
|
||
cfg.IntervalSec = 5
|
||
}
|
||
if cfg.MaxCalls <= 0 {
|
||
cfg.MaxCalls = 250
|
||
}
|
||
if cfg.FailureWindow <= 0 {
|
||
cfg.FailureWindow = 1000
|
||
}
|
||
if cfg.ESL.Port == 0 {
|
||
cfg.ESL.Port = 8021
|
||
}
|
||
if cfg.AMI.Port == 0 {
|
||
cfg.AMI.Port = 5038
|
||
}
|
||
|
||
return &cfg, nil
|
||
}
|
||
|
||
// Validate проверяет корректность конфигурации.
|
||
func (c *Config) Validate() error {
|
||
if c.NodeID == "" {
|
||
return fmt.Errorf("node_id обязателен")
|
||
}
|
||
if c.NatsURL == "" {
|
||
return fmt.Errorf("nats_url обязателен")
|
||
}
|
||
switch c.Type {
|
||
case "freeswitch":
|
||
if c.ESL.Host == "" {
|
||
return fmt.Errorf("esl.host обязателен для type=freeswitch")
|
||
}
|
||
case "asterisk":
|
||
if c.AMI.Host == "" {
|
||
return fmt.Errorf("ami.host обязателен для type=asterisk")
|
||
}
|
||
default:
|
||
return fmt.Errorf("type должен быть freeswitch или asterisk, получен: %s", c.Type)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetSIPGateway возвращает SIP-адрес для авто-транка.
|
||
// Если SIPGatewayAuto = true — вернётся "" (collector определит сам).
|
||
func (c *Config) GetSIPGateway(autoDetected string) string {
|
||
if c.SIPGateway != "" {
|
||
return c.SIPGateway
|
||
}
|
||
if c.SIPGatewayAuto {
|
||
return autoDetected
|
||
}
|
||
return ""
|
||
}
|