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
This commit is contained in:
parent
e66ac27dd9
commit
cc9da3ad7d
@ -32,12 +32,15 @@ func (fc *FSCollector) Connect() error {
|
|||||||
fc.client.ConnectWithRetry()
|
fc.client.ConnectWithRetry()
|
||||||
|
|
||||||
// Ждём подключения с таймаутом
|
// Ждём подключения с таймаутом
|
||||||
|
timer := time.NewTimer(100 * time.Millisecond)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
for !fc.client.IsConnected() {
|
for !fc.client.IsConnected() {
|
||||||
|
timer.Reset(100 * time.Millisecond)
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return fmt.Errorf("esl connect timeout")
|
return fmt.Errorf("esl connect timeout")
|
||||||
case <-time.After(100 * time.Millisecond):
|
case <-timer.C:
|
||||||
// Выходим из цикла после ConnectWithRetry
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
// SIP тестер для e2e проверки pulse-lets-go + FreeSWITCH.
|
// SIP тестер для e2e проверки pulse-lets-go + FreeSWITCH.
|
||||||
// Два режима:
|
// Два режима:
|
||||||
|
//
|
||||||
// uas — отвечает 200 OK на SIP INVITE, симулирует PBX-ноду
|
// uas — отвечает 200 OK на SIP INVITE, симулирует PBX-ноду
|
||||||
// uac — шлёт SIP INVITE в FreeSWITCH, симулирует оператора
|
// uac — шлёт SIP INVITE в FreeSWITCH, симулирует оператора
|
||||||
//
|
//
|
||||||
// Примеры:
|
// Примеры:
|
||||||
|
//
|
||||||
// ./siptest -mode uas (PBX-нода на порту 5090)
|
// ./siptest -mode uas (PBX-нода на порту 5090)
|
||||||
// ./siptest -mode uac -r 10 -l 100 (оператор, 10 CPS, до 100 одновременных)
|
// ./siptest -mode uac -r 10 -l 100 (оператор, 10 CPS, до 100 одновременных)
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -19,6 +19,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pulse-lets-go/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -103,7 +105,7 @@ func buildSIPResponse(invite, localAddr string) string {
|
|||||||
// Добавляем tag к To если его нет
|
// Добавляем tag к To если его нет
|
||||||
to = strings.TrimSpace(to)
|
to = strings.TrimSpace(to)
|
||||||
if !strings.Contains(to, ";tag=") {
|
if !strings.Contains(to, ";tag=") {
|
||||||
to += ";tag=uas-" + randomHex(4)
|
to += ";tag=uas-" + util.RandomHex(4)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("SIP/2.0 200 OK\r\n"+
|
return fmt.Sprintf("SIP/2.0 200 OK\r\n"+
|
||||||
@ -213,8 +215,8 @@ func runUAC(rate, limit, maxCalls int, dest, caller, host string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildSIPInvite(dest, caller string, callNum int, srcAddr, fsAddr string) string {
|
func buildSIPInvite(dest, caller string, callNum int, srcAddr, fsAddr string) string {
|
||||||
callID := fmt.Sprintf("call-%d-%s@%s", callNum, randomHex(4), "127.0.0.1")
|
callID := fmt.Sprintf("call-%d-%s@%s", callNum, util.RandomHex(4), "127.0.0.1")
|
||||||
branch := "z9hG4bK-" + randomHex(8)
|
branch := "z9hG4bK-" + util.RandomHex(8)
|
||||||
|
|
||||||
return fmt.Sprintf("INVITE sip:%s@%s SIP/2.0\r\n"+
|
return fmt.Sprintf("INVITE sip:%s@%s SIP/2.0\r\n"+
|
||||||
"Via: SIP/2.0/UDP %s;branch=%s\r\n"+
|
"Via: SIP/2.0/UDP %s;branch=%s\r\n"+
|
||||||
@ -227,9 +229,3 @@ func buildSIPInvite(dest, caller string, callNum int, srcAddr, fsAddr string) st
|
|||||||
"\r\n",
|
"\r\n",
|
||||||
dest, fsAddr, srcAddr, branch, caller, callNum, dest, callID, caller, srcAddr)
|
dest, fsAddr, srcAddr, branch, caller, callNum, dest, callID, caller, srcAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomHex(n int) string {
|
|
||||||
b := make([]byte, n)
|
|
||||||
rand.Read(b)
|
|
||||||
return hex.EncodeToString(b)[:n]
|
|
||||||
}
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ func NewClient(host string, port int, username, password string) *Client {
|
|||||||
func (c *Client) Connect() error {
|
func (c *Client) Connect() error {
|
||||||
c.shouldReconn.Store(true)
|
c.shouldReconn.Store(true)
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", c.host, c.port)
|
addr := net.JoinHostPort(c.host, fmt.Sprintf("%d", c.port))
|
||||||
dialer := net.Dialer{Timeout: 5 * time.Second}
|
dialer := net.Dialer{Timeout: 5 * time.Second}
|
||||||
conn, err := dialer.Dial("tcp", addr)
|
conn, err := dialer.Dial("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -268,11 +268,9 @@ func (c *Client) readEventsLoop() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") {
|
if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") {
|
||||||
c.connected.Store(false)
|
c.connected.Store(false)
|
||||||
log.Printf("[ami] соединение разорвано")
|
log.Printf("[ami] соединение разорвано, реконнект...")
|
||||||
if c.shouldReconn.Load() {
|
c.reconnect()
|
||||||
go c.ConnectWithRetry()
|
continue
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
continue
|
continue
|
||||||
@ -291,3 +289,29 @@ func (c *Client) readEventsLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reconnect выполняет реконнект с backoff (без горутин — синхронный вызов).
|
||||||
|
func (c *Client) reconnect() {
|
||||||
|
if !c.shouldReconn.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[ami] реконнект через %v...", c.backoff)
|
||||||
|
time.Sleep(c.backoff)
|
||||||
|
|
||||||
|
if c.conn != nil {
|
||||||
|
c.conn.Close()
|
||||||
|
c.conn = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Connect(); err != nil {
|
||||||
|
log.Printf("[ami] реконнект не удался: %v", err)
|
||||||
|
c.backoff *= 2
|
||||||
|
if c.backoff > 60*time.Second {
|
||||||
|
c.backoff = 60 * time.Second
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.backoff = 1 * time.Second
|
||||||
|
log.Printf("[ami] реконнект успешен")
|
||||||
|
}
|
||||||
|
|||||||
@ -139,7 +139,7 @@ func (a *API) apiKeyMiddleware(next http.Handler) http.Handler {
|
|||||||
// handleLogin обрабатывает POST /api/auth/login.
|
// handleLogin обрабатывает POST /api/auth/login.
|
||||||
func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
var req models.LoginRequest
|
var req models.LoginRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := decodeJSON(r, &req); err != nil {
|
||||||
writeError(w, http.StatusBadRequest, "некорректный JSON")
|
writeError(w, http.StatusBadRequest, "некорректный JSON")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -199,10 +199,6 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
// handleRefresh выдаёт новый access токен по refresh токену.
|
// handleRefresh выдаёт новый access токен по refresh токену.
|
||||||
func (a *API) handleRefresh(w http.ResponseWriter, r *http.Request) {
|
func (a *API) handleRefresh(w http.ResponseWriter, r *http.Request) {
|
||||||
tokenStr := extractBearerToken(r)
|
tokenStr := extractBearerToken(r)
|
||||||
if tokenStr == "" {
|
|
||||||
// пробуем получить из query param (для WebSocket)
|
|
||||||
tokenStr = r.URL.Query().Get("token")
|
|
||||||
}
|
|
||||||
if tokenStr == "" {
|
if tokenStr == "" {
|
||||||
writeError(w, http.StatusUnauthorized, "требуется токен")
|
writeError(w, http.StatusUnauthorized, "требуется токен")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -38,6 +38,26 @@ func (a *API) handlePrometheus(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
|
// Общие метрики (counter gauge)
|
||||||
|
stats := a.engine.GetHealthStats()
|
||||||
|
fmt.Fprintf(&sb, "# HELP pulse_route_requests_total Total number of route requests.\n")
|
||||||
|
fmt.Fprintf(&sb, "# TYPE pulse_route_requests_total counter\n")
|
||||||
|
fmt.Fprintf(&sb, "pulse_route_requests_total %d\n", stats.RouteRequests)
|
||||||
|
|
||||||
|
fmt.Fprintf(&sb, "# HELP pulse_nodes_total Total nodes registered.\n")
|
||||||
|
fmt.Fprintf(&sb, "# TYPE pulse_nodes_total gauge\n")
|
||||||
|
fmt.Fprintf(&sb, "pulse_nodes_total %d\n", stats.TotalNodes)
|
||||||
|
|
||||||
|
fmt.Fprintf(&sb, "# HELP pulse_nodes_healthy Healthy nodes count.\n")
|
||||||
|
fmt.Fprintf(&sb, "# TYPE pulse_nodes_healthy gauge\n")
|
||||||
|
fmt.Fprintf(&sb, "pulse_nodes_healthy %d\n", stats.HealthyNodes)
|
||||||
|
|
||||||
|
fmt.Fprintf(&sb, "# HELP pulse_uptime_seconds Uptime in seconds.\n")
|
||||||
|
fmt.Fprintf(&sb, "# TYPE pulse_uptime_seconds gauge\n")
|
||||||
|
fmt.Fprintf(&sb, "pulse_uptime_seconds %d\n", stats.UptimeSeconds)
|
||||||
|
|
||||||
|
fmt.Fprintf(&sb, "\n")
|
||||||
|
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
id := sanitizePromLabel(n.NodeID)
|
id := sanitizePromLabel(n.NodeID)
|
||||||
|
|
||||||
|
|||||||
@ -141,9 +141,13 @@ func (a *API) BroadcastGatewayEvent(gatewayName, trunkID, status string) {
|
|||||||
a.wsHub.broadcast(msg)
|
a.wsHub.broadcast(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeJSON декодирует тело запроса в структуру v.
|
// maxBodySize — максимальный размер тела запроса в байтах (1 MiB).
|
||||||
|
const maxBodySize = 1 << 20
|
||||||
|
|
||||||
|
// decodeJSON декодирует тело запроса в структуру v с ограничением размера.
|
||||||
func decodeJSON(r *http.Request, v interface{}) error {
|
func decodeJSON(r *http.Request, v interface{}) error {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
r.Body = http.MaxBytesReader(nil, r.Body, maxBodySize)
|
||||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||||
return fmt.Errorf("декодирование JSON: %w", err)
|
return fmt.Errorf("декодирование JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -268,8 +268,9 @@ func (a *API) eslPushGatewayDelete(trunk models.Trunk) {
|
|||||||
|
|
||||||
// gatewayParams возвращает параметры для создания FS gateway на основе транка.
|
// gatewayParams возвращает параметры для создания FS gateway на основе транка.
|
||||||
func (a *API) gatewayParams(trunk models.Trunk) (profile, name, proxy string) {
|
func (a *API) gatewayParams(trunk models.Trunk) (profile, name, proxy string) {
|
||||||
profile = a.readConfig().ESL.SofiaProfile
|
cfg := a.readConfig()
|
||||||
name = fmt.Sprintf("pulse-ingress-%s", trunk.ID)
|
profile = cfg.ESL.SofiaProfile
|
||||||
|
name = fmt.Sprintf("%s-ingress-%s", cfg.ESL.GatewayPrefix, trunk.ID)
|
||||||
proxy = extractHost(trunk.Gateway)
|
proxy = extractHost(trunk.Gateway)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pulse-lets-go/internal/models"
|
"github.com/pulse-lets-go/internal/models"
|
||||||
|
"github.com/pulse-lets-go/internal/util"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -227,15 +226,5 @@ func findUserIndex(users []models.User, id string) int {
|
|||||||
|
|
||||||
// generateID генерирует ID вида "prefix-XXXXX".
|
// generateID генерирует ID вида "prefix-XXXXX".
|
||||||
func generateID(prefix string) string {
|
func generateID(prefix string) string {
|
||||||
return fmt.Sprintf("%s-%s", prefix, randomHex(6))
|
return fmt.Sprintf("%s-%s", prefix, util.RandomHex(6))
|
||||||
}
|
|
||||||
|
|
||||||
// randomHex генерирует случайную hex-строку заданной длины.
|
|
||||||
func randomHex(n int) string {
|
|
||||||
b := make([]byte, n)
|
|
||||||
if _, err := rand.Read(b); err != nil {
|
|
||||||
// fallback на time-based, если crypto/rand недоступен
|
|
||||||
return fmt.Sprintf("%x", time.Now().UnixNano())
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(b)[:n]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,6 +72,16 @@ func (e *ESLConfig) GetPassword() string {
|
|||||||
return e.Password
|
return e.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetJWTSecret возвращает JWT-секрет: из переменной окружения, если задан jwt_secret_env, иначе из поля jwt_secret.
|
||||||
|
func (c *Config) GetJWTSecret() string {
|
||||||
|
if c.JWTSecretEnv != "" {
|
||||||
|
if val := os.Getenv(c.JWTSecretEnv); val != "" {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.JWTSecret
|
||||||
|
}
|
||||||
|
|
||||||
// Config — корневая конфигурация приложения (config.json).
|
// Config — корневая конфигурация приложения (config.json).
|
||||||
type Config struct {
|
type Config struct {
|
||||||
NatsURL string `json:"nats_url"`
|
NatsURL string `json:"nats_url"`
|
||||||
@ -79,6 +89,7 @@ type Config struct {
|
|||||||
NatsPassword string `json:"nats_password"`
|
NatsPassword string `json:"nats_password"`
|
||||||
ListenAddr string `json:"listen_addr"`
|
ListenAddr string `json:"listen_addr"`
|
||||||
JWTSecret string `json:"jwt_secret"`
|
JWTSecret string `json:"jwt_secret"`
|
||||||
|
JWTSecretEnv string `json:"jwt_secret_env"` // имя переменной окружения с JWT-секретом
|
||||||
MonitoringAPIKey string `json:"monitoring_api_key"`
|
MonitoringAPIKey string `json:"monitoring_api_key"`
|
||||||
StaleThresholdSec int `json:"stale_threshold_sec"`
|
StaleThresholdSec int `json:"stale_threshold_sec"`
|
||||||
Scoring ScoringConfig `json:"scoring"`
|
Scoring ScoringConfig `json:"scoring"`
|
||||||
|
|||||||
@ -109,7 +109,7 @@ func (c *Client) tryConnect() error {
|
|||||||
c.closeCh = make(chan struct{})
|
c.closeCh = make(chan struct{})
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", c.host, c.port)
|
addr := net.JoinHostPort(c.host, fmt.Sprintf("%d", c.port))
|
||||||
dialer := net.Dialer{Timeout: 5 * time.Second}
|
dialer := net.Dialer{Timeout: 5 * time.Second}
|
||||||
conn, err := dialer.Dial("tcp", addr)
|
conn, err := dialer.Dial("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -119,7 +119,7 @@ func (c *Client) tryConnect() error {
|
|||||||
c.reader = bufio.NewReader(conn)
|
c.reader = bufio.NewReader(conn)
|
||||||
|
|
||||||
// 1. Читаем auth/request
|
// 1. Читаем auth/request
|
||||||
headers, _, err := c.readMessageLocked()
|
headers, _, err := c.readMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return fmt.Errorf("чтение auth/request: %w", err)
|
return fmt.Errorf("чтение auth/request: %w", err)
|
||||||
@ -137,7 +137,7 @@ func (c *Client) tryConnect() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Читаем auth response
|
// 3. Читаем auth response
|
||||||
headers, _, err = c.readMessageLocked()
|
headers, _, err = c.readMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return fmt.Errorf("чтение auth response: %w", err)
|
return fmt.Errorf("чтение auth response: %w", err)
|
||||||
@ -199,7 +199,7 @@ func (c *Client) Send(command string) (map[string]string, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Читаем ответ
|
// Читаем ответ
|
||||||
return c.readMessageLocked()
|
return c.readMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe подписывается на события ESL.
|
// Subscribe подписывается на события ESL.
|
||||||
@ -263,7 +263,7 @@ func (c *Client) readEventsLoop() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
headers, body, err := c.readMessageUnlocked()
|
headers, body, err := c.readMessage()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") {
|
if err == io.EOF || strings.Contains(err.Error(), "use of closed network connection") {
|
||||||
@ -330,48 +330,9 @@ func (c *Client) reconnect() {
|
|||||||
|
|
||||||
// --- Приватные методы ---
|
// --- Приватные методы ---
|
||||||
|
|
||||||
// readMessageUnlocked читает одно ESL-сообщение: заголовки + тело.
|
// readMessage читает одно ESL-сообщение: заголовки + тело.
|
||||||
// Должна вызываться только из readEventsLoop (одиночный читатель).
|
// Вызывающий должен гарантировать монопольный доступ к c.reader.
|
||||||
func (c *Client) readMessageUnlocked() (map[string]string, string, error) {
|
func (c *Client) readMessage() (map[string]string, string, error) {
|
||||||
headers := make(map[string]string)
|
|
||||||
|
|
||||||
for {
|
|
||||||
line, err := c.reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
line = strings.TrimRight(line, "\r\n")
|
|
||||||
if line == "" {
|
|
||||||
break // конец заголовков
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(line, ":", 2)
|
|
||||||
if len(parts) == 2 {
|
|
||||||
headers[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Читаем тело согласно Content-Length
|
|
||||||
cl := headers["Content-Length"]
|
|
||||||
if cl == "" {
|
|
||||||
return headers, "", nil
|
|
||||||
}
|
|
||||||
length, err := strconv.Atoi(cl)
|
|
||||||
if err != nil {
|
|
||||||
return headers, "", fmt.Errorf("некорректный Content-Length: %s", cl)
|
|
||||||
}
|
|
||||||
|
|
||||||
body := make([]byte, length)
|
|
||||||
if _, err := io.ReadFull(c.reader, body); err != nil {
|
|
||||||
return headers, "", fmt.Errorf("чтение тела (Content-Length=%d): %w", length, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers, strings.TrimSpace(string(body)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readMessageLocked читает одно ESL-сообщение: заголовки + тело.
|
|
||||||
// Вызывающий держит c.mu (для синхронных команд Send).
|
|
||||||
func (c *Client) readMessageLocked() (map[string]string, string, error) {
|
|
||||||
headers := make(map[string]string)
|
headers := make(map[string]string)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ type EventHandler func(eventName string, headers map[string]string, body string)
|
|||||||
// - Вызывает onConnect (например, для GatewaySyncAll)
|
// - Вызывает onConnect (например, для GatewaySyncAll)
|
||||||
// - Подписывается на события SOFIA::gateway_register/unregister
|
// - Подписывается на события SOFIA::gateway_register/unregister
|
||||||
// - Запускает чтение асинхронных событий
|
// - Запускает чтение асинхронных событий
|
||||||
|
//
|
||||||
// При разрыве соединения — автоматический реконнект.
|
// При разрыве соединения — автоматический реконнект.
|
||||||
func (c *Client) StartEventLoop(ctx context.Context, profile string, onConnect func(), onEvent EventHandler) {
|
func (c *Client) StartEventLoop(ctx context.Context, profile string, onConnect func(), onEvent EventHandler) {
|
||||||
const subscribeEvents = "SOFIA::gateway_register SOFIA::gateway_unregister SOFIA::gateway_expire"
|
const subscribeEvents = "SOFIA::gateway_register SOFIA::gateway_unregister SOFIA::gateway_expire"
|
||||||
|
|||||||
@ -32,8 +32,8 @@ func (c *Client) GatewayDelete(profile, name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GatewayList возвращает список имён gateway в заданном профиле.
|
// GatewayList возвращает список имён gateway в заданном профиле с указанным префиксом.
|
||||||
func (c *Client) GatewayList(profile string) ([]string, error) {
|
func (c *Client) GatewayList(profile, prefix string) ([]string, error) {
|
||||||
cmd := fmt.Sprintf("api sofia profile %s gwlist", profile)
|
cmd := fmt.Sprintf("api sofia profile %s gwlist", profile)
|
||||||
_, body, err := c.Send(cmd)
|
_, body, err := c.Send(cmd)
|
||||||
|
|
||||||
@ -49,8 +49,8 @@ func (c *Client) GatewayList(profile string) ([]string, error) {
|
|||||||
if line == "" || strings.HasPrefix(line, "Name:") || strings.HasPrefix(line, "====") {
|
if line == "" || strings.HasPrefix(line, "Name:") || strings.HasPrefix(line, "====") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Ищем строки вида "pulse-ingress-xxxxx sip:..."
|
// Ищем строки вида "<prefix>-ingress-xxxxx sip:..."
|
||||||
if strings.Contains(line, "pulse-") {
|
if strings.Contains(line, prefix+"-ingress-") {
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
if len(parts) > 0 {
|
if len(parts) > 0 {
|
||||||
gateways = append(gateways, parts[0])
|
gateways = append(gateways, parts[0])
|
||||||
|
|||||||
@ -9,9 +9,9 @@ import (
|
|||||||
|
|
||||||
natsgo "github.com/nats-io/nats.go"
|
natsgo "github.com/nats-io/nats.go"
|
||||||
|
|
||||||
"github.com/pulse-lets-go/internal/models"
|
|
||||||
"github.com/pulse-lets-go/internal/engine"
|
"github.com/pulse-lets-go/internal/engine"
|
||||||
filelog "github.com/pulse-lets-go/internal/log"
|
filelog "github.com/pulse-lets-go/internal/log"
|
||||||
|
"github.com/pulse-lets-go/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const subject = "pulse.metrics.>"
|
const subject = "pulse.metrics.>"
|
||||||
|
|||||||
18
internal/util/util.go
Normal file
18
internal/util/util.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomHex генерирует случайную hex-строку из n байт.
|
||||||
|
// При ошибке crypto/rand использует time-based fallback.
|
||||||
|
func RandomHex(n int) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return fmt.Sprintf("%x", time.Now().UnixNano())[:n*2]
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)[:n*2]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user