Skip to content

Commit

Permalink
feat(api): Expose uptime data as text via API (#758)
Browse files Browse the repository at this point in the history
* Expose Raw Uptime Data via API

Signed-off-by: James Hillyard <[email protected]>

* Add Test for Raw Uptime Data API Endpoint

Signed-off-by: James Hillyard <[email protected]>

* Document Raw Uptime Data API Endpoint

Signed-off-by: James Hillyard <[email protected]>

* Fix Test after #759 Core Refactor

Signed-off-by: James Hillyard <[email protected]>

* Update Raw Data Content Type

Signed-off-by: James Hillyard <[email protected]>

* Support 30d Data from Raw Uptime Endpoint

Signed-off-by: James Hillyard <[email protected]>

* Update README.md

* Update README.md

---------

Signed-off-by: James Hillyard <[email protected]>
Co-authored-by: TwiN <[email protected]>
  • Loading branch information
JamesHillyard and TwiN authored Dec 28, 2024
1 parent e88f47f commit c44d998
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 0 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Response time](#response-time)
- [How to change the color thresholds of the response time badge](#how-to-change-the-color-thresholds-of-the-response-time-badge)
- [API](#api)
- [Raw Data](#raw-data)
- [Installing as binary](#installing-as-binary)
- [High level design overview](#high-level-design-overview)

Expand Down Expand Up @@ -2404,6 +2405,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 `30d` (alpha), `7d`, `24h` or `1h`
- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` 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:
Expand Down
1 change: 1 addition & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions api/raw.go
Original file line number Diff line number Diff line change
@@ -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 changes: 93 additions & 0 deletions api/raw_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit c44d998

Please sign in to comment.