2020-12-29 20:22:17 -05:00
package controller
import (
"bytes"
"compress/gzip"
2021-02-05 20:45:28 -05:00
"context"
2021-01-27 18:25:37 -05:00
"encoding/json"
2020-12-29 20:22:17 -05:00
"fmt"
"log"
"net/http"
2021-01-24 04:48:07 -05:00
"os"
2020-12-29 20:22:17 -05:00
"strings"
"time"
"github.com/TwinProduction/gatus/config"
"github.com/TwinProduction/gatus/security"
2021-02-24 22:41:36 -05:00
"github.com/TwinProduction/gatus/storage"
2021-08-12 21:54:23 -04:00
"github.com/TwinProduction/gatus/storage/store/common"
"github.com/TwinProduction/gatus/storage/store/common/paging"
2020-12-30 01:08:20 -05:00
"github.com/TwinProduction/gocache"
2021-02-12 23:29:21 -05:00
"github.com/TwinProduction/health"
2020-12-29 20:22:17 -05:00
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
cacheTTL = 10 * time . Second
)
var (
2021-03-05 00:40:11 -05:00
cache = gocache . NewCache (). WithMaxSize ( 100 ). WithEvictionPolicy ( gocache . FirstInFirstOut )
2021-02-01 01:37:56 -05:00
// staticFolder is the path to the location of the static folder from the root path of the project
// The only reason this is exposed is to allow running tests from a different path than the root path of the project
staticFolder = "./web/static"
// server is the http.Server created by Handle.
// The only reason it exists is for testing purposes.
server * http . Server
2020-12-29 20:22:17 -05:00
)
// Handle creates the router and starts the server
2021-05-18 22:29:15 -04:00
func Handle ( securityConfig * security . Config , webConfig * config . WebConfig , enableMetrics bool ) {
var router http . Handler = CreateRouter ( securityConfig , enableMetrics )
2021-01-24 04:48:07 -05:00
if os . Getenv ( "ENVIRONMENT" ) == "dev" {
router = developmentCorsHandler ( router )
}
2021-02-01 01:37:56 -05:00
server = & http . Server {
2021-05-18 22:29:15 -04:00
Addr : fmt . Sprintf ( "%s:%d" , webConfig . Address , webConfig . Port ),
2020-12-29 20:22:17 -05:00
Handler : router ,
ReadTimeout : 15 * time . Second ,
WriteTimeout : 15 * time . Second ,
IdleTimeout : 15 * time . Second ,
}
2021-05-18 22:29:15 -04:00
log . Println ( "[controller][Handle] Listening on " + webConfig . SocketAddress ())
2021-02-01 01:37:56 -05:00
if os . Getenv ( "ROUTER_TEST" ) == "true" {
return
}
2021-02-05 20:45:28 -05:00
log . Println ( "[controller][Handle]" , server . ListenAndServe ())
}
// Shutdown stops the server
func Shutdown () {
if server != nil {
_ = server . Shutdown ( context . TODO ())
server = nil
}
2020-12-29 20:22:17 -05:00
}
// CreateRouter creates the router for the http server
2021-05-18 22:29:15 -04:00
func CreateRouter ( securityConfig * security . Config , enabledMetrics bool ) * mux . Router {
2020-12-29 20:22:17 -05:00
router := mux . NewRouter ()
2021-05-18 22:29:15 -04:00
if enabledMetrics {
2021-01-31 05:49:01 -05:00
router . Handle ( "/metrics" , promhttp . Handler ()). Methods ( "GET" )
2020-12-29 20:22:17 -05:00
}
2021-02-12 23:29:21 -05:00
router . Handle ( "/health" , health . Handler (). WithJSON ( true )). Methods ( "GET" )
2021-02-01 01:37:56 -05:00
router . HandleFunc ( "/favicon.ico" , favIconHandler ). Methods ( "GET" )
2021-08-21 17:38:23 -04:00
// Deprecated endpoints
router . HandleFunc ( "/api/v1/statuses" , secureIfNecessary ( securityConfig , serviceStatusesHandler )). Methods ( "GET" ) // No GzipHandler for this one, because we cache the content as Gzipped already
2021-05-18 22:29:15 -04:00
router . HandleFunc ( "/api/v1/statuses/{key}" , secureIfNecessary ( securityConfig , GzipHandlerFunc ( serviceStatusHandler ))). Methods ( "GET" )
2021-08-21 17:38:23 -04:00
router . HandleFunc ( "/api/v1/badges/uptime/{duration}/{identifier}" , uptimeBadgeHandler ). Methods ( "GET" )
// New endpoints
router . HandleFunc ( "/api/v1/services/statuses" , secureIfNecessary ( securityConfig , serviceStatusesHandler )). 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 ( serviceStatusHandler ))). Methods ( "GET" )
// TODO: router.HandleFunc("/api/v1/services/{key}/uptimes", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceUptimesHandler))).Methods("GET")
// TODO: router.HandleFunc("/api/v1/services/{key}/events", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceEventsHandler))).Methods("GET")
router . HandleFunc ( "/api/v1/services/{key}/uptimes/{duration}/badge.svg" , uptimeBadgeHandler ). Methods ( "GET" )
router . HandleFunc ( "/api/v1/services/{key}/response-times/{duration}/badge.svg" , responseTimeBadgeHandler ). Methods ( "GET" )
router . HandleFunc ( "/api/v1/services/{key}/response-times/{duration}/chart.svg" , responseTimeChartHandler ). Methods ( "GET" )
2021-02-01 01:37:56 -05:00
// SPA
router . HandleFunc ( "/services/{service}" , spaHandler ). Methods ( "GET" )
// Everything else falls back on static content
router . PathPrefix ( "/" ). Handler ( GzipHandler ( http . FileServer ( http . Dir ( staticFolder ))))
2020-12-29 20:22:17 -05:00
return router
}
2021-05-18 22:29:15 -04:00
func secureIfNecessary ( securityConfig * security . Config , handler http . HandlerFunc ) http . HandlerFunc {
if securityConfig != nil && securityConfig . IsValid () {
return security . Handler ( handler , securityConfig )
2021-01-27 18:25:37 -05:00
}
return handler
}
// serviceStatusesHandler handles requests to retrieve all service statuses
// Due to the size of the response, this function leverages a cache.
// Must not be wrapped by GzipHandler
2020-12-29 20:22:17 -05:00
func serviceStatusesHandler ( writer http . ResponseWriter , r * http . Request ) {
2021-02-24 22:41:36 -05:00
page , pageSize := extractPageAndPageSizeFromRequest ( r )
2020-12-30 01:08:20 -05:00
gzipped := strings . Contains ( r . Header . Get ( "Accept-Encoding" ), "gzip" )
var exists bool
var value interface {}
if gzipped {
writer . Header (). Set ( "Content-Encoding" , "gzip" )
2021-02-24 22:41:36 -05:00
value , exists = cache . Get ( fmt . Sprintf ( "service-status-%d-%d-gzipped" , page , pageSize ))
2020-12-30 01:08:20 -05:00
} else {
2021-02-24 22:41:36 -05:00
value , exists = cache . Get ( fmt . Sprintf ( "service-status-%d-%d" , page , pageSize ))
2020-12-30 01:08:20 -05:00
}
var data [] byte
if ! exists {
var err error
2020-12-29 20:22:17 -05:00
buffer := & bytes . Buffer {}
gzipWriter := gzip . NewWriter ( buffer )
2021-07-14 22:26:51 -04:00
data , err = json . Marshal ( storage . Get (). GetAllServiceStatuses ( paging . NewServiceStatusParams (). WithResults ( page , pageSize )))
2020-12-29 20:22:17 -05:00
if err != nil {
2021-01-27 18:25:37 -05:00
log . Printf ( "[controller][serviceStatusesHandler] Unable to marshal object to JSON: %s" , err . Error ())
2020-12-29 20:22:17 -05:00
writer . WriteHeader ( http . StatusInternalServerError )
_ , _ = writer . Write ([] byte ( "Unable to marshal object to JSON" ))
return
}
2020-12-30 01:08:20 -05:00
_ , _ = gzipWriter . Write ( data )
_ = gzipWriter . Close ()
gzippedData := buffer . Bytes ()
2021-02-24 22:41:36 -05:00
cache . SetWithTTL ( fmt . Sprintf ( "service-status-%d-%d" , page , pageSize ), data , cacheTTL )
cache . SetWithTTL ( fmt . Sprintf ( "service-status-%d-%d-gzipped" , page , pageSize ), gzippedData , cacheTTL )
2020-12-30 01:08:20 -05:00
if gzipped {
data = gzippedData
}
2020-12-29 20:22:17 -05:00
} else {
2020-12-30 01:08:20 -05:00
data = value .([] byte )
2020-12-29 20:22:17 -05:00
}
2020-12-30 01:08:20 -05:00
writer . Header (). Add ( "Content-Type" , "application/json" )
2020-12-29 20:22:17 -05:00
writer . WriteHeader ( http . StatusOK )
_ , _ = writer . Write ( data )
}
2021-01-27 18:25:37 -05:00
// serviceStatusHandler retrieves a single ServiceStatus by group name and service name
func serviceStatusHandler ( writer http . ResponseWriter , r * http . Request ) {
2021-02-24 22:41:36 -05:00
page , pageSize := extractPageAndPageSizeFromRequest ( r )
2021-01-27 18:25:37 -05:00
vars := mux . Vars ( r )
2021-08-13 00:38:39 -04:00
serviceStatus := storage . Get (). GetServiceStatusByKey ( vars [ "key" ], paging . NewServiceStatusParams (). WithResults ( page , pageSize ). WithEvents ( 1 , common . MaximumNumberOfEvents ))
2021-01-27 18:25:37 -05:00
if serviceStatus == nil {
log . Printf ( "[controller][serviceStatusHandler] Service with key=%s not found" , vars [ "key" ])
writer . WriteHeader ( http . StatusNotFound )
_ , _ = writer . Write ([] byte ( "not found" ))
return
}
2021-08-19 23:07:21 -04:00
uptime7Days , _ := storage . Get (). GetUptimeByKey ( vars [ "key" ], time . Now (). Add ( - 24 * 7 * time . Hour ), time . Now ())
uptime24Hours , _ := storage . Get (). GetUptimeByKey ( vars [ "key" ], time . Now (). Add ( - 24 * time . Hour ), time . Now ())
2021-08-13 00:38:39 -04:00
uptime1Hour , _ := storage . Get (). GetUptimeByKey ( vars [ "key" ], time . Now (). Add ( - time . Hour ), time . Now ())
2021-08-19 23:07:21 -04:00
hourlyAverageResponseTime , _ := storage . Get (). GetHourlyAverageResponseTimeByKey ( vars [ "key" ], time . Now (). Add ( - 24 * time . Hour ), time . Now ())
2021-01-28 22:44:31 -05:00
data := map [ string ] interface {}{
2021-07-14 22:26:51 -04:00
"serviceStatus" : serviceStatus ,
2021-02-02 23:06:34 -05:00
// The following fields, while present on core.ServiceStatus, are annotated to remain hidden so that we can
// expose only the necessary data on /api/v1/statuses.
// Since the /api/v1/statuses/{key} endpoint does need this data, however, we explicitly expose it here
2021-01-28 22:44:31 -05:00
"events" : serviceStatus . Events ,
2021-08-13 00:38:39 -04:00
// TODO: remove this in v3.0.0. Not used by front-end, only used for API. Left here for v2.x.x backward compatibility
"uptime" : map [ string ] float64 {
"7d" : uptime7Days ,
"24h" : uptime24Hours ,
"1h" : uptime1Hour ,
},
2021-08-19 23:07:21 -04:00
"hourlyAverageResponseTime" : hourlyAverageResponseTime ,
2021-01-28 22:44:31 -05:00
}
output , err := json . Marshal ( data )
2021-01-27 18:25:37 -05:00
if err != nil {
log . Printf ( "[controller][serviceStatusHandler] Unable to marshal object to JSON: %s" , err . Error ())
writer . WriteHeader ( http . StatusInternalServerError )
2021-02-24 22:41:36 -05:00
_ , _ = writer . Write ([] byte ( "unable to marshal object to JSON" ))
2021-01-27 18:25:37 -05:00
return
}
writer . Header (). Add ( "Content-Type" , "application/json" )
writer . WriteHeader ( http . StatusOK )
2021-01-28 22:44:31 -05:00
_ , _ = writer . Write ( output )
2021-01-27 18:25:37 -05:00
}