Skip to content

Commit 2835bfa

Browse files
authored
Add healthcheck endpoint (#428)
2 parents ee4fadf + 6ce24c4 commit 2835bfa

File tree

6 files changed

+148
-3
lines changed

6 files changed

+148
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- Add "Enforceable Restrictions"
2626
- Depending on a user attribute different restriction templates can be
2727
enforced
28+
- Add possibility to have an healthcheck endpoint
2829

2930
### Enhancements
3031

cmd/mytoken-server/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/oidc-mytoken/server/internal/oidc/oidcfed"
2121
provider2 "github.com/oidc-mytoken/server/internal/oidc/provider"
2222
"github.com/oidc-mytoken/server/internal/server"
23+
"github.com/oidc-mytoken/server/internal/server/healthcheck"
2324
"github.com/oidc-mytoken/server/internal/server/routes"
2425
"github.com/oidc-mytoken/server/internal/utils/cache"
2526
"github.com/oidc-mytoken/server/internal/utils/cookies"
@@ -44,6 +45,7 @@ func main() {
4445
settings.InitSettings()
4546
cookies.Init()
4647
notifier.Init()
48+
healthcheck.Start()
4749
server.Start()
4850
}
4951

config/example-config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ server:
4646
# hostnames including wildcards.
4747
always_allow:
4848
- "127.0.0.1"
49+
healthcheck:
50+
enabled: true
51+
port: 9876
4952

5053
# The database file for ip geolocation. Will be installed by setup to this location.
5154
geo_ip_db_file: "/IP2LOCATION-LITE-DB1.IPV6.BIN"

internal/config/config.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,9 +414,15 @@ type serverConf struct {
414414
TLS tlsConf `yaml:"tls"`
415415
Secure bool `yaml:"-"` // Secure indicates if the connection to the mytoken server is secure. This is
416416
// independent of TLS, e.g. a Proxy can be used.
417-
ProxyHeader string `yaml:"proxy_header"`
418-
Limiter limiterConf `yaml:"request_limits"`
419-
DistributedServers bool `yaml:"distributed_servers"`
417+
ProxyHeader string `yaml:"proxy_header"`
418+
Limiter limiterConf `yaml:"request_limits"`
419+
DistributedServers bool `yaml:"distributed_servers"`
420+
Healthcheck healtcheckConfig `yaml:"healthcheck"`
421+
}
422+
423+
type healtcheckConfig struct {
424+
Enabled bool `yaml:"enabled"`
425+
Port int `yaml:"port"`
420426
}
421427

422428
type limiterConf struct {
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package healthcheck
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
"time"
7+
8+
"github.com/gofiber/fiber/v2"
9+
"github.com/oidc-mytoken/utils/httpclient"
10+
"github.com/oidc-mytoken/utils/utils"
11+
log "github.com/sirupsen/logrus"
12+
"github.com/zachmann/go-oidfed/pkg"
13+
"github.com/zachmann/go-oidfed/pkg/cache"
14+
15+
"github.com/oidc-mytoken/server/internal/config"
16+
"github.com/oidc-mytoken/server/internal/db/dbrepo/versionrepo"
17+
"github.com/oidc-mytoken/server/internal/model/version"
18+
"github.com/oidc-mytoken/server/internal/server/routes"
19+
)
20+
21+
// Start starts the healthcheck endpoint on the configured port if enabled
22+
func Start() {
23+
if !config.Get().Server.Healthcheck.Enabled {
24+
return
25+
}
26+
httpServer := fiber.New()
27+
httpServer.Get(
28+
"", handleHealthCheck,
29+
)
30+
addr := fmt.Sprintf(":%d", config.Get().Server.Healthcheck.Port)
31+
log.Infof("Healthcheck endpoint started on %s", addr)
32+
go func() {
33+
log.WithError(httpServer.Listen(addr)).Fatal()
34+
}()
35+
}
36+
37+
type status struct {
38+
Healthy bool `json:"healthy"`
39+
Operational bool `json:"operational"`
40+
Components componentsStatus `json:"components"`
41+
Version string `json:"version"`
42+
Timestamp pkg.Unixtime `json:"timestamp"`
43+
}
44+
45+
type componentsStatus struct {
46+
ServerUp bool `json:"server_up"`
47+
ServerReachable bool `json:"server_reachable"`
48+
Database bool `json:"database_up"`
49+
Cache bool `json:"cache_up"`
50+
}
51+
52+
func (c componentsStatus) healthy() bool {
53+
return c.ServerUp && c.ServerReachable && c.Database && c.Cache
54+
}
55+
func (c componentsStatus) operational() bool {
56+
return c.ServerUp && c.ServerReachable && c.Database
57+
}
58+
59+
func handleHealthCheck(ctx *fiber.Ctx) error {
60+
state := healthcheck()
61+
if !state.Operational {
62+
ctx.Status(fiber.StatusServiceUnavailable)
63+
}
64+
return ctx.JSON(state)
65+
}
66+
67+
func healthcheck() status {
68+
components := componentsStatus{
69+
ServerUp: true,
70+
ServerReachable: checkServer(),
71+
Database: checkDB(),
72+
Cache: checkCache(),
73+
}
74+
return status{
75+
Healthy: components.healthy(),
76+
Operational: components.operational(),
77+
Components: components,
78+
Version: version.VERSION,
79+
Timestamp: pkg.Unixtime{Time: time.Now()},
80+
}
81+
}
82+
83+
func checkServer() bool {
84+
_, err := httpclient.Do().R().Get(routes.ConfigEndpoint)
85+
if err != nil {
86+
log.WithError(err).WithField("healthcheck", "server_reachable").Error("error server healthcheck")
87+
return false
88+
}
89+
return true
90+
}
91+
92+
func checkDB() bool {
93+
_, err := versionrepo.GetVersionState(log.StandardLogger(), nil)
94+
if err != nil {
95+
log.WithError(err).WithField("healthcheck", "db").Error("error db healthcheck")
96+
return false
97+
}
98+
return true
99+
}
100+
101+
var cacheMutex sync.Mutex
102+
103+
func checkCache() bool {
104+
cacheMutex.Lock()
105+
defer cacheMutex.Unlock()
106+
k := "healthcheck"
107+
v := utils.RandASCIIString(64)
108+
if err := cache.Set(k, v, time.Second); err != nil {
109+
log.WithError(err).WithField("healthcheck", "cache").Error("error caching healthcheck")
110+
return false
111+
}
112+
var cached string
113+
set, err := cache.Get(k, &cached)
114+
if err != nil {
115+
log.WithError(err).WithField("healthcheck", "cache").
116+
Error("error obtaining cached healthcheck")
117+
return false
118+
}
119+
if !set {
120+
log.WithField("healthcheck", "cache").Error("cached healthcheck not found")
121+
return false
122+
}
123+
if cached != v {
124+
log.WithField("healthcheck", "cache").
125+
WithField("cached", v).
126+
WithField("obtained", cached).
127+
Error("cached value does not match")
128+
return false
129+
}
130+
return true
131+
}

internal/server/routes/routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var (
1818
CalendarDownloadEndpoint string
1919
ActionsEndpoint string
2020
NotificationManagementEndpoint string
21+
ConfigEndpoint string
2122
)
2223

2324
// Init initializes the authcode component
@@ -31,6 +32,7 @@ func Init() {
3132
config.Get().IssuerURL,
3233
generalPaths.NotificationManagementEndpoint,
3334
)
35+
ConfigEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.ConfigurationEndpoint)
3436
}
3537

3638
// ActionsURL builds an action url from a pkg.ActionInfo

0 commit comments

Comments
 (0)