uptime.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. package uptime
  2. import (
  3. "encoding/json"
  4. "log"
  5. "net/http"
  6. "strings"
  7. "time"
  8. "github.com/go-ping/ping"
  9. "imuslab.com/zoraxy/mod/utils"
  10. )
  11. type Record struct {
  12. Timestamp int64
  13. ID string
  14. Name string
  15. URL string
  16. Protocol string
  17. Online bool
  18. StatusCode int
  19. Latency int64
  20. }
  21. type Target struct {
  22. ID string
  23. Name string
  24. URL string
  25. Protocol string
  26. }
  27. type Config struct {
  28. Targets []*Target
  29. Interval int
  30. MaxRecordsStore int
  31. }
  32. type Monitor struct {
  33. Config *Config
  34. OnlineStatusLog map[string][]*Record
  35. }
  36. // Default configs
  37. var exampleTarget = Target{
  38. ID: "example",
  39. Name: "Example",
  40. URL: "example.com",
  41. Protocol: "https",
  42. }
  43. // Create a new uptime monitor
  44. func NewUptimeMonitor(config *Config) (*Monitor, error) {
  45. //Create new monitor object
  46. thisMonitor := Monitor{
  47. Config: config,
  48. OnlineStatusLog: map[string][]*Record{},
  49. }
  50. //Start the endpoint listener
  51. ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
  52. done := make(chan bool)
  53. //Start the uptime check once first before entering loop
  54. thisMonitor.ExecuteUptimeCheck()
  55. go func() {
  56. for {
  57. select {
  58. case <-done:
  59. return
  60. case t := <-ticker.C:
  61. log.Println("Uptime updated - ", t.Unix())
  62. thisMonitor.ExecuteUptimeCheck()
  63. }
  64. }
  65. }()
  66. return &thisMonitor, nil
  67. }
  68. func (m *Monitor) ExecuteUptimeCheck() {
  69. for _, target := range m.Config.Targets {
  70. //For each target to check online, do the following
  71. var thisRecord Record
  72. if target.Protocol == "http" || target.Protocol == "https" {
  73. online, laterncy, statusCode := getWebsiteStatusWithLatency(target.URL)
  74. if !online {
  75. //Try ICMP ping as well
  76. pinger, err := ping.NewPinger(target.URL)
  77. if err != nil {
  78. log.Println("Unable to start uptime check pinger: " + err.Error())
  79. } else {
  80. pinger.Count = 2
  81. err = pinger.Run()
  82. if err != nil {
  83. //Unable to ping it. Assume offline
  84. }
  85. }
  86. }
  87. thisRecord = Record{
  88. Timestamp: time.Now().Unix(),
  89. ID: target.ID,
  90. Name: target.Name,
  91. URL: target.URL,
  92. Protocol: target.Protocol,
  93. Online: online,
  94. StatusCode: statusCode,
  95. Latency: laterncy,
  96. }
  97. //fmt.Println(thisRecord)
  98. } else {
  99. log.Println("Unknown protocol: " + target.Protocol + ". Skipping")
  100. continue
  101. }
  102. thisRecords, ok := m.OnlineStatusLog[target.ID]
  103. if !ok {
  104. //First record. Create the array
  105. m.OnlineStatusLog[target.ID] = []*Record{&thisRecord}
  106. } else {
  107. //Append to the previous record
  108. thisRecords = append(thisRecords, &thisRecord)
  109. //Check if the record is longer than the logged record. If yes, clear out the old records
  110. if len(thisRecords) > m.Config.MaxRecordsStore {
  111. thisRecords = thisRecords[1:]
  112. }
  113. m.OnlineStatusLog[target.ID] = thisRecords
  114. }
  115. }
  116. //TODO: Write results to db
  117. }
  118. func (m *Monitor) AddTargetToMonitor(target *Target) {
  119. // Add target to Config
  120. m.Config.Targets = append(m.Config.Targets, target)
  121. // Add target to OnlineStatusLog
  122. m.OnlineStatusLog[target.ID] = []*Record{}
  123. }
  124. func (m *Monitor) RemoveTargetFromMonitor(targetId string) {
  125. // Remove target from Config
  126. for i, target := range m.Config.Targets {
  127. if target.ID == targetId {
  128. m.Config.Targets = append(m.Config.Targets[:i], m.Config.Targets[i+1:]...)
  129. break
  130. }
  131. }
  132. // Remove target from OnlineStatusLog
  133. delete(m.OnlineStatusLog, targetId)
  134. }
  135. // Scan the config target. If a target exists in m.OnlineStatusLog no longer
  136. // exists in m.Monitor.Config.Targets, it remove it from the log as well.
  137. func (m *Monitor) CleanRecords() {
  138. // Create a set of IDs for all targets in the config
  139. targetIDs := make(map[string]bool)
  140. for _, target := range m.Config.Targets {
  141. targetIDs[target.ID] = true
  142. }
  143. // Iterate over all log entries and remove any that have a target ID that
  144. // is not in the set of current target IDs
  145. newStatusLog := m.OnlineStatusLog
  146. for id, _ := range m.OnlineStatusLog {
  147. _, idExistsInTargets := targetIDs[id]
  148. if !idExistsInTargets {
  149. delete(newStatusLog, id)
  150. }
  151. }
  152. m.OnlineStatusLog = newStatusLog
  153. }
  154. /*
  155. Web Interface Handler
  156. */
  157. func (m *Monitor) HandleUptimeLogRead(w http.ResponseWriter, r *http.Request) {
  158. id, _ := utils.GetPara(r, "id")
  159. if id == "" {
  160. js, _ := json.Marshal(m.OnlineStatusLog)
  161. w.Header().Set("Content-Type", "application/json")
  162. w.Write(js)
  163. } else {
  164. //Check if that id exists
  165. log, ok := m.OnlineStatusLog[id]
  166. if !ok {
  167. http.NotFound(w, r)
  168. return
  169. }
  170. js, _ := json.MarshalIndent(log, "", " ")
  171. w.Header().Set("Content-Type", "application/json")
  172. w.Write(js)
  173. }
  174. }
  175. /*
  176. Utilities
  177. */
  178. // Get website stauts with latency given URL, return is conn succ and its latency and status code
  179. func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
  180. start := time.Now().UnixNano() / int64(time.Millisecond)
  181. statusCode, err := getWebsiteStatus(url)
  182. end := time.Now().UnixNano() / int64(time.Millisecond)
  183. if err != nil {
  184. log.Println(err.Error())
  185. return false, 0, 0
  186. } else {
  187. diff := end - start
  188. succ := false
  189. if statusCode >= 200 && statusCode < 300 {
  190. //OK
  191. succ = true
  192. } else if statusCode >= 300 && statusCode < 400 {
  193. //Redirection code
  194. succ = true
  195. } else {
  196. succ = false
  197. }
  198. return succ, diff, statusCode
  199. }
  200. }
  201. func getWebsiteStatus(url string) (int, error) {
  202. resp, err := http.Get(url)
  203. if err != nil {
  204. //Try replace the http with https and vise versa
  205. rewriteURL := ""
  206. if strings.Contains(url, "https://") {
  207. rewriteURL = strings.ReplaceAll(url, "https://", "http://")
  208. } else if strings.Contains(url, "http://") {
  209. rewriteURL = strings.ReplaceAll(url, "http://", "https://")
  210. }
  211. resp, err = http.Get(rewriteURL)
  212. if err != nil {
  213. if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
  214. //Invalid downstream reverse proxy settings, but it is online
  215. //return SSL handshake failed
  216. return 525, nil
  217. }
  218. return 0, err
  219. }
  220. }
  221. status_code := resp.StatusCode
  222. resp.Body.Close()
  223. return status_code, nil
  224. }