From c04d8efc0f38e0af7da6f2528701a1d1cdd523b9 Mon Sep 17 00:00:00 2001 From: zachmann Date: Fri, 29 Nov 2024 14:45:06 +0100 Subject: [PATCH 1/3] implement healthcheck endpoint --- cmd/mytoken-server/main.go | 2 + config/example-config.yaml | 3 + internal/config/config.go | 12 +- internal/server/healthcheck/healthcheck.go | 131 +++++++++++++++++++++ internal/server/routes/routes.go | 2 + 5 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 internal/server/healthcheck/healthcheck.go diff --git a/cmd/mytoken-server/main.go b/cmd/mytoken-server/main.go index 96c16209..5388e799 100644 --- a/cmd/mytoken-server/main.go +++ b/cmd/mytoken-server/main.go @@ -20,6 +20,7 @@ import ( "github.com/oidc-mytoken/server/internal/oidc/oidcfed" provider2 "github.com/oidc-mytoken/server/internal/oidc/provider" "github.com/oidc-mytoken/server/internal/server" + "github.com/oidc-mytoken/server/internal/server/healthcheck" "github.com/oidc-mytoken/server/internal/server/routes" "github.com/oidc-mytoken/server/internal/utils/cache" "github.com/oidc-mytoken/server/internal/utils/cookies" @@ -44,6 +45,7 @@ func main() { settings.InitSettings() cookies.Init() notifier.Init() + healthcheck.Start() server.Start() } diff --git a/config/example-config.yaml b/config/example-config.yaml index e9a20e05..ea1e9e77 100644 --- a/config/example-config.yaml +++ b/config/example-config.yaml @@ -46,6 +46,9 @@ server: # hostnames including wildcards. always_allow: - "127.0.0.1" + healthcheck: + enabled: true + port: 9876 # The database file for ip geolocation. Will be installed by setup to this location. geo_ip_db_file: "/IP2LOCATION-LITE-DB1.IPV6.BIN" diff --git a/internal/config/config.go b/internal/config/config.go index 1f9c4e49..2fa94b27 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -414,9 +414,15 @@ type serverConf struct { TLS tlsConf `yaml:"tls"` Secure bool `yaml:"-"` // Secure indicates if the connection to the mytoken server is secure. This is // independent of TLS, e.g. a Proxy can be used. - ProxyHeader string `yaml:"proxy_header"` - Limiter limiterConf `yaml:"request_limits"` - DistributedServers bool `yaml:"distributed_servers"` + ProxyHeader string `yaml:"proxy_header"` + Limiter limiterConf `yaml:"request_limits"` + DistributedServers bool `yaml:"distributed_servers"` + Healthcheck healtcheckConfig `yaml:"healthcheck"` +} + +type healtcheckConfig struct { + Enabled bool `yaml:"enabled"` + Port int `yaml:"port"` } type limiterConf struct { diff --git a/internal/server/healthcheck/healthcheck.go b/internal/server/healthcheck/healthcheck.go new file mode 100644 index 00000000..54e8e984 --- /dev/null +++ b/internal/server/healthcheck/healthcheck.go @@ -0,0 +1,131 @@ +package healthcheck + +import ( + "fmt" + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/oidc-mytoken/utils/httpclient" + "github.com/oidc-mytoken/utils/utils" + log "github.com/sirupsen/logrus" + "github.com/zachmann/go-oidfed/pkg" + "github.com/zachmann/go-oidfed/pkg/cache" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db/dbrepo/versionrepo" + "github.com/oidc-mytoken/server/internal/model/version" + "github.com/oidc-mytoken/server/internal/server/routes" +) + +// Start starts the healthcheck endpoint on the configured port if enabled +func Start() { + if !config.Get().Server.Healthcheck.Enabled { + return + } + httpServer := fiber.New() + httpServer.Get( + "", handleHealthCheck, + ) + addr := fmt.Sprintf(":%d", config.Get().Server.Healthcheck.Port) + log.Info("Healthcheck endpoint started on %s", addr) + go func() { + log.WithError(httpServer.Listen(addr)).Fatal() + }() +} + +type status struct { + Healthy bool `json:"healthy"` + Operational bool `json:"operational"` + Components componentsStatus `json:"components"` + Version string `json:"version"` + Timestamp pkg.Unixtime `json:"timestamp"` +} + +type componentsStatus struct { + ServerUp bool `json:"server_up"` + ServerReachable bool `json:"server_reachable"` + Database bool `json:"database_up"` + Cache bool `json:"cache_up"` +} + +func (c componentsStatus) healthy() bool { + return c.ServerUp && c.ServerReachable && c.Database && c.Cache +} +func (c componentsStatus) operational() bool { + return c.ServerUp && c.ServerReachable && c.Database +} + +func handleHealthCheck(ctx *fiber.Ctx) error { + state := healthcheck() + if !state.Operational { + ctx.Status(fiber.StatusServiceUnavailable) + } + return ctx.JSON(state) +} + +func healthcheck() status { + components := componentsStatus{ + ServerUp: true, + ServerReachable: checkServer(), + Database: checkDB(), + Cache: checkCache(), + } + return status{ + Healthy: components.healthy(), + Operational: components.operational(), + Components: components, + Version: version.VERSION, + Timestamp: pkg.Unixtime{Time: time.Now()}, + } +} + +func checkServer() bool { + _, err := httpclient.Do().R().Get(routes.ConfigEndpoint) + if err != nil { + log.WithError(err).WithField("healthcheck", "server_reachable").Error("error server healthcheck") + return false + } + return true +} + +func checkDB() bool { + _, err := versionrepo.GetVersionState(log.StandardLogger(), nil) + if err != nil { + log.WithError(err).WithField("healthcheck", "db").Error("error db healthcheck") + return false + } + return true +} + +var cacheMutex sync.Mutex + +func checkCache() bool { + cacheMutex.Lock() + defer cacheMutex.Unlock() + k := "healthcheck" + v := utils.RandASCIIString(64) + if err := cache.Set(k, v, time.Second); err != nil { + log.WithError(err).WithField("healthcheck", "cache").Error("error caching healthcheck") + return false + } + var cached string + set, err := cache.Get(k, &cached) + if err != nil { + log.WithError(err).WithField("healthcheck", "cache"). + Error("error obtaining cached healthcheck") + return false + } + if !set { + log.WithField("healthcheck", "cache").Error("cached healthcheck not found") + return false + } + if cached != v { + log.WithField("healthcheck", "cache"). + WithField("cached", v). + WithField("obtained", cached). + Error("cached value does not match") + return false + } + return true +} diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index 50d2c145..f41cd48f 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -18,6 +18,7 @@ var ( CalendarDownloadEndpoint string ActionsEndpoint string NotificationManagementEndpoint string + ConfigEndpoint string ) // Init initializes the authcode component @@ -31,6 +32,7 @@ func Init() { config.Get().IssuerURL, generalPaths.NotificationManagementEndpoint, ) + ConfigEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.ConfigurationEndpoint) } // ActionsURL builds an action url from a pkg.ActionInfo From 5adf81150b5eeaf66468a51f65b5aaf719ebf228 Mon Sep 17 00:00:00 2001 From: zachmann Date: Fri, 29 Nov 2024 14:55:05 +0100 Subject: [PATCH 2/3] fix log formatting string --- internal/server/healthcheck/healthcheck.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/server/healthcheck/healthcheck.go b/internal/server/healthcheck/healthcheck.go index 54e8e984..d2988c42 100644 --- a/internal/server/healthcheck/healthcheck.go +++ b/internal/server/healthcheck/healthcheck.go @@ -28,7 +28,7 @@ func Start() { "", handleHealthCheck, ) addr := fmt.Sprintf(":%d", config.Get().Server.Healthcheck.Port) - log.Info("Healthcheck endpoint started on %s", addr) + log.Infof("Healthcheck endpoint started on %s", addr) go func() { log.WithError(httpServer.Listen(addr)).Fatal() }() From 6ce24c4c7ff14281ac09191c3183dac385b0f7ca Mon Sep 17 00:00:00 2001 From: zachmann Date: Mon, 2 Dec 2024 12:31:10 +0100 Subject: [PATCH 3/3] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7c9969..3f741985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Add "Enforceable Restrictions" - Depending on a user attribute different restriction templates can be enforced +- Add possibility to have an healthcheck endpoint ### Enhancements