From fe214e9e251ddbcbc70e7b3e07837b7a9510bc59 Mon Sep 17 00:00:00 2001 From: Adrian <161029+aalmenar@users.noreply.github.com> Date: Tue, 29 Apr 2025 23:21:18 +0200 Subject: [PATCH] feat(api): Add endpoint to retrieve response time (#1070) Add in the API the ability to get Response Times Co-authored-by: Adrian Almenar --- README.md | 16 ++++++++++++++++ api/api.go | 1 + api/raw.go | 32 ++++++++++++++++++++++++++++++++ api/raw_test.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+) diff --git a/README.md b/README.md index b6d79b15..282e6e2f 100644 --- a/README.md +++ b/README.md @@ -118,10 +118,12 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga - [Health](#health) - [Health (Shields.io)](#health-shieldsio) - [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) - [API](#api) - [Raw Data](#raw-data) - [Uptime](#uptime-1) + - [Response Time](#response-time-1) - [Installing as binary](#installing-as-binary) - [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 ``` +##### 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 `_` 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 You can download Gatus as a binary using the following command: ``` diff --git a/api/api.go b/api/api.go index 9ecc56f4..b918af2a 100644 --- a/api/api.go +++ b/api/api.go @@ -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/uptimes/:duration", UptimeRaw) 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/chart.svg", ResponseTimeChart) // This endpoint requires authz with bearer token, so technically it is protected diff --git a/api/raw.go b/api/raw.go index de6738ba..f6cd2b41 100644 --- a/api/raw.go +++ b/api/raw.go @@ -41,3 +41,35 @@ func UptimeRaw(c *fiber.Ctx) error { c.Set("Expires", "0") 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))) +} diff --git a/api/raw_test.go b/api/raw_test.go index d227c0aa..ac62c450 100644 --- a/api/raw_test.go +++ b/api/raw_test.go @@ -74,6 +74,36 @@ func TestRawDataEndpoint(t *testing.T) { Path: "/api/v1/endpoints/invalid_key/uptimes/7d", 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 { t.Run(scenario.Name, func(t *testing.T) {