feat(api): Add endpoint to retrieve response time (#1070)

Add in the API the ability to get Response Times

Co-authored-by: Adrian Almenar <adrian@tecnocratica.net>
This commit is contained in:
Adrian 2025-04-29 23:21:18 +02:00 committed by GitHub
parent 493c7165fe
commit fe214e9e25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 79 additions and 0 deletions

View File

@ -118,10 +118,12 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Health](#health) - [Health](#health)
- [Health (Shields.io)](#health-shieldsio) - [Health (Shields.io)](#health-shieldsio)
- [Response time](#response-time) - [Response time](#response-time)
- [Response time (chart)](#response-time-chart)
- [How to change the color thresholds of the response time badge](#how-to-change-the-color-thresholds-of-the-response-time-badge) - [How to change the color thresholds of the response time badge](#how-to-change-the-color-thresholds-of-the-response-time-badge)
- [API](#api) - [API](#api)
- [Raw Data](#raw-data) - [Raw Data](#raw-data)
- [Uptime](#uptime-1) - [Uptime](#uptime-1)
- [Response Time](#response-time-1)
- [Installing as binary](#installing-as-binary) - [Installing as binary](#installing-as-binary)
- [High level design overview](#high-level-design-overview) - [High level design overview](#high-level-design-overview)
@ -2511,6 +2513,20 @@ For instance, if you want the raw uptime data for the last 24 hours from the end
https://example.com/api/v1/endpoints/core_frontend/uptimes/24h https://example.com/api/v1/endpoints/core_frontend/uptimes/24h
``` ```
##### Response Time
The path to get raw response time data for an endpoint is:
```
/api/v1/endpoints/{key}/response-times/{duration}
```
Where:
- `{duration}` is `30d`, `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 response time 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/response-times/24h
```
### Installing as binary ### Installing as binary
You can download Gatus as a binary using the following command: You can download Gatus as a binary using the following command:
``` ```

View File

@ -80,6 +80,7 @@ func (a *API) createRouter(cfg *config.Config) *fiber.App {
unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.shields", HealthBadgeShields) unprotectedAPIRouter.Get("/v1/endpoints/:key/health/badge.shields", HealthBadgeShields)
unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration", UptimeRaw) unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration", UptimeRaw)
unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration/badge.svg", UptimeBadge) unprotectedAPIRouter.Get("/v1/endpoints/:key/uptimes/:duration/badge.svg", UptimeBadge)
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration", ResponseTimeRaw)
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/badge.svg", ResponseTimeBadge(cfg)) unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/badge.svg", ResponseTimeBadge(cfg))
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/chart.svg", ResponseTimeChart) unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/chart.svg", ResponseTimeChart)
// This endpoint requires authz with bearer token, so technically it is protected // This endpoint requires authz with bearer token, so technically it is protected

View File

@ -41,3 +41,35 @@ func UptimeRaw(c *fiber.Ctx) error {
c.Set("Expires", "0") c.Set("Expires", "0")
return c.Status(200).Send([]byte(fmt.Sprintf("%f", uptime))) return c.Status(200).Send([]byte(fmt.Sprintf("%f", uptime)))
} }
func ResponseTimeRaw(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")
responseTime, err := store.Get().GetAverageResponseTimeByKey(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("%d", responseTime)))
}

View File

@ -74,6 +74,36 @@ func TestRawDataEndpoint(t *testing.T) {
Path: "/api/v1/endpoints/invalid_key/uptimes/7d", Path: "/api/v1/endpoints/invalid_key/uptimes/7d",
ExpectedCode: http.StatusNotFound, ExpectedCode: http.StatusNotFound,
}, },
{
Name: "raw-response-times-1h",
Path: "/api/v1/endpoints/core_frontend/response-times/1h",
ExpectedCode: http.StatusOK,
},
{
Name: "raw-response-times-24h",
Path: "/api/v1/endpoints/core_backend/response-times/24h",
ExpectedCode: http.StatusOK,
},
{
Name: "raw-response-times-7d",
Path: "/api/v1/endpoints/core_frontend/response-times/7d",
ExpectedCode: http.StatusOK,
},
{
Name: "raw-response-times-30d",
Path: "/api/v1/endpoints/core_frontend/response-times/30d",
ExpectedCode: http.StatusOK,
},
{
Name: "raw-response-times-with-invalid-duration",
Path: "/api/v1/endpoints/core_backend/response-times/3d",
ExpectedCode: http.StatusBadRequest,
},
{
Name: "raw-response-times-for-invalid-key",
Path: "/api/v1/endpoints/invalid_key/response-times/7d",
ExpectedCode: http.StatusNotFound,
},
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { t.Run(scenario.Name, func(t *testing.T) {