Skip to content

Commit

Permalink
Add healthcheck endpoint (#428)
Browse files Browse the repository at this point in the history
  • Loading branch information
zachmann authored Dec 3, 2024
2 parents ee4fadf + 6ce24c4 commit 2835bfa
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions cmd/mytoken-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -44,6 +45,7 @@ func main() {
settings.InitSettings()
cookies.Init()
notifier.Init()
healthcheck.Start()
server.Start()
}

Expand Down
3 changes: 3 additions & 0 deletions config/example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 9 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
131 changes: 131 additions & 0 deletions internal/server/healthcheck/healthcheck.go
Original file line number Diff line number Diff line change
@@ -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.Infof("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
}
2 changes: 2 additions & 0 deletions internal/server/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
CalendarDownloadEndpoint string
ActionsEndpoint string
NotificationManagementEndpoint string
ConfigEndpoint string
)

// Init initializes the authcode component
Expand All @@ -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
Expand Down

0 comments on commit 2835bfa

Please sign in to comment.