2020-12-30 02:22:17 +01:00
package controller
import (
"bytes"
"compress/gzip"
2021-02-06 02:45:28 +01:00
"context"
2021-01-28 00:25:37 +01:00
"encoding/json"
2020-12-30 02:22:17 +01:00
"fmt"
"log"
"net/http"
2021-01-24 10:48:07 +01:00
"os"
2020-12-30 02:22:17 +01:00
"strings"
"time"
"github.com/TwinProduction/gatus/config"
"github.com/TwinProduction/gatus/security"
"github.com/TwinProduction/gatus/watchdog"
2020-12-30 07:08:20 +01:00
"github.com/TwinProduction/gocache"
2020-12-30 02:22:17 +01:00
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
cacheTTL = 10 * time . Second
)
var (
2020-12-30 07:08:20 +01:00
cache = gocache . NewCache ( ) . WithMaxSize ( 100 ) . WithEvictionPolicy ( gocache . LeastRecentlyUsed )
2021-02-01 07:37:56 +01: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-30 02:22:17 +01:00
)
2020-12-30 07:08:20 +01:00
func init ( ) {
if err := cache . StartJanitor ( ) ; err != nil {
log . Fatal ( "[controller][init] Failed to start cache janitor:" , err . Error ( ) )
}
}
2020-12-30 02:22:17 +01:00
// Handle creates the router and starts the server
func Handle ( ) {
cfg := config . Get ( )
2021-01-24 10:48:07 +01:00
var router http . Handler = CreateRouter ( cfg )
if os . Getenv ( "ENVIRONMENT" ) == "dev" {
router = developmentCorsHandler ( router )
}
2021-02-01 07:37:56 +01:00
server = & http . Server {
2020-12-30 02:22:17 +01:00
Addr : fmt . Sprintf ( "%s:%d" , cfg . Web . Address , cfg . Web . Port ) ,
Handler : router ,
ReadTimeout : 15 * time . Second ,
WriteTimeout : 15 * time . Second ,
IdleTimeout : 15 * time . Second ,
}
2021-02-03 05:06:34 +01:00
log . Println ( "[controller][Handle] Listening on " + cfg . Web . SocketAddress ( ) )
2021-02-01 07:37:56 +01:00
if os . Getenv ( "ROUTER_TEST" ) == "true" {
return
}
2021-02-06 02:45:28 +01:00
log . Println ( "[controller][Handle]" , server . ListenAndServe ( ) )
}
// Shutdown stops the server
func Shutdown ( ) {
if server != nil {
_ = server . Shutdown ( context . TODO ( ) )
server = nil
}
2020-12-30 02:22:17 +01:00
}
// CreateRouter creates the router for the http server
func CreateRouter ( cfg * config . Config ) * mux . Router {
router := mux . NewRouter ( )
if cfg . Metrics {
2021-01-31 11:49:01 +01:00
router . Handle ( "/metrics" , promhttp . Handler ( ) ) . Methods ( "GET" )
2020-12-30 02:22:17 +01:00
}
2021-02-01 07:37:56 +01:00
router . HandleFunc ( "/favicon.ico" , favIconHandler ) . Methods ( "GET" )
router . HandleFunc ( "/health" , healthHandler ) . Methods ( "GET" )
router . HandleFunc ( "/api/v1/statuses" , secureIfNecessary ( cfg , serviceStatusesHandler ) ) . Methods ( "GET" ) // No GzipHandler for this one, because we cache the content
router . HandleFunc ( "/api/v1/statuses/{key}" , secureIfNecessary ( cfg , GzipHandlerFunc ( serviceStatusHandler ) ) ) . Methods ( "GET" )
router . HandleFunc ( "/api/v1/badges/uptime/{duration}/{identifier}" , badgeHandler ) . Methods ( "GET" )
// 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-30 02:22:17 +01:00
return router
}
2021-01-28 00:25:37 +01:00
func secureIfNecessary ( cfg * config . Config , handler http . HandlerFunc ) http . HandlerFunc {
if cfg . Security != nil && cfg . Security . IsValid ( ) {
2021-02-01 05:29:48 +01:00
return security . Handler ( handler , cfg . Security )
2021-01-28 00:25:37 +01: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-30 02:22:17 +01:00
func serviceStatusesHandler ( writer http . ResponseWriter , r * http . Request ) {
2020-12-30 07:08:20 +01:00
gzipped := strings . Contains ( r . Header . Get ( "Accept-Encoding" ) , "gzip" )
var exists bool
var value interface { }
if gzipped {
writer . Header ( ) . Set ( "Content-Encoding" , "gzip" )
value , exists = cache . Get ( "service-status-gzipped" )
} else {
value , exists = cache . Get ( "service-status" )
}
var data [ ] byte
if ! exists {
var err error
2020-12-30 02:22:17 +01:00
buffer := & bytes . Buffer { }
gzipWriter := gzip . NewWriter ( buffer )
2020-12-31 21:28:57 +01:00
data , err = watchdog . GetServiceStatusesAsJSON ( )
2020-12-30 02:22:17 +01:00
if err != nil {
2021-01-28 00:25:37 +01:00
log . Printf ( "[controller][serviceStatusesHandler] Unable to marshal object to JSON: %s" , err . Error ( ) )
2020-12-30 02:22:17 +01:00
writer . WriteHeader ( http . StatusInternalServerError )
_ , _ = writer . Write ( [ ] byte ( "Unable to marshal object to JSON" ) )
return
}
2020-12-30 07:08:20 +01:00
_ , _ = gzipWriter . Write ( data )
_ = gzipWriter . Close ( )
gzippedData := buffer . Bytes ( )
cache . SetWithTTL ( "service-status" , data , cacheTTL )
cache . SetWithTTL ( "service-status-gzipped" , gzippedData , cacheTTL )
if gzipped {
data = gzippedData
}
2020-12-30 02:22:17 +01:00
} else {
2020-12-30 07:08:20 +01:00
data = value . ( [ ] byte )
2020-12-30 02:22:17 +01:00
}
2020-12-30 07:08:20 +01:00
writer . Header ( ) . Add ( "Content-Type" , "application/json" )
2020-12-30 02:22:17 +01:00
writer . WriteHeader ( http . StatusOK )
_ , _ = writer . Write ( data )
}
2021-01-28 00:25:37 +01:00
// serviceStatusHandler retrieves a single ServiceStatus by group name and service name
func serviceStatusHandler ( writer http . ResponseWriter , r * http . Request ) {
vars := mux . Vars ( r )
serviceStatus := watchdog . GetServiceStatusByKey ( vars [ "key" ] )
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-01-29 04:44:31 +01:00
data := map [ string ] interface { } {
"serviceStatus" : serviceStatus ,
2021-02-03 05:06:34 +01: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-29 04:44:31 +01:00
"events" : serviceStatus . Events ,
2021-02-03 05:06:34 +01:00
"uptime" : serviceStatus . Uptime ,
2021-01-29 04:44:31 +01:00
}
output , err := json . Marshal ( data )
2021-01-28 00:25:37 +01:00
if err != nil {
log . Printf ( "[controller][serviceStatusHandler] Unable to marshal object to JSON: %s" , err . Error ( ) )
writer . WriteHeader ( http . StatusInternalServerError )
_ , _ = writer . Write ( [ ] byte ( "Unable to marshal object to JSON" ) )
return
}
writer . Header ( ) . Add ( "Content-Type" , "application/json" )
writer . WriteHeader ( http . StatusOK )
2021-01-29 04:44:31 +01:00
_ , _ = writer . Write ( output )
2021-01-28 00:25:37 +01:00
}
2020-12-30 02:22:17 +01:00
func healthHandler ( writer http . ResponseWriter , _ * http . Request ) {
2020-12-30 07:08:20 +01:00
writer . Header ( ) . Add ( "Content-Type" , "application/json" )
2020-12-30 02:22:17 +01:00
writer . WriteHeader ( http . StatusOK )
_ , _ = writer . Write ( [ ] byte ( "{\"status\":\"UP\"}" ) )
}
// favIconHandler handles requests for /favicon.ico
func favIconHandler ( writer http . ResponseWriter , request * http . Request ) {
2021-02-01 07:37:56 +01:00
http . ServeFile ( writer , request , staticFolder + "/favicon.ico" )
2021-01-29 05:25:29 +01:00
}
// spaHandler handles requests for /favicon.ico
func spaHandler ( writer http . ResponseWriter , request * http . Request ) {
2021-02-01 07:37:56 +01:00
http . ServeFile ( writer , request , staticFolder + "/index.html" )
2020-12-30 02:22:17 +01:00
}