health.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package sdwan
  2. import (
  3. "context"
  4. "log"
  5. "math"
  6. "os/exec"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "time"
  12. )
  13. var (
  14. rttRegex = regexp.MustCompile(`time=([0-9.]+)\s*ms`)
  15. lossRegex = regexp.MustCompile(`([0-9.]+)%\s*packet\s*loss`)
  16. )
  17. type probeSample struct {
  18. latency float64
  19. success bool
  20. t time.Time
  21. }
  22. // HealthMonitor periodically probes SD-WAN member interfaces and updates SLA telemetry.
  23. type HealthMonitor struct {
  24. mu sync.RWMutex
  25. samples map[string][]probeSample // memberID -> rolling probe samples
  26. cancel context.CancelFunc
  27. done chan struct{}
  28. onStateChange func()
  29. applyResults func([]*SDWANGroup) bool
  30. probe func(string, string) (float64, bool)
  31. }
  32. func NewHealthMonitor(onStateChange func()) *HealthMonitor {
  33. return &HealthMonitor{
  34. samples: make(map[string][]probeSample),
  35. onStateChange: onStateChange,
  36. }
  37. }
  38. // Start begins periodic background health checking for active SD-WAN members.
  39. func (h *HealthMonitor) Start(getGroups func() []*SDWANGroup) {
  40. ctx, cancel := context.WithCancel(context.Background())
  41. h.cancel = cancel
  42. h.done = make(chan struct{})
  43. go func() {
  44. defer close(h.done)
  45. ticker := time.NewTicker(3 * time.Second)
  46. defer ticker.Stop()
  47. for {
  48. select {
  49. case <-ctx.Done():
  50. return
  51. case <-ticker.C:
  52. groups := getGroups()
  53. changed := h.runProbeIteration(groups)
  54. if ctx.Err() != nil {
  55. return
  56. }
  57. if h.applyResults != nil {
  58. changed = h.applyResults(groups)
  59. }
  60. if changed && h.onStateChange != nil {
  61. h.onStateChange()
  62. }
  63. }
  64. }
  65. }()
  66. }
  67. // Stop terminates the probing goroutine.
  68. func (h *HealthMonitor) Stop() {
  69. if h.cancel != nil {
  70. h.cancel()
  71. if h.done != nil {
  72. <-h.done
  73. }
  74. }
  75. }
  76. func (h *HealthMonitor) runProbeIteration(groups []*SDWANGroup) bool {
  77. var wg sync.WaitGroup
  78. slots := make(chan struct{}, 8)
  79. changes := make(chan bool, 1)
  80. for _, group := range groups {
  81. if group == nil || group.Disabled {
  82. continue
  83. }
  84. for _, member := range group.Members {
  85. if member == nil {
  86. continue
  87. }
  88. if member.Disabled || member.Interface == "" {
  89. member.Status = StatusDisabled
  90. continue
  91. }
  92. wg.Add(1)
  93. slots <- struct{}{}
  94. go func(m *SDWANMember) {
  95. defer wg.Done()
  96. defer func() { <-slots }()
  97. target := m.TargetProbe
  98. if target == "" {
  99. target = "1.1.1.1"
  100. }
  101. probe := h.executeProbe
  102. if h.probe != nil {
  103. probe = h.probe
  104. }
  105. latency, success := probe(m.Interface, target)
  106. h.mu.Lock()
  107. sampleKey := m.ID + "\x00" + m.Interface + "\x00" + target
  108. history := append(h.samples[sampleKey], probeSample{latency: latency, success: success, t: time.Now()})
  109. if len(history) > 10 {
  110. history = history[len(history)-10:]
  111. }
  112. h.samples[sampleKey] = history
  113. history = append([]probeSample(nil), history...)
  114. h.mu.Unlock()
  115. previous := m.Status
  116. m.LastChecked = time.Now()
  117. if success {
  118. m.ConsecutiveSuccesses++
  119. m.ConsecutiveFails = 0
  120. } else {
  121. m.ConsecutiveFails++
  122. m.ConsecutiveSuccesses = 0
  123. }
  124. h.calculateMetrics(m, history)
  125. if m.ConsecutiveFails >= 5 {
  126. m.Status = StatusDown
  127. } else if previous == StatusDown && m.ConsecutiveSuccesses < 2 {
  128. m.Status = StatusDown
  129. } else {
  130. maxLat := float64(m.MaxLatency)
  131. if maxLat <= 0 {
  132. maxLat = 250
  133. }
  134. maxLoss := float64(m.MaxLoss)
  135. if maxLoss <= 0 {
  136. maxLoss = 20
  137. }
  138. if m.PacketLoss > maxLoss || m.LiveLatency > maxLat {
  139. m.Status = StatusDegraded
  140. } else {
  141. m.Status = StatusHealthy
  142. }
  143. }
  144. if (previous == StatusDown) != (m.Status == StatusDown) {
  145. select {
  146. case changes <- true:
  147. default:
  148. }
  149. log.Printf("[SD-WAN] Health alert: %s status changed from %s to %s (latency: %.1fms, loss: %.1f%%)", m.Interface, previous, m.Status, m.LiveLatency, m.PacketLoss)
  150. }
  151. }(member)
  152. }
  153. }
  154. wg.Wait()
  155. select {
  156. case <-changes:
  157. return true
  158. default:
  159. return false
  160. }
  161. }
  162. func (h *HealthMonitor) executeProbe(iface, target string) (float64, bool) {
  163. // Sanitize probe target: fallback if empty, ending in .0, or known rate-limited anycast target
  164. cleanTarget := strings.TrimSpace(target)
  165. if cleanTarget == "" || cleanTarget == "188.114.97.0" || strings.HasSuffix(cleanTarget, ".0") {
  166. cleanTarget = "1.1.1.1"
  167. }
  168. // ping -c 2 -W 2 -i 0.2 -I <iface> <cleanTarget>
  169. cmd := exec.Command("ping", "-c", "2", "-W", "2", "-i", "0.2", "-I", iface, cleanTarget)
  170. out, err := cmd.CombinedOutput()
  171. if err != nil {
  172. return 0, false
  173. }
  174. str := string(out)
  175. if strings.Contains(str, "100% packet loss") || strings.Contains(str, " 0 received") || strings.Contains(str, "0 packets received") {
  176. return 0, false
  177. }
  178. matches := rttRegex.FindStringSubmatch(str)
  179. if len(matches) > 1 {
  180. if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
  181. return val, true
  182. }
  183. }
  184. return 1.0, true
  185. }
  186. func (h *HealthMonitor) calculateMetrics(m *SDWANMember, history []probeSample) {
  187. if len(history) == 0 {
  188. return
  189. }
  190. total := len(history)
  191. failed := 0
  192. var sumLat float64
  193. var countSuccess int
  194. var latencies []float64
  195. for _, s := range history {
  196. if !s.success {
  197. failed++
  198. } else {
  199. sumLat += s.latency
  200. countSuccess++
  201. latencies = append(latencies, s.latency)
  202. }
  203. }
  204. m.PacketLoss = (float64(failed) / float64(total)) * 100.0
  205. if countSuccess > 0 {
  206. avg := sumLat / float64(countSuccess)
  207. m.LiveLatency = math.Round(avg*10) / 10
  208. // Jitter calculation (average difference between consecutive probes)
  209. if len(latencies) >= 2 {
  210. var diffSum float64
  211. for i := 1; i < len(latencies); i++ {
  212. diffSum += math.Abs(latencies[i] - latencies[i-1])
  213. }
  214. m.Jitter = math.Round((diffSum/float64(len(latencies)-1))*10) / 10
  215. }
  216. } else {
  217. m.LiveLatency = 0
  218. m.Jitter = 0
  219. }
  220. }