Skip to content

Commit

Permalink
Add apiserver
Browse files Browse the repository at this point in the history
  • Loading branch information
Chrisbattarbee committed Mar 27, 2024
1 parent 2a7118f commit 428b540
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 11 deletions.
37 changes: 37 additions & 0 deletions apiserver/internal/server/caches.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package server

import (
"context"
"github.com/patrickmn/go-cache"
"go.uber.org/zap"
"time"
)

func (s *Server) StartCaches(ctx context.Context) {
go s.updateStatusPageCache(ctx)
}

const statusPageCacheRefreshInterval = 5 * time.Minute

func (s *Server) updateStatusPageCache(ctx context.Context) {
ticker := time.NewTicker(statusPageCacheRefreshInterval)
s.updateStatusPageCacheInner(ctx)
for {
select {
case <-ticker.C:
s.updateStatusPageCacheInner(ctx)
}
}
}

func (s *Server) updateStatusPageCacheInner(ctx context.Context) {
statusPages, err := s.dbClient.GetAllStatusPages(ctx)
if err != nil {
s.logger.Error("failed to get status pages", zap.Error(err))
return
}

for _, statusPage := range statusPages {
s.statusPageCache.Set(statusPage.URL, statusPage, cache.DefaultExpiration)
}
}
103 changes: 103 additions & 0 deletions apiserver/internal/server/current_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package server

import (
"context"
"github.com/gin-gonic/gin"
"github.com/metoro-io/statusphere/common/api"
"github.com/patrickmn/go-cache"
"github.com/pkg/errors"
"go.uber.org/zap"
"net/http"
)

type CurrentStatusResponse struct {
IsOkay bool `json:"isOkay"`
}

// currentStatus is a handler for the /current-status endpoint.
// It has a required query parameter of statusPageUrl
func (s *Server) currentStatus(context *gin.Context) {
ctx := context.Request.Context()
statusPageUrl := context.Query("statusPageUrl")
if statusPageUrl == "" {
context.JSON(http.StatusBadRequest, gin.H{"error": "statusPageUrl is required"})
return
}

// Attempt to get the incidents from the cache
incidents, found, err := s.getCurrentIncidentsFromCache(ctx, statusPageUrl)
if err != nil {
s.logger.Error("failed to get incidents from cache", zap.Error(err))
context.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get incidents from cache"})
return
}
if found {
if len(incidents) > 0 {
context.JSON(http.StatusOK, CurrentStatusResponse{IsOkay: false})
return
}
context.JSON(http.StatusOK, CurrentStatusResponse{IsOkay: true})
return
}

// Attempt to get the incidents from the database
incidents, found, err = s.getCurrentIncidentsFromDatabase(ctx, statusPageUrl)
if err != nil {
s.logger.Error("failed to get incidents from database", zap.Error(err))
context.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get incidents from database"})
return
}
if !found {
context.JSON(http.StatusNotFound, gin.H{"error": "status page not indexed"})
return
}

s.currentIncidentCache.Set(statusPageUrl, incidents, cache.DefaultExpiration)
if len(incidents) > 0 {
context.JSON(http.StatusOK, CurrentStatusResponse{IsOkay: false})
return
}
context.JSON(http.StatusOK, CurrentStatusResponse{IsOkay: true})
}

// getCurrentIncidentsFromCache attempts to get the current incidents from the cache.
// If the incidents are found in the cache, it returns them.
// If the incidents are not found in the cache, it returns false for the second return value.

func (s *Server) getCurrentIncidentsFromCache(ctx context.Context, statusPageUrl string) ([]api.Incident, bool, error) {
incidents, found := s.currentIncidentCache.Get(statusPageUrl)
if !found {
return nil, false, nil
}

incidentsCasted, ok := incidents.([]api.Incident)
if !ok {
return nil, false, errors.New("failed to cast incidents to []api.Incident")
}

return incidentsCasted, true, nil
}

