diff --git a/controller/handler/chart.go b/controller/handler/chart.go index b8213b9c..5ef4b803 100644 --- a/controller/handler/chart.go +++ b/controller/handler/chart.go @@ -1,6 +1,7 @@ package handler import ( + "encoding/json" "log" "math" "net/http" @@ -117,3 +118,41 @@ func ResponseTimeChart(writer http.ResponseWriter, r *http.Request) { return } } + +func ResponseTime(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 + } + data, err := json.Marshal(hourlyAverageResponseTime) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + writer.Header().Add("Content-Type", "application/json") + writer.WriteHeader(http.StatusOK) + _, _ = writer.Write(data) +} diff --git a/controller/handler/handler.go b/controller/handler/handler.go index 9253b929..a1b1b255 100644 --- a/controller/handler/handler.go +++ b/controller/handler/handler.go @@ -23,6 +23,7 @@ func CreateRouter(staticFolder string, securityConfig *security.Config, uiConfig router.HandleFunc("/api/v1/endpoints/{key}/uptimes/{duration}/badge.svg", UptimeBadge).Methods("GET") router.HandleFunc("/api/v1/endpoints/{key}/response-times/{duration}/badge.svg", ResponseTimeBadge).Methods("GET") router.HandleFunc("/api/v1/endpoints/{key}/response-times/{duration}/chart.svg", ResponseTimeChart).Methods("GET") + router.HandleFunc("/api/v1/endpoints/{key}/response-times/{duration}", ResponseTime).Methods("GET") // XXX: Remove the lines between this and the next XXX comment in v4.0.0 router.HandleFunc("/api/v1/services/statuses", secureIfNecessary(securityConfig, EndpointStatuses)).Methods("GET") // No GzipHandler for this one, because we cache the content as Gzipped already router.HandleFunc("/api/v1/services/{key}/statuses", secureIfNecessary(securityConfig, GzipHandlerFunc(EndpointStatus))).Methods("GET") diff --git a/web/app/package.json b/web/app/package.json index 7846c39c..e3a183aa 100644 --- a/web/app/package.json +++ b/web/app/package.json @@ -10,6 +10,7 @@ "dependencies": { "core-js": "^3.19.1", "vue": "3.2.21", + "vue-chart-3": "^0.5.11", "vue-router": "^4.0.11" }, "devDependencies": { diff --git a/web/app/src/views/Details.vue b/web/app/src/views/Details.vue index a30aeeec..3b5962c3 100644 --- a/web/app/src/views/Details.vue +++ b/web/app/src/views/Details.vue @@ -37,6 +37,9 @@

RESPONSE TIME


+ + + response time chart
@@ -92,17 +95,24 @@ import {SERVER_URL} from "@/main.js"; import {helper} from "@/mixins/helper.js"; import Pagination from "@/components/Pagination"; +import { LineChart } from 'vue-chart-3'; +import { Chart, CategoryScale, LineController, LinearScale, LineElement, PointElement, TimeScale, Title, Tooltip} from 'chart.js'; +Chart.register(LineController, CategoryScale, LinearScale, LineElement, PointElement, TimeScale, Title, Tooltip); + + export default { name: 'Details', components: { Pagination, Endpoint, Settings, + LineChart, }, emits: ['showTooltip'], mixins: [helper], methods: { fetchData() { + // XXX: This should probably be called every 15 minutes or so //console.log("[Details][fetchData] Fetching data"); fetch(`${this.serverUrl}/api/v1/endpoints/${this.$route.params.key}/statuses?page=${this.currentPage}`) .then(response => response.json()) @@ -140,8 +150,39 @@ export default { } this.events = events; } + this.fetchUptimeChartData(); }); }, + fetchUptimeChartData() { + fetch(`${this.serverUrl}/api/v1/endpoints/${this.$route.params.key}/response-times/24h`).then(response => { + response.json().then(data => { + let chart = { + labels: [], + datasets: [ + { + label: 'Average response time (ms)', + data: [], + borderColor: 'rgb(75, 192, 192)', + }, + ] + }; + let latest = null; + for (const [key] of Object.entries(data)) { + latest = key; + } + for (let i = 24; i >= 0; i--) { + let date = new Date((latest*1000)-(i*3600000)); + chart.labels.push(date.toLocaleTimeString().replaceAll(":00", "")); + if (data[date.getTime()/1000]) { + chart.datasets[0].data.push(data[date.getTime()/1000]); + } else { + chart.datasets[0].data.push(0); + } + } + this.chartData = chart; + }) + }); + }, generateUptimeBadgeImageURL(duration) { return `${this.serverUrl}/api/v1/endpoints/${this.endpointStatus.key}/uptimes/${duration}/badge.svg`; }, @@ -182,8 +223,18 @@ export default { serverUrl: SERVER_URL === '.' ? '..' : SERVER_URL, currentPage: 1, showAverageResponseTime: true, - chartLabels: [], - chartValues: [], + chartData: {labels: [], datasets: [{data: []}]}, + chartOptions: { + scales: { + y: { + min: 0, + title: { + display: true, + text: 'Average response time (ms)' + } + } + } + } } }, created() {