health.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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(time.Second)
  46. defer ticker.Stop()
  47. lastProbe := make(map[string]time.Time)
  48. for {
  49. select {
  50. case <-ctx.Done():
  51. return
  52. case <-ticker.C:
  53. groups := dueProbeGroups(getGroups(), lastProbe, time.Now())
  54. if len(groups) == 0 {
  55. continue
  56. }
  57. changed := h.runProbeIteration(groups)
  58. if ctx.Err() != nil {
  59. return
  60. }
  61. if h.applyResults != nil {
  62. changed = h.applyResults(groups)
  63. }
  64. if changed && h.onStateChange != nil {
  65. h.onStateChange()
  66. }
  67. }
  68. }
  69. }()
  70. }
  71. // dueProbeGroups respects the configured interval for each group. New groups
  72. // are probed on the next tick; removed or disabled groups lose their schedule.
  73. func dueProbeGroups(groups []*SDWANGroup, last map[string]time.Time, now time.Time) []*SDWANGroup {
  74. active := make(map[string]bool)
  75. var due []*SDWANGroup
  76. for _, group := range groups {
  77. if group == nil || group.Disabled {
  78. continue
  79. }
  80. active[group.ID] = true
  81. interval := group.ProbeInterval
  82. if interval <= 0 {
  83. interval = 3
  84. }
  85. if previous, ok := last[group.ID]; !ok || now.Sub(previous) >= time.Duration(interval)*time.Second {
  86. due = append(due, group)
  87. last[group.ID] = now
  88. }
  89. }
  90. for id := range last {
  91. if !active[id] {
  92. delete(last, id)
  93. }
  94. }
  95. return due
  96. }
  97. // Stop terminates the probing goroutine.
  98. func (h *HealthMonitor) Stop() {
  99. if h.cancel != nil {
  100. h.cancel()
  101. if h.done != nil {
  102. <-h.done
  103. }
  104. }
  105. }
  106. func (h *HealthMonitor) runProbeIteration(groups []*SDWANGroup) bool {
  107. var wg sync.WaitGroup
  108. slots := make(chan struct{}, 8)
  109. changes := make(chan bool, 1)
  110. for _, group := range groups {
  111. if group == nil || group.Disabled {
  112. continue
  113. }
  114. for _, member := range group.Members {
  115. if member == nil {
  116. continue
  117. }
  118. if member.Disabled || member.Interface == "" {
  119. member.Status = StatusDisabled
  120. continue
  121. }
  122. wg.Add(1)
  123. slots <- struct{}{}
  124. go func(m *SDWANMember) {
  125. defer wg.Done()
  126. defer func() { <-slots }()
  127. target := m.TargetProbe
  128. if target == "" {
  129. target = "1.1.1.1"
  130. }
  131. probe := h.executeProbe
  132. if h.probe != nil {
  133. probe = h.probe
  134. }
  135. latency, success := probe(m.Interface, target)
  136. h.mu.Lock()
  137. sampleKey := m.ID + "\x00" + m.Interface + "\x00" + target
  138. history := append(h.samples[sampleKey], probeSample{latency: latency, success: success, t: time.Now()})
  139. if len(history) > 10 {
  140. history = history[len(history)-10:]
  141. }
  142. h.samples[sampleKey] = history
  143. history = append([]probeSample(nil), history...)
  144. h.mu.Unlock()
  145. previous := m.Status
  146. m.LastChecked = time.Now()
  147. if success {
  148. m.ConsecutiveSuccesses++
  149. m.ConsecutiveFails = 0
  150. } else {
  151. m.ConsecutiveFails++
  152. m.ConsecutiveSuccesses = 0
  153. }
  154. h.calculateMetrics(m, history)
  155. if m.ConsecutiveFails >= 5 {
  156. m.Status = StatusDown
  157. } else if previous == StatusDown && m.ConsecutiveSuccesses < 2 {
  158. m.Status = StatusDown
  159. } else {
  160. maxLat := float64(m.MaxLatency)
  161. if maxLat <= 0 {
  162. maxLat = 250
  163. }
  164. maxLoss := float64(m.MaxLoss)
  165. if maxLoss <= 0 {
  166. maxLoss = 20
  167. }
  168. if m.PacketLoss > maxLoss || m.LiveLatency > maxLat {
  169. m.Status = StatusDegraded
  170. } else {
  171. m.Status = StatusHealthy
  172. }
  173. }
  174. if (previous == StatusDown) != (m.Status == StatusDown) {
  175. select {
  176. case changes <- true:
  177. default:
  178. }
  179. 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)
  180. }
  181. }(member)
  182. }
  183. }
  184. wg.Wait()
  185. select {
  186. case <-changes:
  187. return true
  188. default:
  189. return false
  190. }
  191. }
  192. func (h *HealthMonitor) executeProbe(iface, target string) (float64, bool) {
  193. // Sanitize probe target: fallback if empty, ending in .0, or known rate-limited anycast target
  194. cleanTarget := strings.TrimSpace(target)
  195. if cleanTarget == "" || cleanTarget == "188.114.97.0" || strings.HasSuffix(cleanTarget, ".0") {
  196. cleanTarget = "1.1.1.1"
  197. }
  198. // ping -c 2 -W 2 -i 0.2 -I <iface> <cleanTarget>
  199. cmd := exec.Command("ping", "-c", "2", "-W", "2", "-i", "0.2", "-I", iface, cleanTarget)
  200. out, err := cmd.CombinedOutput()
  201. if err != nil {
  202. return 0, false
  203. }
  204. str := string(out)
  205. if strings.Contains(str, "100% packet loss") || strings.Contains(str, " 0 received") || strings.Contains(str, "0 packets received") {
  206. return 0, false
  207. }
  208. matches := rttRegex.FindStringSubmatch(str)
  209. if len(matches) > 1 {
  210. if val, err := strconv.ParseFloat(matches[1], 64); err == nil {
  211. return val, true
  212. }
  213. }
  214. return 1.0, true
  215. }
  216. func (h *HealthMonitor) calculateMetrics(m *SDWANMember, history []probeSample) {
  217. if len(history) == 0 {
  218. return
  219. }
  220. total := len(history)
  221. failed := 0
  222. var sumLat float64
  223. var countSuccess int
  224. var latencies []float64
  225. for _, s := range history {
  226. if !s.success {
  227. failed++
  228. } else {
  229. sumLat += s.latency
  230. countSuccess++
  231. latencies = append(latencies, s.latency)
  232. }
  233. }
  234. m.PacketLoss = (float64(failed) / float64(total)) * 100.0
  235. if countSuccess > 0 {
  236. avg := sumLat / float64(countSuccess)
  237. m.LiveLatency = math.Round(avg*10) / 10
  238. // Jitter calculation (average difference between consecutive probes)
  239. if len(latencies) >= 2 {
  240. var diffSum float64
  241. for i := 1; i < len(latencies); i++ {
  242. diffSum += math.Abs(latencies[i] - latencies[i-1])
  243. }
  244. m.Jitter = math.Round((diffSum/float64(len(latencies)-1))*10) / 10
  245. }
  246. } else {
  247. m.LiveLatency = 0
  248. m.Jitter = 0
  249. }
  250. }