add healthcheck service

This commit is contained in:
2024-08-17 01:44:20 +03:00
parent 25762cbae8
commit f635d4ca5b
11 changed files with 422 additions and 15 deletions

View File

@ -5,6 +5,7 @@ import (
"net/http"
"net/http/pprof"
"git.loyso.art/frx/devsim/internal/probe"
"git.loyso.art/frx/devsim/internal/store"
)
@ -36,6 +37,11 @@ func (s *handlersBuilder) MountProfileHandlers() {
s.mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
}
func (s *handlersBuilder) MountProbeHandlers(r probe.Reporter) {
s.mux.HandleFunc("/health", livenessHandler(r))
s.mux.HandleFunc("/ready", readinessHandler(r))
}
func (s *handlersBuilder) Build() http.Handler {
return s.mux
}

View File

@ -0,0 +1,55 @@
package http
import (
"net/http"
"git.loyso.art/frx/devsim/internal/probe"
)
// ListenAndServe runs server to accept incoming connections. This function blocks on
// handling connections.
func livenessHandler(reporter probe.Reporter) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
switch reporter.ReportLiveness() {
case probe.LivenessOk:
w.WriteHeader(http.StatusOK)
case probe.LivenessTimeout:
w.WriteHeader(http.StatusInternalServerError)
case probe.LivenessUnknown:
w.WriteHeader(http.StatusOK)
}
})
}
func readinessHandler(reporter probe.Reporter) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var status string
var code int
switch reporter.ReportReadiness() {
case probe.ReadinessOk:
status = "ok"
code = http.StatusOK
case probe.ReadinessFailed:
status = "failed"
code = http.StatusInternalServerError
case probe.ReadinessNotReady:
status = "not-ready"
code = http.StatusOK
case probe.ReadinessUnknown:
status = "unknown"
code = http.StatusOK
}
w.Header().Set("X-Readiness-Status", status)
w.WriteHeader(code)
})
}

View File

@ -39,6 +39,7 @@ func New(addr string) (*client, error) {
KeepAlive: time.Second * 30,
}).DialContext,
MaxIdleConns: 10,
MaxConnsPerHost: 100,
IdleConnTimeout: time.Second * 90,
TLSHandshakeTimeout: time.Second * 5,
ExpectContinueTimeout: time.Second * 1,

View File

@ -0,0 +1,13 @@
package probe
// Liveness reports the application is alive.
type Liveness int8
const (
// LivenessUnknown reports nothing.
LivenessUnknown Liveness = iota
// LivenessOk reports service is alive.
LivenessOk
// LivenessTimeout reports service was unable to answer at a time.
LivenessTimeout
)

View File

@ -0,0 +1,30 @@
package probe
// Readiness reports compoent's readiness.
type Readiness int8
const (
// ReadinessUnknown means rediness was unset.
ReadinessUnknown Readiness = iota
// ReadinessNotReady reports provided component is not ready.
ReadinessNotReady
// ReadinessFailed reports there were a problem with component.
ReadinessFailed
// ReadinessOk reports the component is ready to work.
ReadinessOk
)
type ReadinessAggregate []Readiness
func (a ReadinessAggregate) Status() Readiness {
for _, item := range a {
switch item {
case ReadinessOk, ReadinessUnknown:
continue
}
return item
}
return ReadinessOk
}

119
internal/probe/reporter.go Normal file
View File

@ -0,0 +1,119 @@
package probe
import (
"context"
"sync"
"sync/atomic"
"time"
)
type (
ReadinessFunc func() Readiness
LivenessFunc func(context.Context) Liveness
ReadinessAggregateFuncs []ReadinessFunc
)
func (fs ReadinessAggregateFuncs) check() (a ReadinessAggregate) {
a = make(ReadinessAggregate, 0, len(fs))
for _, f := range fs {
a = append(a, f())
}
return a
}
type Reporter interface {
ReportReadiness() Readiness
ReportLiveness() Liveness
RegisterReadiness(ReadinessFunc)
RegisterLiveness(LivenessFunc)
}
func NewReporter(livenessTimeout time.Duration) *reporter {
return &reporter{
livenessTimeout: livenessTimeout,
}
}
type reporter struct {
readinessComponents ReadinessAggregateFuncs
livenessComponents []LivenessFunc
livenessTimeout time.Duration
mu sync.Mutex
livemu sync.Mutex
readmu sync.Mutex
}
func (r *reporter) ReportReadiness() Readiness {
r.readmu.Lock()
defer r.readmu.Unlock()
return r.readinessComponents.check().Status()
}
func (r *reporter) ReportLiveness() (out Liveness) {
r.livemu.Lock()
defer r.livemu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), r.livenessTimeout)
defer cancel()
for _, f := range r.livenessComponents {
status := f(ctx)
if status == LivenessTimeout {
return status
}
}
return LivenessOk
}
func (r *reporter) RegisterReadiness(f ReadinessFunc) {
r.readmu.Lock()
defer r.readmu.Unlock()
r.readinessComponents = append(r.readinessComponents, f)
}
func (r *reporter) RegisterLiveness(f LivenessFunc) {
r.livemu.Lock()
defer r.livemu.Unlock()
r.livenessComponents = append(r.livenessComponents, f)
}
func SimpleReadinessSwitcher() (f ReadinessFunc, toggle func(newStatus Readiness)) {
var status atomic.Int32
f = func() Readiness {
return Readiness(status.Load())
}
toggle = func(newStatus Readiness) {
status.Store(int32(newStatus))
}
return f, toggle
}
func SimpleLivenessSwitcher() (f LivenessFunc, toggle func()) {
var liveness atomic.Bool
f = func(context.Context) Liveness {
if liveness.Load() {
return LivenessOk
}
return LivenessUnknown
}
toggle = func() {
liveness.Store(true)
}
return f, toggle
}

View File

@ -0,0 +1,40 @@
package memory
import (
"context"
"sync"
"git.loyso.art/frx/devsim/internal/entities"
)
type store struct {
stats map[entities.DeviceID]entities.DeviceStatistics
mu sync.Mutex
}
func NewStore() *store {
return &store{
stats: make(map[entities.DeviceID]entities.DeviceStatistics),
}
}
func (s *store) List(ctx context.Context) ([]entities.DeviceStatistics, error) {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]entities.DeviceStatistics, 0, len(s.stats))
for _, s := range s.stats {
out = append(out, s)
}
return out, nil
}
func (s *store) Upsert(ctx context.Context, stats entities.DeviceStatistics) error {
s.mu.Lock()
defer s.mu.Unlock()
s.stats[stats.ID] = stats
return nil
}