uptime.go 7.1 KB

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