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"
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
)
2020-12-30 01:08:20 -05:00
func init () {
if err := cache . StartJanitor (); err != nil {
log . Fatal ( "[controller][init] Failed to start cache janitor:" , err . Error ())
}
}
2020-12-29 20:22:17 -05:00
// Handle creates the router and starts the server
func Handle () {
cfg := config . Get ()
2021-01-24 04:48:07 -05:00
var router http . Handler = CreateRouter ( cfg )
if os . Getenv ( "ENVIRONMENT" ) == "dev" {
router = developmentCorsHandler ( router )
}
2021-02-01 01:37:56 -05:00
server = & http . Server {
2020-12-29 20:22:17 -05: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-02 23:06:34 -05:00
log . Println ( "[controller][Handle] Listening on " + cfg . Web . 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
func CreateRouter ( cfg * config . Config ) * mux . Router {
router := mux . NewRouter ()
if cfg . Metrics {
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" )
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-29 20:22:17 -05:00
return router
}
2021-01-27 18:25:37 -05:00
func secureIfNecessary ( cfg * config . Config , handler http . HandlerFunc ) http . HandlerFunc {
if cfg . Security != nil && cfg . Security . IsValid () {
2021-01-31 23:29:48 -05:00
return security . Handler ( handler , cfg . Security )
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-02-24 22:41:36 -05:00
data , err = json . Marshal ( storage . Get (). GetAllServiceStatusesWithResultPagination ( 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-02-24 22:41:36 -05:00
serviceStatus := storage . Get (). GetServiceStatusByKey ( vars [ "key" ])
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-01-28 22:44:31 -05:00
data := map [ string ] interface {}{
2021-02-24 22:41:36 -05:00
"serviceStatus" : serviceStatus . WithResultPagination ( page , pageSize ),
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-02-02 23:06:34 -05:00
"uptime" : serviceStatus . Uptime ,
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
}