uptime.go 7.1 KB

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