health.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. package aerotun
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "sync"
  8. "time"
  9. "golang.org/x/sys/unix"
  10. )
  11. const healthHistoryLimit = 500
  12. type HealthEvent struct {
  13. Time time.Time `json:"time"`
  14. TunnelID string `json:"tunnel_id"`
  15. Name string `json:"name"`
  16. Type string `json:"type"`
  17. Status string `json:"status"`
  18. Previous string `json:"previous,omitempty"`
  19. PreviousDurationSeconds float64 `json:"previous_duration_seconds,omitempty"`
  20. Detail string `json:"detail,omitempty"`
  21. }
  22. type healthState struct {
  23. name, status string
  24. since time.Time
  25. }
  26. type healthMonitor struct {
  27. mu sync.Mutex
  28. checkMu sync.Mutex
  29. path string
  30. events []HealthEvent
  31. states map[string]healthState
  32. done, stopped chan struct{}
  33. once sync.Once
  34. lastCPUSeconds float64
  35. lastCPUAt time.Time
  36. processCPU *float64
  37. }
  38. func newHealthMonitor(path string) *healthMonitor {
  39. h := &healthMonitor{path: path, states: make(map[string]healthState), done: make(chan struct{}), stopped: make(chan struct{})}
  40. if data, err := os.ReadFile(path); err == nil && len(data) <= 1024*1024 {
  41. _ = json.Unmarshal(data, &h.events)
  42. }
  43. if len(h.events) > healthHistoryLimit {
  44. h.events = h.events[len(h.events)-healthHistoryLimit:]
  45. }
  46. return h
  47. }
  48. func (h *healthMonitor) stop() { h.once.Do(func() { close(h.done) }); <-h.stopped }
  49. func tunnelHealthStatus(t TunnelInstance) string {
  50. if t.Disabled {
  51. return "disabled"
  52. }
  53. if !t.Running {
  54. if t.LastError != "" {
  55. return "error"
  56. }
  57. return "stopped"
  58. }
  59. if t.Connected {
  60. return "connected"
  61. }
  62. return "waiting"
  63. }
  64. func (h *healthMonitor) addLocked(e HealthEvent) {
  65. h.events = append(h.events, e)
  66. if len(h.events) > healthHistoryLimit {
  67. h.events = append([]HealthEvent(nil), h.events[len(h.events)-healthHistoryLimit:]...)
  68. }
  69. }
  70. func (h *healthMonitor) saveLocked() error {
  71. data, err := json.Marshal(h.events)
  72. if err != nil {
  73. return err
  74. }
  75. if err = os.MkdirAll(filepath.Dir(h.path), 0700); err != nil {
  76. return err
  77. }
  78. f, err := os.CreateTemp(filepath.Dir(h.path), ".aerotun-health-*")
  79. if err != nil {
  80. return err
  81. }
  82. defer os.Remove(f.Name())
  83. if err = f.Chmod(0600); err == nil {
  84. _, err = f.Write(data)
  85. }
  86. if err == nil {
  87. err = f.Sync()
  88. }
  89. if closeErr := f.Close(); err == nil {
  90. err = closeErr
  91. }
  92. if err != nil {
  93. return err
  94. }
  95. return os.Rename(f.Name(), h.path)
  96. }
  97. func (h *healthMonitor) observe(rows []TunnelInstance, now time.Time) {
  98. h.mu.Lock()
  99. defer h.mu.Unlock()
  100. var usage unix.Rusage
  101. if unix.Getrusage(unix.RUSAGE_SELF, &usage) == nil {
  102. seconds := float64(usage.Utime.Sec) + float64(usage.Utime.Usec)/1e6 + float64(usage.Stime.Sec) + float64(usage.Stime.Usec)/1e6
  103. elapsed := now.Sub(h.lastCPUAt).Seconds()
  104. if !h.lastCPUAt.IsZero() && elapsed > 0 && seconds >= h.lastCPUSeconds {
  105. v := (seconds - h.lastCPUSeconds) / elapsed * 100
  106. h.processCPU = &v
  107. }
  108. h.lastCPUAt = now
  109. h.lastCPUSeconds = seconds
  110. }
  111. seen := make(map[string]bool)
  112. changed := false
  113. for _, row := range rows {
  114. seen[row.ID] = true
  115. status := tunnelHealthStatus(row)
  116. old, ok := h.states[row.ID]
  117. if !ok || old.status != status {
  118. e := HealthEvent{Time: now, TunnelID: row.ID, Name: row.Name, Type: "state", Status: status, Detail: row.LastError}
  119. if ok {
  120. e.Previous = old.status
  121. e.PreviousDurationSeconds = now.Sub(old.since).Seconds()
  122. }
  123. h.addLocked(e)
  124. h.states[row.ID] = healthState{name: row.Name, status: status, since: now}
  125. changed = true
  126. } else if old.name != row.Name {
  127. old.name = row.Name
  128. h.states[row.ID] = old
  129. }
  130. }
  131. for id, old := range h.states {
  132. if !seen[id] {
  133. h.addLocked(HealthEvent{Time: now, TunnelID: id, Name: old.name, Type: "state", Status: "removed", Previous: old.status, PreviousDurationSeconds: now.Sub(old.since).Seconds()})
  134. delete(h.states, id)
  135. changed = true
  136. }
  137. }
  138. if changed {
  139. if err := h.saveLocked(); err != nil {
  140. fmt.Fprintf(os.Stderr, "AeroTUN health history: %v\n", err)
  141. }
  142. }
  143. }
  144. func (h *healthMonitor) history(id string) []HealthEvent {
  145. h.mu.Lock()
  146. defer h.mu.Unlock()
  147. out := make([]HealthEvent, 0, len(h.events))
  148. for i := len(h.events) - 1; i >= 0; i-- {
  149. if id == "" || h.events[i].TunnelID == id {
  150. out = append(out, h.events[i])
  151. }
  152. }
  153. return out
  154. }
  155. func (h *healthMonitor) status(id string) (string, time.Time) {
  156. h.mu.Lock()
  157. defer h.mu.Unlock()
  158. s := h.states[id]
  159. return s.status, s.since
  160. }
  161. func (m *Manager) monitorHealth() {
  162. defer close(m.health.stopped)
  163. tick := time.NewTicker(2 * time.Second)
  164. defer tick.Stop()
  165. for {
  166. m.health.observe(m.List(), time.Now())
  167. select {
  168. case <-m.health.done:
  169. return
  170. case <-tick.C:
  171. }
  172. }
  173. }
  174. func (m *Manager) HealthHistory(id string) []HealthEvent {
  175. if m.health == nil {
  176. return []HealthEvent{}
  177. }
  178. return m.health.history(id)
  179. }
  180. func (m *Manager) ProcessCPUPercent() *float64 {
  181. if m.health == nil {
  182. return nil
  183. }
  184. m.health.mu.Lock()
  185. defer m.health.mu.Unlock()
  186. if m.health.processCPU == nil {
  187. return nil
  188. }
  189. v := *m.health.processCPU
  190. return &v
  191. }
  192. type HealthSummary struct {
  193. TunnelID string `json:"tunnel_id"`
  194. Status string `json:"status"`
  195. Since time.Time `json:"since"`
  196. }
  197. func (m *Manager) HealthSummaries() []HealthSummary {
  198. rows := m.List()
  199. out := make([]HealthSummary, 0, len(rows))
  200. for _, t := range rows {
  201. status, since := tunnelHealthStatus(t), time.Time{}
  202. if m.health != nil {
  203. if recorded, at := m.health.status(t.ID); recorded == status {
  204. since = at
  205. }
  206. }
  207. out = append(out, HealthSummary{TunnelID: t.ID, Status: status, Since: since})
  208. }
  209. return out
  210. }