add healthcheck service
This commit is contained in:
@ -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
|
||||
}
|
||||
|
||||
55
internal/api/http/healthhandlers.go
Normal file
55
internal/api/http/healthhandlers.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@ -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,
|
||||
|
||||
13
internal/probe/liveness.go
Normal file
13
internal/probe/liveness.go
Normal 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
|
||||
)
|
||||
30
internal/probe/readiness.go
Normal file
30
internal/probe/readiness.go
Normal 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
119
internal/probe/reporter.go
Normal 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
|
||||
}
|
||||
40
internal/store/memory/store.go
Normal file
40
internal/store/memory/store.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user