// getCurrentIncidentsFromDatabase attempts to get the current incidents from the database.
// If the incidents are found in the database, it returns them.
// If the incidents are not found in the database, it returns false for the second return value.
func (s *Server) getCurrentIncidentsFromDatabase(ctx context.Context, statusPageUrl string) ([]api.Incident, bool, error) {
incidents, err := s.dbClient.GetCurrentIncidents(ctx, statusPageUrl)
if err != nil {
return nil, false, err
}

if len(incidents) == 0 {
// See if the status page exists
statusPage, err := s.dbClient.GetStatusPage(ctx, statusPageUrl)
if err != nil {
return nil, false, err
}
if statusPage == nil {
// The status page does not exist
return nil, false, nil
}
}

return incidents, true, nil
}
94 changes: 94 additions & 0 deletions apiserver/internal/server/incidents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package server

import (
"context"
"github.com/gin-gonic/gin"
"github.com/metoro-io/statusphere/common/api"
"github.com/patrickmn/go-cache"
"github.com/pkg/errors"
"go.uber.org/zap"
"net/http"
)

type IncidentsResponse struct {
Incidents []api.Incident `json:"incidents"`
}

// incidents is a handler for the /incidents endpoint.
// It has a required query parameter of statusPageUrl
func (s *Server) incidents(context *gin.Context) {
ctx := context.Request.Context()
statusPageUrl := context.Query("statusPageUrl")
if statusPageUrl == "" {
context.JSON(http.StatusBadRequest, gin.H{"error": "statusPageUrl is required"})
return
}

// Attempt to get the incidents from the cache
incidents, found, err := s.getIncidentsFromCache(ctx, statusPageUrl)
if err != nil {
s.logger.Error("failed to get incidents from cache", zap.Error(err))
context.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get incidents from cache"})
return
}
if found {
context.JSON(http.StatusOK, IncidentsResponse{Incidents: incidents})
return
}

// Attempt to get the incidents from the database
incidents, found, err = s.getIncidentsFromDatabase(ctx, statusPageUrl)
if err != nil {
s.logger.Error("failed to get incidents from database", zap.Error(err))
context.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get incidents from database"})
return
}
if !found {
context.JSON(http.StatusNotFound, gin.H{"error": "status page not indexed"})
return
}

s.incidentCache.Set(statusPageUrl, incidents, cache.DefaultExpiration)
context.JSON(http.StatusOK, IncidentsResponse{Incidents: incidents})
}

// getIncidentsFromCache attempts to get the incidents from the cache.
// If the incidents are found in the cache, it returns them.
// If the incidents are not found in the cache, it returns false for the second return value.
func (s *Server) getIncidentsFromCache(ctx context.Context, statusPageUrl string) ([]api.Incident, bool, error) {
incidents, found := s.incidentCache.Get(statusPageUrl)
if !found {
return nil, false, nil
}

incidentsCasted, ok := incidents.([]api.Incident)
if !ok {
return nil, false, errors.New("failed to cast incidents to []api.Incident")
}

return incidentsCasted, true, nil
}

// getIncidentsFromDatabase attempts to get the incidents from the database.
// If the incidents are found in the database, it returns them.
// If the incidents are not found in the database, it returns false for the second return value.
func (s *Server) getIncidentsFromDatabase(ctx context.Context, statusPageUrl string) ([]api.Incident, bool, error) {
incidents, err := s.dbClient.GetIncidents(ctx, statusPageUrl)
if err != nil {
return nil, false, err
}

if len(incidents) == 0 {
// See if the status page exists
statusPage, err := s.dbClient.GetStatusPage(ctx, statusPageUrl)
if err != nil {
return nil, false, err
}
if statusPage == nil {
// The status page does not exist
return nil, false, nil
}
}

return incidents, true, nil
}
18 changes: 14 additions & 4 deletions apiserver/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,27 @@ import (
"github.com/gin-gonic/gin"
"github.com/metoro-io/statusphere/common/db"
"github.com/metoro-io/statusphere/common/utils"
"github.com/patrickmn/go-cache"
"github.com/pkg/errors"
"go.uber.org/zap"
"time"
)

type Server struct {
logger *zap.Logger
dbClient *db.DbClient
logger *zap.Logger
dbClient *db.DbClient
statusPageCache *cache.Cache
incidentCache *cache.Cache
currentIncidentCache *cache.Cache
}

func NewServer(logger *zap.Logger, dbClient *db.DbClient) *Server {
return &Server{
logger: logger,
dbClient: dbClient,
logger: logger,
dbClient: dbClient,
statusPageCache: cache.New(15*time.Minute, 15*time.Minute),
incidentCache: cache.New(1*time.Minute, 1*time.Minute),
currentIncidentCache: cache.New(1*time.Minute, 1*time.Minute),
}
}

