package handler import ( "log" "math" "net/http" "sort" "time" "github.com/TwiN/gatus/v3/storage/store" "github.com/TwiN/gatus/v3/storage/store/common" "github.com/gorilla/mux" "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" ) const timeFormat = "3:04PM" var ( gridStyle = chart.Style{ StrokeColor: drawing.Color{R: 119, G: 119, B: 119, A: 40}, StrokeWidth: 1.0, } axisStyle = chart.Style{ FontColor: drawing.Color{R: 119, G: 119, B: 119, A: 255}, } transparentStyle = chart.Style{ FillColor: drawing.Color{R: 255, G: 255, B: 255, A: 0}, } ) func ResponseTimeChart(writer http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) duration := vars["duration"] var from time.Time switch duration { case "7d": from = time.Now().Truncate(time.Hour).Add(-24 * 7 * time.Hour) case "24h": from = time.Now().Truncate(time.Hour).Add(-24 * time.Hour) default: http.Error(writer, "Durations supported: 7d, 24h", http.StatusBadRequest) return } hourlyAverageResponseTime, err := store.Get().GetHourlyAverageResponseTimeByKey(vars["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 } if len(hourlyAverageResponseTime) == 0 { http.Error(writer, "", http.StatusNoContent) return } series := chart.TimeSeries{ Name: "Average response time per hour", Style: chart.Style{ StrokeWidth: 1.5, DotWidth: 2.0, }, } keys := make([]int, 0, len(hourlyAverageResponseTime)) earliestTimestamp := int64(0) for hourlyTimestamp := range hourlyAverageResponseTime { keys = append(keys, int(hourlyTimestamp)) if earliestTimestamp == 0 || hourlyTimestamp < earliestTimestamp { earliestTimestamp = hourlyTimestamp } } for earliestTimestamp > from.Unix() { earliestTimestamp -= int64(time.Hour.Seconds()) keys = append(keys, int(earliestTimestamp)) } sort.Ints(keys) var maxAverageResponseTime float64 for _, key := range keys { averageResponseTime := float64(hourlyAverageResponseTime[int64(key)]) if maxAverageResponseTime < averageResponseTime { maxAverageResponseTime = averageResponseTime } series.XValues = append(series.XValues, time.Unix(int64(key), 0)) series.YValues = append(series.YValues, averageResponseTime) } graph := chart.Chart{ Canvas: transparentStyle, Background: transparentStyle, Width: 1280, Height: 300, XAxis: chart.XAxis{ ValueFormatter: chart.TimeValueFormatterWithFormat(timeFormat), GridMajorStyle: gridStyle, GridMinorStyle: gridStyle, Style: axisStyle, NameStyle: axisStyle, }, YAxis: chart.YAxis{ Name: "Average response time", GridMajorStyle: gridStyle, GridMinorStyle: gridStyle, Style: axisStyle, NameStyle: axisStyle, Range: &chart.ContinuousRange{ Min: 0, Max: math.Ceil(maxAverageResponseTime * 1.25), }, }, Series: []chart.Series{series}, } writer.Header().Set("Content-Type", "image/svg+xml") writer.Header().Set("Cache-Control", "no-cache, no-store") writer.Header().Set("Expires", "0") writer.WriteHeader(http.StatusOK) if err := graph.Render(chart.SVG, writer); err != nil { log.Println("[handler][ResponseTimeChart] Failed to render response time chart:", err.Error()) return } }