feat(api): Expose uptime data as text via API (#758)

* Expose Raw Uptime Data via API

Signed-off-by: James Hillyard <james.hillyard@payara.fish>

* Add Test for Raw Uptime Data API Endpoint

Signed-off-by: James Hillyard <james.hillyard@payara.fish>

* Document Raw Uptime Data API Endpoint

Signed-off-by: James Hillyard <james.hillyard@payara.fish>

* Fix Test after #759 Core Refactor

Signed-off-by: James Hillyard <james.hillyard@payara.fish>

* Update Raw Data Content Type

Signed-off-by: James Hillyard <james.hillyard@payara.fish>

* Support 30d Data from Raw Uptime Endpoint

Signed-off-by: James Hillyard <james.hillyard@payara.fish>

* Update README.md

* Update README.md

---------

Signed-off-by: James Hillyard <james.hillyard@payara.fish>
Co-authored-by: TwiN <twin@linux.com>
This commit is contained in:
JamesHillyard
2024-12-28 20:59:28 +00:00
committed by GitHub
parent e88f47f0f4
commit c44d998fb3
4 changed files with 155 additions and 0 deletions

View File

@ -78,6 +78,7 @@ func (a *API) createRouter(cfg *config.Config) *fiber.App {
unprotectedAPIRouter.Get("/v1/config", ConfigHandler{securityConfig: cfg.Security}.GetConfig)
unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.svg", HealthBadge)
unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.shields", HealthBadgeShields)
unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration", UptimeRaw)
unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration/badge.svg", UptimeBadge)
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/badge.svg", ResponseTimeBadge(cfg))
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/chart.svg", ResponseTimeChart)

43
api/raw.go Normal file
View File

@ -0,0 +1,43 @@
package api
import (
"errors"
"fmt"
"time"
"github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/storage/store/common"
"github.com/gofiber/fiber/v2"
)
func UptimeRaw(c *fiber.Ctx) error {
duration := c.Params("duration")
var from time.Time
switch duration {
case "30d":
from = time.Now().Add(-30 * 24 * time.Hour)
case "7d":
from = time.Now().Add(-7 * 24 * time.Hour)
case "24h":
from = time.Now().Add(-24 * time.Hour)
case "1h":
from = time.Now().Add(-2 * time.Hour) // Because uptime metrics are stored by hour, we have to cheat a little
default:
return c.Status(400).SendString("Durations supported: 30d,7d, 24h, 1h")
}
key := c.Params("key")
uptime, err := store.Get().GetUptimeByKey(key, from, time.Now())
if err != nil {
if errors.Is(err, common.ErrEndpointNotFound) {
return c.Status(404).SendString(err.Error())
} else if errors.Is(err, common.ErrInvalidTimeRange) {
return c.Status(400).SendString(err.Error())
}
return c.Status(500).SendString(err.Error())
}
c.Set("Content-Type", "text/plain")
c.Set("Cache-Control", "no-cache, no-store, must-revalidate")
c.Set("Expires", "0")
return c.Status(200).Send([]byte(fmt.Sprintf("%f", uptime)))
}

93
api/raw_test.go Normal file
View File

@ -0,0 +1,93 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/TwiN/gatus/v5/config"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/gatus/v5/config/endpoint/ui"
"github.com/TwiN/gatus/v5/storage/store"
"github.com/TwiN/gatus/v5/watchdog"
)
func TestRawDataEndpoint(t *testing.T) {
defer store.Get().Clear()
defer cache.Clear()
cfg := &config.Config{
Metrics: true,
Endpoints: []*endpoint.Endpoint{
{
Name: "frontend",
Group: "core",
},
{
Name: "backend",
Group: "core",
},
},
}
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &endpoint.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &endpoint.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
api := New(cfg)
router := api.Router()
type Scenario struct {
Name string
Path string
ExpectedCode int
Gzip bool
}
scenarios := []Scenario{
{
Name: "raw-uptime-1h",
Path: "/api/v1/endpoints/core_frontend/uptimes/1h",
ExpectedCode: http.StatusOK,
},
{
Name: "raw-uptime-24h",
Path: "/api/v1/endpoints/core_backend/uptimes/24h",
ExpectedCode: http.StatusOK,
},
{
Name: "raw-uptime-7d",
Path: "/api/v1/endpoints/core_frontend/uptimes/7d",
ExpectedCode: http.StatusOK,
},
{
Name: "raw-uptime-30d",
Path: "/api/v1/endpoints/core_frontend/uptimes/30d",
ExpectedCode: http.StatusOK,
},
{
Name: "raw-uptime-with-invalid-duration",
Path: "/api/v1/endpoints/core_backend/uptimes/3d",
ExpectedCode: http.StatusBadRequest,
},
{
Name: "raw-uptime-for-invalid-key",
Path: "/api/v1/endpoints/invalid_key/uptimes/7d",
ExpectedCode: http.StatusNotFound,
},
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
request := httptest.NewRequest("GET", scenario.Path, http.NoBody)
if scenario.Gzip {
request.Header.Set("Accept-Encoding", "gzip")
}
response, err := router.Test(request)
if err != nil {
return
}
if response.StatusCode != scenario.ExpectedCode {
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode)
}
})
}
}