Expand All @@ -37,6 +44,9 @@ func (s *Server) Serve() error {
apiV1 := r.Group("/api/v1")
{
apiV1.Use(addNoIndexHeader())
apiV1.GET("/statusPages", s.statusPages)
apiV1.GET("/incidents", s.incidents)
apiV1.GET("/currentStatus", s.currentStatus)
}

s.addFrontendRoutes(r)
Expand Down
20 changes: 20 additions & 0 deletions apiserver/internal/server/status_pages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package server

import (
"github.com/gin-gonic/gin"
"github.com/metoro-io/statusphere/common/api"
"net/http"
)

type StatusPageResponse struct {
StatusPages []api.StatusPage `json:"statusPages"`
}

func (s *Server) statusPages(context *gin.Context) {
var statusPages []api.StatusPage
for _, statusPage := range s.statusPageCache.Items() {
statusPages = append(statusPages, statusPage.Object.(api.StatusPage))
}

context.JSON(http.StatusOK, StatusPageResponse{StatusPages: statusPages})
}
1 change: 1 addition & 0 deletions apiserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func main() {
}

s := server.NewServer(logger, dbClient)
s.StartCaches(ctx)

go func() {
if err := s.Serve(); err != nil && !errors.Is(err, http.ErrServerClosed) {
Expand Down
2 changes: 1 addition & 1 deletion common/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type Incident struct {
Components []string `gorm:"column:components;type:jsonb"`
Events IncidentEventArray `gorm:"column:events;type:jsonb"`
StartTime time.Time `gorm:"secondarykey"`
EndTime *time.Time
EndTime *time.Time `gorm:"secondarykey"`
Description *string
DeepLink string `gorm:"primarykey"`
Impact Impact `gorm:"secondarykey"`
Expand Down
12 changes: 12 additions & 0 deletions common/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ func (d *DbClient) GetStatusPage(ctx context.Context, url string) (*api.StatusPa
var statusPage api.StatusPage
result := d.db.Table(fmt.Sprintf(fmt.Sprintf("%s.%s", schemaName, statusPageTableName))).Where("url = ?", url).First(&statusPage)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, result.Error
}
return &statusPage, nil
Expand Down Expand Up @@ -160,6 +163,15 @@ func (d *DbClient) GetIncidents(ctx context.Context, statusPageUrl string) ([]ap
return incidents, nil
}

func (d *DbClient) GetCurrentIncidents(ctx context.Context, statusPageUrl string) ([]api.Incident, error) {
var incidents []api.Incident
result := d.db.Table(fmt.Sprintf("%s.%s", schemaName, incidentsTableName)).Where("status_page_url = ? AND end_time IS NULL", statusPageUrl).Find(&incidents)
if result.Error != nil {
return nil, result.Error
}
return incidents, nil
}

func (d *DbClient) CreateOrUpdateIncidents(ctx context.Context, incidents []api.Incident) error {
result := d.db.Table(fmt.Sprintf("%s.%s", schemaName, incidentsTableName)).Clauses(
clause.OnConflict{
Expand Down
14 changes: 8 additions & 6 deletions scraper/internal/scraper/providers/statusio/statusio.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,13 @@ func (s *StatusioProvider) parseIncidents(url string, html string) ([]api.Incide

// Example transformation, customize as needed
incident := api.Incident{
Title: inc.Name,
Description: &inc.Message,
StartTime: startTime,
EndTime: endTime,
Impact: api.Impact(inc.Impact),
DeepLink: link,
Title: inc.Name,
Description: &inc.Message,
StartTime: startTime,
EndTime: endTime,
Impact: api.Impact(inc.Impact),
DeepLink: link,
StatusPageUrl: url,
}
incidents = append(incidents, incident)
}
Expand Down Expand Up @@ -220,6 +221,7 @@ func (s *StatusioProvider) parseCurrentIncidents(url string, html string) ([]api
deepLink := selection.Find(".incident-title a").First().AttrOr("href", "")
deepLink = url + deepLink
incident.DeepLink = deepLink
incident.StatusPageUrl = url
var minTime *time.Time = nil

selection.Find(".update").Each(func(i int, sel *goquery.Selection) {
Expand Down

0 comments on commit 428b540

Please sign in to comment.