cloudflare_status_page.go 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. package origin
  2. import (
  3. "encoding/json"
  4. "io/ioutil"
  5. "net/http"
  6. "strings"
  7. "time"
  8. "github.com/cloudflare/golibs/lrucache"
  9. )
  10. // StatusPage.io API docs:
  11. // https://www.cloudflarestatus.com/api/v2/#incidents-unresolved
  12. const (
  13. activeIncidentsURL = "https://yh6f0r4529hb.statuspage.io/api/v2/incidents/unresolved.json"
  14. argoTunnelKeyword = "argo tunnel"
  15. incidentDetailsPrefix = "https://www.cloudflarestatus.com/incidents/"
  16. )
  17. // IncidentLookup is an object that checks for active incidents in
  18. // the Cloudflare infrastructure.
  19. type IncidentLookup interface {
  20. ActiveIncidents() []Incident
  21. }
  22. // NewIncidentLookup returns a new IncidentLookup instance that caches its
  23. // results with a 1-minute TTL.
  24. func NewIncidentLookup() IncidentLookup {
  25. return newCachedIncidentLookup(fetchActiveIncidents)
  26. }
  27. type IncidentUpdate struct {
  28. Body string
  29. }
  30. type Incident struct {
  31. Name string
  32. ID string `json:"id"`
  33. Updates []IncidentUpdate `json:"incident_updates"`
  34. }
  35. type StatusPage struct {
  36. Incidents []Incident
  37. }
  38. func (i Incident) URL() string {
  39. return incidentDetailsPrefix + i.ID
  40. }
  41. func parseStatusPage(data []byte) (*StatusPage, error) {
  42. var result StatusPage
  43. err := json.Unmarshal(data, &result)
  44. return &result, err
  45. }
  46. func isArgoTunnelIncident(i Incident) bool {
  47. if strings.Contains(strings.ToLower(i.Name), argoTunnelKeyword) {
  48. return true
  49. }
  50. for _, u := range i.Updates {
  51. if strings.Contains(strings.ToLower(u.Body), argoTunnelKeyword) {
  52. return true
  53. }
  54. }
  55. return false
  56. }
  57. func fetchActiveIncidents() (incidents []Incident) {
  58. resp, err := http.Get(activeIncidentsURL)
  59. if err != nil {
  60. return
  61. }
  62. defer resp.Body.Close()
  63. body, err := ioutil.ReadAll(resp.Body)
  64. if err != nil {
  65. return
  66. }
  67. statusPage, err := parseStatusPage(body)
  68. if err != nil {
  69. return
  70. }
  71. for _, i := range statusPage.Incidents {
  72. if isArgoTunnelIncident(i) {
  73. incidents = append(incidents, i)
  74. }
  75. }
  76. return incidents
  77. }
  78. type cachedIncidentLookup struct {
  79. cache *lrucache.LRUCache
  80. ttl time.Duration
  81. uncachedLookup func() []Incident
  82. }
  83. func newCachedIncidentLookup(uncachedLookup func() []Incident) *cachedIncidentLookup {
  84. return &cachedIncidentLookup{
  85. cache: lrucache.NewLRUCache(1),
  86. ttl: time.Minute,
  87. uncachedLookup: uncachedLookup,
  88. }
  89. }
  90. // We only need one cache entry. Always use the empty string as its key.
  91. const cacheKey = ""
  92. func (c *cachedIncidentLookup) ActiveIncidents() []Incident {
  93. if cached, ok := c.cache.GetNotStale(cacheKey); ok {
  94. if incidents, ok := cached.([]Incident); ok {
  95. return incidents
  96. }
  97. }
  98. incidents := c.uncachedLookup()
  99. c.cache.Set(cacheKey, incidents, time.Now().Add(c.ttl))
  100. return incidents
  101. }