From 4fe66a82eac23cc5933ae30d7dd6159069a0e68c Mon Sep 17 00:00:00 2001 From: Callum Jones Date: Mon, 4 Nov 2019 14:17:01 +0000 Subject: [PATCH] add a health check endpoint --- CHANGELOG.md | 1 + healthcheck.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++ resolver.go | 12 +++++++ router.go | 3 ++ server_install.go | 10 ++++++ 5 files changed, 117 insertions(+) create mode 100644 healthcheck.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 5986e9cee..b21147665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Added: * You can now filter stock, DLC and mod cars in or out of the car search. Check out the car search help for more details! Please note that you will need to rebuild your search index for this to work. Go to Server Options, scroll down to "Maintenance" and click "Rebuild Search Index"! * Server Manager will now set up some example Championships and Custom Races if you have not yet created any * You can now sort the EntryList for a Championship Race Weekend Session by the number of Championship points a driver has. This could be useful for running reverse grid qualifying races! +* Added a health-check endpoint. Hopefully this will help us with debugging issues! Fixes: diff --git a/healthcheck.go b/healthcheck.go new file mode 100644 index 000000000..6be7fb140 --- /dev/null +++ b/healthcheck.go @@ -0,0 +1,91 @@ +package servermanager + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "runtime" + "time" + + "github.com/cj123/assetto-server-manager/pkg/udp" +) + +var LaunchTime = time.Now() + +type HealthCheck struct { + raceControl *RaceControl +} + +func NewHealthCheck(raceControl *RaceControl) *HealthCheck { + return &HealthCheck{raceControl: raceControl} +} + +type HealthCheckResponse struct { + OK bool + Version string + IsPremium bool + IsHosted bool + + OS string + NumCPU int + NumGoroutines int + Uptime string + GoVersion string + + AssettoIsInstalled bool + StrackerIsInstalled bool + + CarDirectoryIsWritable bool + TrackDirectoryIsWritable bool + WeatherDirectoryIsWritable bool + SetupsDirectoryIsWritable bool + ConfigDirectoryIsWritable bool + ResultsDirectoryIsWritable bool + + EventInProgress bool + EventIsCritical bool + NumConnectedDrivers int + MaxClientsOverride int +} + +func (h *HealthCheck) ServeHTTP(w http.ResponseWriter, r *http.Request) { + event := h.raceControl.process.Event() + + _ = json.NewEncoder(w).Encode(HealthCheckResponse{ + OK: true, + OS: runtime.GOOS + "/" + runtime.GOARCH, + Version: BuildVersion, + IsPremium: IsPremium == "true", + IsHosted: IsHosted, + MaxClientsOverride: MaxClientsOverride, + NumCPU: runtime.NumCPU(), + NumGoroutines: runtime.NumGoroutine(), + Uptime: time.Since(LaunchTime).String(), + GoVersion: runtime.Version(), + + EventInProgress: h.raceControl.process.IsRunning(), + EventIsCritical: !event.IsPractice() && (event.IsChampionship() || event.IsRaceWeekend() || h.raceControl.SessionInfo.Type == udp.SessionTypeRace || h.raceControl.SessionInfo.Type == udp.SessionTypeQualifying), + NumConnectedDrivers: h.raceControl.ConnectedDrivers.Len(), + AssettoIsInstalled: IsAssettoInstalled(), + StrackerIsInstalled: IsStrackerInstalled(), + + ConfigDirectoryIsWritable: IsDirWriteable(filepath.Join(ServerInstallPath, "cfg")) == nil, + CarDirectoryIsWritable: IsDirWriteable(filepath.Join(ServerInstallPath, "content", "cars")) == nil, + TrackDirectoryIsWritable: IsDirWriteable(filepath.Join(ServerInstallPath, "content", "tracks")) == nil, + WeatherDirectoryIsWritable: IsDirWriteable(filepath.Join(ServerInstallPath, "content", "weather")) == nil, + SetupsDirectoryIsWritable: IsDirWriteable(filepath.Join(ServerInstallPath, "setups")) == nil, + ResultsDirectoryIsWritable: IsDirWriteable(filepath.Join(ServerInstallPath, "results")) == nil, + }) +} + +func IsDirWriteable(dir string) error { + file := filepath.Join(dir, ".test-write") + + if err := ioutil.WriteFile(file, []byte(""), 0600); err != nil { + return err + } + + return os.Remove(file) +} diff --git a/resolver.go b/resolver.go index dcea4b511..7a4080b4c 100644 --- a/resolver.go +++ b/resolver.go @@ -45,6 +45,7 @@ type Resolver struct { serverAdministrationHandler *ServerAdministrationHandler raceWeekendHandler *RaceWeekendHandler strackerHandler *StrackerHandler + healthCheck *HealthCheck } func NewResolver(templateLoader TemplateLoader, reloadTemplates bool, store Store) (*Resolver, error) { @@ -426,6 +427,16 @@ func (r *Resolver) resolveStrackerHandler() *StrackerHandler { return r.strackerHandler } +func (r *Resolver) resolveHealthCheck() *HealthCheck { + if r.healthCheck != nil { + return r.healthCheck + } + + r.healthCheck = NewHealthCheck(r.resolveRaceControl()) + + return r.healthCheck +} + func (r *Resolver) ResolveRouter(fs http.FileSystem) http.Handler { return Router( fs, @@ -445,6 +456,7 @@ func (r *Resolver) ResolveRouter(fs http.FileSystem) http.Handler { r.resolveScheduledRacesHandler(), r.resolveRaceWeekendHandler(), r.resolveStrackerHandler(), + r.resolveHealthCheck(), ) } diff --git a/router.go b/router.go index 7c9f43fff..ebb529f49 100644 --- a/router.go +++ b/router.go @@ -61,6 +61,7 @@ func Router( scheduledRacesHandler *ScheduledRacesHandler, raceWeekendHandler *RaceWeekendHandler, strackerHandler *StrackerHandler, + healthCheck *HealthCheck, ) http.Handler { r := chi.NewRouter() @@ -86,6 +87,8 @@ func Router( r.Get("/", serverAdministrationHandler.home) r.Get("/changelog", serverAdministrationHandler.changelog) + r.Get("/healthcheck.json", healthCheck.ServeHTTP) + r.Mount("/stracker/", http.HandlerFunc(strackerHandler.proxy)) // content diff --git a/server_install.go b/server_install.go index cdc4df606..12c46e8ab 100644 --- a/server_install.go +++ b/server_install.go @@ -36,6 +36,16 @@ func SetAssettoInstallPath(installPath string) { } } +func IsAssettoInstalled() bool { + _, err := os.Stat(filepath.Join(ServerInstallPath, "system")) + + if err != nil { + return false + } + + return true +} + // InstallAssettoCorsaServer takes a steam login and password and runs steamcmd to install the assetto server. // If the "ServerInstallPath" exists, this function will exit without installing - unless force == true. func InstallAssettoCorsaServer(login, password string, force bool) error {