feat(api): Configurable response time badge thresholds (#309)
* recreated all changes for setting thresholds on Uptime Badges * Suggestion accepted: Update core/ui/ui.go Co-authored-by: TwiN <twin@linux.com> * Suggestion accepted: Update core/ui/ui.go Co-authored-by: TwiN <twin@linux.com> * implemented final suggestions by Twin * Update controller/handler/badge.go * Update README.md * test: added the suggestons to set the UiConfig at another line Co-authored-by: TwiN <twin@linux.com>
This commit is contained in:
@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
"github.com/TwiN/gatus/v4/storage/store"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common"
|
||||
"github.com/TwiN/gatus/v4/storage/store/common/paging"
|
||||
@ -28,6 +29,10 @@ const (
|
||||
HealthStatusUnknown = "?"
|
||||
)
|
||||
|
||||
var (
|
||||
badgeColors = []string{badgeColorHexAwesome, badgeColorHexGreat, badgeColorHexGood, badgeColorHexPassable, badgeColorHexBad}
|
||||
)
|
||||
|
||||
// UptimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||
//
|
||||
// Valid values for {duration}: 7d, 24h, 1h
|
||||
@ -68,38 +73,40 @@ func UptimeBadge(writer http.ResponseWriter, request *http.Request) {
|
||||
// ResponseTimeBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||
//
|
||||
// Valid values for {duration}: 7d, 24h, 1h
|
||||
func ResponseTimeBadge(writer http.ResponseWriter, request *http.Request) {
|
||||
variables := mux.Vars(request)
|
||||
duration := variables["duration"]
|
||||
var from time.Time
|
||||
switch duration {
|
||||
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 response time metrics are stored by hour, we have to cheat a little
|
||||
default:
|
||||
http.Error(writer, "Durations supported: 7d, 24h, 1h", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
key := variables["key"]
|
||||
averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now())
|
||||
if err != nil {
|
||||
if err == common.ErrEndpointNotFound {
|
||||
http.Error(writer, err.Error(), http.StatusNotFound)
|
||||
} else if err == common.ErrInvalidTimeRange {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
} else {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
func ResponseTimeBadge(config *config.Config) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
variables := mux.Vars(request)
|
||||
duration := variables["duration"]
|
||||
var from time.Time
|
||||
switch duration {
|
||||
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 response time metrics are stored by hour, we have to cheat a little
|
||||
default:
|
||||
http.Error(writer, "Durations supported: 7d, 24h, 1h", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
return
|
||||
key := variables["key"]
|
||||
averageResponseTime, err := store.Get().GetAverageResponseTimeByKey(key, from, time.Now())
|
||||
if err != nil {
|
||||
if err == common.ErrEndpointNotFound {
|
||||
http.Error(writer, err.Error(), http.StatusNotFound)
|
||||
} else if err == common.ErrInvalidTimeRange {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
} else {
|
||||
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
writer.Header().Set("Expires", "0")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write(generateResponseTimeBadgeSVG(duration, averageResponseTime, key, config))
|
||||
}
|
||||
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
writer.Header().Set("Expires", "0")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, _ = writer.Write(generateResponseTimeBadgeSVG(duration, averageResponseTime))
|
||||
}
|
||||
|
||||
// HealthBadge handles the automatic generation of badge based on the group name and endpoint name passed.
|
||||
@ -199,7 +206,7 @@ func getBadgeColorFromUptime(uptime float64) string {
|
||||
return badgeColorHexVeryBad
|
||||
}
|
||||
|
||||
func generateResponseTimeBadgeSVG(duration string, averageResponseTime int) []byte {
|
||||
func generateResponseTimeBadgeSVG(duration string, averageResponseTime int, key string, cfg *config.Config) []byte {
|
||||
var labelWidth, valueWidth int
|
||||
switch duration {
|
||||
case "7d":
|
||||
@ -210,7 +217,7 @@ func generateResponseTimeBadgeSVG(duration string, averageResponseTime int) []by
|
||||
labelWidth = 105
|
||||
default:
|
||||
}
|
||||
color := getBadgeColorFromResponseTime(averageResponseTime)
|
||||
color := getBadgeColorFromResponseTime(averageResponseTime, key, cfg)
|
||||
sanitizedValue := strconv.Itoa(averageResponseTime) + "ms"
|
||||
valueWidth = len(sanitizedValue) * 11
|
||||
width := labelWidth + valueWidth
|
||||
@ -247,17 +254,13 @@ func generateResponseTimeBadgeSVG(duration string, averageResponseTime int) []by
|
||||
return svg
|
||||
}
|
||||
|
||||
func getBadgeColorFromResponseTime(responseTime int) string {
|
||||
if responseTime <= 50 {
|
||||
return badgeColorHexAwesome
|
||||
} else if responseTime <= 200 {
|
||||
return badgeColorHexGreat
|
||||
} else if responseTime <= 300 {
|
||||
return badgeColorHexGood
|
||||
} else if responseTime <= 500 {
|
||||
return badgeColorHexPassable
|
||||
} else if responseTime <= 750 {
|
||||
return badgeColorHexBad
|
||||
func getBadgeColorFromResponseTime(responseTime int, key string, cfg *config.Config) string {
|
||||
endpoint := cfg.GetEndpointByKey(key)
|
||||
// the threshold config requires 5 values, so we can be sure it's set here
|
||||
for i := 0; i < 5; i++ {
|
||||
if responseTime <= endpoint.UIConfig.Badge.ResponseTime.Thresholds[i] {
|
||||
return badgeColors[i]
|
||||
}
|
||||
}
|
||||
return badgeColorHexVeryBad
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/TwiN/gatus/v4/config"
|
||||
"github.com/TwiN/gatus/v4/core"
|
||||
"github.com/TwiN/gatus/v4/core/ui"
|
||||
"github.com/TwiN/gatus/v4/storage/store"
|
||||
"github.com/TwiN/gatus/v4/watchdog"
|
||||
)
|
||||
@ -29,6 +30,36 @@ func TestBadge(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testSuccessfulResult = core.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
Errors: nil,
|
||||
Connected: true,
|
||||
Success: true,
|
||||
Timestamp: timestamp,
|
||||
Duration: 150 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[RESPONSE_TIME] < 500",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
||||
Success: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg.Endpoints[0].UIConfig = ui.GetDefaultConfig()
|
||||
cfg.Endpoints[1].UIConfig = ui.GetDefaultConfig()
|
||||
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[0], &core.Result{Success: true, Connected: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateEndpointStatuses(cfg.Endpoints[1], &core.Result{Success: false, Connected: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter("../../web/static", cfg)
|
||||
@ -180,7 +211,61 @@ func TestGetBadgeColorFromUptime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
firstCondition = core.Condition("[STATUS] == 200")
|
||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||
)
|
||||
|
||||
func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||
defer store.Get().Clear()
|
||||
defer cache.Clear()
|
||||
|
||||
testEndpoint = core.Endpoint{
|
||||
Name: "name",
|
||||
Group: "group",
|
||||
URL: "https://example.org/what/ever",
|
||||
Method: "GET",
|
||||
Body: "body",
|
||||
Interval: 30 * time.Second,
|
||||
Conditions: []core.Condition{firstCondition, secondCondition, thirdCondition},
|
||||
Alerts: nil,
|
||||
NumberOfFailuresInARow: 0,
|
||||
NumberOfSuccessesInARow: 0,
|
||||
UIConfig: ui.GetDefaultConfig(),
|
||||
}
|
||||
testSuccessfulResult = core.Result{
|
||||
Hostname: "example.org",
|
||||
IP: "127.0.0.1",
|
||||
HTTPStatus: 200,
|
||||
Errors: nil,
|
||||
Connected: true,
|
||||
Success: true,
|
||||
Timestamp: timestamp,
|
||||
Duration: 150 * time.Millisecond,
|
||||
CertificateExpiration: 10 * time.Hour,
|
||||
ConditionResults: []*core.ConditionResult{
|
||||
{
|
||||
Condition: "[STATUS] == 200",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[RESPONSE_TIME] < 500",
|
||||
Success: true,
|
||||
},
|
||||
{
|
||||
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
||||
Success: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
cfg := &config.Config{
|
||||
Metrics: true,
|
||||
Endpoints: []*core.Endpoint{&testEndpoint},
|
||||
}
|
||||
|
||||
store.Get().Insert(&testEndpoint, &testSuccessfulResult)
|
||||
|
||||
scenarios := []struct {
|
||||
ResponseTime int
|
||||
ExpectedColor string
|
||||
@ -228,8 +313,8 @@ func TestGetBadgeColorFromResponseTime(t *testing.T) {
|
||||
}
|
||||
for _, scenario := range scenarios {
|
||||
t.Run("response-time-"+strconv.Itoa(scenario.ResponseTime), func(t *testing.T) {
|
||||
if getBadgeColorFromResponseTime(scenario.ResponseTime) != scenario.ExpectedColor {
|
||||
t.Errorf("expected %s from %d, got %v", scenario.ExpectedColor, scenario.ResponseTime, getBadgeColorFromResponseTime(scenario.ResponseTime))
|
||||
if getBadgeColorFromResponseTime(scenario.ResponseTime, "group_name", cfg) != scenario.ExpectedColor {
|
||||
t.Errorf("expected %s from %d, got %v", scenario.ExpectedColor, scenario.ResponseTime, getBadgeColorFromResponseTime(scenario.ResponseTime, "group_name", cfg))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ func CreateRouter(staticFolder string, cfg *config.Config) *mux.Router {
|
||||
protected.HandleFunc("/v1/endpoints/{key}/statuses", GzipHandlerFunc(EndpointStatus)).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/health/badge.svg", HealthBadge).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/uptimes/{duration}/badge.svg", UptimeBadge).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/response-times/{duration}/badge.svg", ResponseTimeBadge).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/response-times/{duration}/badge.svg", ResponseTimeBadge(cfg)).Methods("GET")
|
||||
unprotected.HandleFunc("/v1/endpoints/{key}/response-times/{duration}/chart.svg", ResponseTimeChart).Methods("GET")
|
||||
// Misc
|
||||
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
||||
|
Reference in New Issue
Block a user