diff --git a/README.md b/README.md index 2bcc324bb..98ebb51fd 100644 --- a/README.md +++ b/README.md @@ -2320,6 +2320,23 @@ Gzip compression will be used if the `Accept-Encoding` HTTP header contains `gzi The API will return a JSON payload with the `Content-Type` response header set to `application/json`. No such header is required to query the API. +#### Raw Data +Gatus exposes the raw data for one of your monitored endpoints. +This allows you to track and aggregate data in your own applications for monitored endpoints. For instance if you want to track uptime for a period longer than 7 days. + +##### Uptime +The path to get raw uptime data for an endpoint is: +``` +/api/v1/endpoints/{key}/uptimes/{duration} +``` +Where: +- `{duration}` is `7d`, `24h` or `1h` +- `{key}` has the pattern `_` in which both variables have ` `, `/`, `_`, `,` and `.` replaced by `-`. + +For instance, if you want the raw uptime data for the last 24 hours from the endpoint `frontend` in the group `core`, the URL would look like this: +``` +https://example.com/api/v1/endpoints/core_frontend/uptimes/24h +``` ### Installing as binary You can download Gatus as a binary using the following command: diff --git a/api/api.go b/api/api.go index 4635708bc..7cac80361 100644 --- a/api/api.go +++ b/api/api.go @@ -73,6 +73,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) diff --git a/api/raw.go b/api/raw.go new file mode 100644 index 000000000..34fe67262 --- /dev/null +++ b/api/raw.go @@ -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))) +} diff --git a/api/raw_test.go b/api/raw_test.go new file mode 100644 index 000000000..d227c0aad --- /dev/null +++ b/api/raw_test.go @@ -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) + } + }) + } +}