// Эмулятор метрик для pulse-lets-go. // Публикует метрики от эмулируемых нод в NATS, имитируя разные сценарии нагрузки. // // Сценарии: // normal — все ноды здоровы, случайная загрузка (проверка weighted score + best_node) // overload — последняя нода перегружена (active_calls >= max_calls → lethal) // cpu-low — последняя нода с низким idle_cpu (< 5 → lethal) // fail-high — последняя нода с высоким call_failure_rate (> 15 → lethal) // node-down — последняя нода status="down" (→ lethal) // stale — первая нода перестаёт отправлять метрики через 10 сек (→ lethal) // chaos — случайное чередование lethal-состояний у случайных нод package main import ( "encoding/json" "flag" "fmt" "log" "math/rand" "os" "os/signal" "sync" "syscall" "time" natsgo "github.com/nats-io/nats.go" ) // --- Модель метрики --- type nodeMetric struct { NodeID string `json:"node_id"` TS int64 `json:"ts"` Status string `json:"status"` ActiveCalls int `json:"active_calls"` MaxCalls int `json:"max_calls"` IdleCPU float64 `json:"idle_cpu"` LoadAvg float64 `json:"load_avg"` CallFailureRate float64 `json:"call_failure_rate"` SIPGateway string `json:"sip_gateway,omitempty"` } // --- Спецификация значения (фиксированное или случайный диапазон) --- type valueSpec struct { fixed float64 min float64 max float64 isRand bool } func fixed(v float64) valueSpec { return valueSpec{fixed: v} } func rrange(min, max float64) valueSpec { return valueSpec{min: min, max: max, isRand: true} } func (v valueSpec) get() float64 { if v.isRand { return v.min + rand.Float64()*(v.max-v.min) } return v.fixed } // --- Конфигурация ноды в эмуляторе --- type nodeSpec struct { nodeID string status valueSpec // строка: ok / down calls valueSpec maxCalls float64 // фиксированное idleCPU valueSpec loadAvg valueSpec failRate valueSpec sipGateway string // SIP-адрес для авто-создания транка } func (ns *nodeSpec) generate() nodeMetric { return nodeMetric{ NodeID: ns.nodeID, TS: time.Now().Unix(), Status: statusFromValue(ns.status.get()), ActiveCalls: int(ns.calls.get()), MaxCalls: int(ns.maxCalls), IdleCPU: ns.idleCPU.get(), LoadAvg: ns.loadAvg.get(), CallFailureRate: ns.failRate.get(), SIPGateway: ns.sipGateway, } } func statusFromValue(v float64) string { if v < 0.5 { return "down" } return "ok" } // --- Сценарии --- func scenarioNormal() []nodeSpec { return []nodeSpec{ { nodeID: "pbx-01", status: fixed(1), calls: rrange(10, 100), maxCalls: 250, idleCPU: rrange(40, 95), loadAvg: rrange(0.1, 1.0), failRate: rrange(0, 2), sipGateway: "sip:pbx-01.lan:5060", }, { nodeID: "pbx-02", status: fixed(1), calls: rrange(50, 200), maxCalls: 250, idleCPU: rrange(15, 45), loadAvg: rrange(0.5, 2.0), failRate: rrange(1, 8), sipGateway: "sip:pbx-02.lan:5060", }, { nodeID: "pbx-03", status: fixed(1), calls: rrange(5, 60), maxCalls: 250, idleCPU: rrange(60, 90), loadAvg: rrange(0.05, 0.5), failRate: rrange(0, 1), sipGateway: "sip:pbx-03.lan:5060", }, } } func scenarioOverload() []nodeSpec { specs := scenarioNormal() // Последняя нода перегружена: active_calls = max_calls → lethal specs[len(specs)-1].calls = fixed(250) specs[len(specs)-1].idleCPU = fixed(10) specs[len(specs)-1].loadAvg = fixed(5.0) return specs } func scenarioCPULow() []nodeSpec { specs := scenarioNormal() specs[len(specs)-1].idleCPU = fixed(3) // < 5 → lethal return specs } func scenarioFailHigh() []nodeSpec { specs := scenarioNormal() specs[len(specs)-1].failRate = fixed(20) // > 15 → lethal return specs } func scenarioNodeDown() []nodeSpec { specs := scenarioNormal() specs[len(specs)-1].status = fixed(0) // status=down → lethal return specs } func scenarioStale() []nodeSpec { // Identical to normal — stale управляется логикой в main (пропуск отправки) return scenarioNormal() } // --- Main --- func main() { scenario := flag.String("scenario", "normal", "Сценарий: normal | overload | cpu-low | fail-high | node-down | stale | chaos") natsURL := flag.String("nats", "nats://localhost:4222", "NATS URL") interval := flag.Duration("interval", 5*time.Second, "Интервал отправки метрик") flag.Parse() nc, err := natsgo.Connect(*natsURL, natsgo.MaxReconnects(-1)) if err != nil { log.Fatalf("ошибка NATS: %v", err) } defer nc.Close() var specs []nodeSpec switch *scenario { case "normal": specs = scenarioNormal() case "overload": specs = scenarioOverload() case "cpu-low": specs = scenarioCPULow() case "fail-high": specs = scenarioFailHigh() case "node-down": specs = scenarioNodeDown() case "stale": specs = scenarioStale() case "chaos": specs = scenarioChaos() default: log.Fatalf("неизвестный сценарий: %s", *scenario) } log.Printf("[emulator] сценарий=%s, нод=%d, NATS=%s, интервал=%s", *scenario, len(specs), *natsURL, *interval) if *scenario == "chaos" { runChaos(nc, specs, *interval) return } if *scenario == "stale" { runStale(nc, specs, *interval) return } runNormal(nc, specs, *interval) } // runNormal — обычный цикл: все ноды шлют метрики каждые interval. func runNormal(nc *natsgo.Conn, specs []nodeSpec, interval time.Duration) { log.Printf("[emulator] запущен, Ctrl+C для остановки") ticker := time.NewTicker(interval) defer ticker.Stop() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // Первая отправка сразу publishAll(nc, specs) for { select { case <-ticker.C: publishAll(nc, specs) case <-quit: log.Println("[emulator] остановлен") return } } } // runStale — первая нода шлёт метрики только первые 10 секунд, потом замолкает. func runStale(nc *natsgo.Conn, specs []nodeSpec, interval time.Duration) { log.Printf("[emulator] сценарий stale: нода %s замолкнет через 10 сек", specs[0].nodeID) ticker := time.NewTicker(interval) defer ticker.Stop() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) startTime := time.Now() staleNodeID := specs[0].nodeID publishAll(nc, specs) for { select { case <-ticker.C: elapsed := time.Since(startTime) for _, spec := range specs { if spec.nodeID == staleNodeID && elapsed > 10*time.Second { continue // пропускаем stale-ноду } metric := spec.generate() publish(nc, metric) } case <-quit: log.Println("[emulator] остановлен") return } } } // runChaos — случайно чередует lethal-состояния у случайных нод. func runChaos(nc *natsgo.Conn, specs []nodeSpec, interval time.Duration) { log.Printf("[emulator] сценарий chaos: случайные lethal-состояния") ticker := time.NewTicker(interval) defer ticker.Stop() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // Сохраняем оригинальные спецификации для восстановления original := make([]nodeSpec, len(specs)) copy(original, specs) var mu sync.Mutex publishAll(nc, specs) for { select { case <-ticker.C: mu.Lock() // Каждые 15-30 секунд случайно портим случайную ноду if rand.Intn(4) == 0 { // ~25% шанс на каждом тике idx := rand.Intn(len(specs)) kind := rand.Intn(5) switch kind { case 0: specs[idx].status = fixed(0) // down case 1: specs[idx].calls = fixed(250) // overload case 2: specs[idx].idleCPU = fixed(2) // idle_cpu < 5 case 3: specs[idx].failRate = fixed(25) // failure rate > 15 case 4: // Восстановить оригинал specs[idx] = original[idx] } } publishAll(nc, specs) mu.Unlock() case <-quit: log.Println("[emulator] остановлен") return } } } func scenarioChaos() []nodeSpec { // Начинаем с normal return scenarioNormal() } // --- Helpers --- func publishAll(nc *natsgo.Conn, specs []nodeSpec) { for _, spec := range specs { metric := spec.generate() publish(nc, metric) } } func publish(nc *natsgo.Conn, m nodeMetric) { subject := fmt.Sprintf("pulse.metrics.%s", m.NodeID) data, err := json.Marshal(m) if err != nil { log.Printf("[emulator] ошибка сериализации %s: %v", m.NodeID, err) return } if err := nc.Publish(subject, data); err != nil { log.Printf("[emulator] ошибка публикации %s: %v", m.NodeID, err) return } log.Printf("[emulator] → %s: calls=%d/%d idle=%.0f%% load=%.2f fail=%.1f%% status=%s gw=%s", m.NodeID, m.ActiveCalls, m.MaxCalls, m.IdleCPU, m.LoadAvg, m.CallFailureRate, m.Status, m.SIPGateway) }