Skip to content

Commit e2f06e9

Browse files
authored
fix(ui): Modernize response time chart (#1373)
1 parent beb9a2f commit e2f06e9

File tree

13 files changed

+656
-536
lines changed

13 files changed

+656
-536
lines changed

api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func (a *API) createRouter(cfg *config.Config) *fiber.App {
8484
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration", ResponseTimeRaw)
8585
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/badge.svg", ResponseTimeBadge(cfg))
8686
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/chart.svg", ResponseTimeChart)
87+
unprotectedAPIRouter.Get("/v1/endpoints/:key/response-times/:duration/history", ResponseTimeHistory)
8788
// This endpoint requires authz with bearer token, so technically it is protected
8889
unprotectedAPIRouter.Post("/v1/endpoints/:key/external", CreateExternalEndpointResult(cfg))
8990
// SPA

api/chart.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,63 @@ func ResponseTimeChart(c *fiber.Ctx) error {
126126
}
127127
return nil
128128
}
129+
130+
func ResponseTimeHistory(c *fiber.Ctx) error {
131+
duration := c.Params("duration")
132+
var from time.Time
133+
switch duration {
134+
case "30d":
135+
from = time.Now().Truncate(time.Hour).Add(-30 * 24 * time.Hour)
136+
case "7d":
137+
from = time.Now().Truncate(time.Hour).Add(-7 * 24 * time.Hour)
138+
case "24h":
139+
from = time.Now().Truncate(time.Hour).Add(-24 * time.Hour)
140+
default:
141+
return c.Status(400).SendString("Durations supported: 30d, 7d, 24h")
142+
}
143+
endpointKey, err := url.QueryUnescape(c.Params("key"))
144+
if err != nil {
145+
return c.Status(400).SendString("invalid key encoding")
146+
}
147+
hourlyAverageResponseTime, err := store.Get().GetHourlyAverageResponseTimeByKey(endpointKey, from, time.Now())
148+
if err != nil {
149+
if errors.Is(err, common.ErrEndpointNotFound) {
150+
return c.Status(404).SendString(err.Error())
151+
}
152+
if errors.Is(err, common.ErrInvalidTimeRange) {
153+
return c.Status(400).SendString(err.Error())
154+
}
155+
return c.Status(500).SendString(err.Error())
156+
}
157+
if len(hourlyAverageResponseTime) == 0 {
158+
return c.Status(200).JSON(map[string]interface{}{
159+
"timestamps": []int64{},
160+
"values": []int{},
161+
})
162+
}
163+
hourlyTimestamps := make([]int, 0, len(hourlyAverageResponseTime))
164+
earliestTimestamp := int64(0)
165+
for hourlyTimestamp := range hourlyAverageResponseTime {
166+
hourlyTimestamps = append(hourlyTimestamps, int(hourlyTimestamp))
167+
if earliestTimestamp == 0 || hourlyTimestamp < earliestTimestamp {
168+
earliestTimestamp = hourlyTimestamp
169+
}
170+
}
171+
for earliestTimestamp > from.Unix() {
172+
earliestTimestamp -= int64(time.Hour.Seconds())
173+
hourlyTimestamps = append(hourlyTimestamps, int(earliestTimestamp))
174+
}
175+
sort.Ints(hourlyTimestamps)
176+
timestamps := make([]int64, 0, len(hourlyTimestamps))
177+
values := make([]int, 0, len(hourlyTimestamps))
178+
for _, hourlyTimestamp := range hourlyTimestamps {
179+
timestamp := int64(hourlyTimestamp)
180+
averageResponseTime := hourlyAverageResponseTime[timestamp]
181+
timestamps = append(timestamps, timestamp*1000)
182+
values = append(values, averageResponseTime)
183+
}
184+
return c.Status(http.StatusOK).JSON(map[string]interface{}{
185+
"timestamps": timestamps,
186+
"values": values,
187+
})
188+
}

api/chart_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,69 @@ func TestResponseTimeChart(t *testing.T) {
8181
})
8282
}
8383
}
84+
85+
func TestResponseTimeHistory(t *testing.T) {
86+
defer store.Get().Clear()
87+
defer cache.Clear()
88+
cfg := &config.Config{
89+
Metrics: true,
90+
Endpoints: []*endpoint.Endpoint{
91+
{
92+
Name: "frontend",
93+
Group: "core",
94+
},
95+
{
96+
Name: "backend",
97+
Group: "core",
98+
},
99+
},
100+
}
101+
watchdog.UpdateEndpointStatus(cfg.Endpoints[0], &endpoint.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
102+
watchdog.UpdateEndpointStatus(cfg.Endpoints[1], &endpoint.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
103+
api := New(cfg)
104+
router := api.Router()
105+
type Scenario struct {
106+
Name string
107+
Path string
108+
ExpectedCode int
109+
}
110+
scenarios := []Scenario{
111+
{
112+
Name: "history-response-time-24h",
113+
Path: "/api/v1/endpoints/core_backend/response-times/24h/history",
114+
ExpectedCode: http.StatusOK,
115+
},
116+
{
117+
Name: "history-response-time-7d",
118+
Path: "/api/v1/endpoints/core_frontend/response-times/7d/history",
119+
ExpectedCode: http.StatusOK,
120+
},
121+
{
122+
Name: "history-response-time-30d",
123+
Path: "/api/v1/endpoints/core_frontend/response-times/30d/history",
124+
ExpectedCode: http.StatusOK,
125+
},
126+
{
127+
Name: "history-response-time-with-invalid-duration",
128+
Path: "/api/v1/endpoints/core_backend/response-times/3d/history",
129+
ExpectedCode: http.StatusBadRequest,
130+
},
131+
{
132+
Name: "history-response-time-for-invalid-key",
133+
Path: "/api/v1/endpoints/invalid_key/response-times/7d/history",
134+
ExpectedCode: http.StatusNotFound,
135+
},
136+
}
137+
for _, scenario := range scenarios {
138+
t.Run(scenario.Name, func(t *testing.T) {
139+
request := httptest.NewRequest("GET", scenario.Path, http.NoBody)
140+
response, err := router.Test(request)
141+
if err != nil {
142+
t.Fatal(err)
143+
}
144+
if response.StatusCode != scenario.ExpectedCode {
145+
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, response.StatusCode)
146+
}
147+
})
148+
}
149+
}

0 commit comments

Comments
 (0)