Initial cloud-services repo - gateway service + pkg modules
This commit is contained in:
22
pkg/health/clickhouse.go
Normal file
22
pkg/health/clickhouse.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type GetClickhouseConsumerFunc func() (ClickhouseConnCheckInterface, error)
|
||||
|
||||
func NewClickhouseCheck(fn GetClickhouseConsumerFunc) CheckFunc {
|
||||
return func(ctx context.Context) error {
|
||||
conn, err := fn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return conn.Ping(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
type ClickhouseConnCheckInterface interface {
|
||||
Ping(ctx context.Context) error
|
||||
}
|
||||
33
pkg/health/clickhouse_test.go
Normal file
33
pkg/health/clickhouse_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package health_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/health"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestClickhouseCheck(t *testing.T) {
|
||||
mock := MockClickhouseDB{}
|
||||
fn := func() (health.ClickhouseConnCheckInterface, error) {
|
||||
return &mock, nil
|
||||
}
|
||||
check := health.NewClickhouseCheck(fn)
|
||||
|
||||
err := check(context.Background())
|
||||
testhelper.NoError(t, "No error", err)
|
||||
|
||||
mock.Error = errors.New("ping error")
|
||||
err = check(context.Background())
|
||||
testhelper.Error(t, "Ping error", err)
|
||||
}
|
||||
|
||||
type MockClickhouseDB struct {
|
||||
Error error
|
||||
}
|
||||
|
||||
func (m *MockClickhouseDB) Ping(ctx context.Context) error {
|
||||
return m.Error
|
||||
}
|
||||
18
pkg/health/goroutines.go
Normal file
18
pkg/health/goroutines.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func NewGoRoutinesCheck(threshold int) CheckFunc {
|
||||
return func(ctx context.Context) error {
|
||||
count := runtime.NumGoroutine()
|
||||
if count > threshold {
|
||||
return errors.Errorf("too many goroutines (%d > %d)", count, threshold)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
19
pkg/health/goroutines_test.go
Normal file
19
pkg/health/goroutines_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package health_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/health"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
func TestGoRoutinesCheck(t *testing.T) {
|
||||
check := health.NewGoRoutinesCheck(100)
|
||||
err := check(context.Background())
|
||||
testhelper.NoError(t, "no error", err)
|
||||
|
||||
check = health.NewGoRoutinesCheck(0)
|
||||
err = check(context.Background())
|
||||
testhelper.Error(t, "has error", err)
|
||||
}
|
||||
298
pkg/health/health.go
Normal file
298
pkg/health/health.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Based on https://github.com/hellofresh/health-go
|
||||
|
||||
// Status type represents health status
|
||||
type Status string
|
||||
|
||||
// Possible health statuses
|
||||
const (
|
||||
StatusOK Status = "OK"
|
||||
StatusPartiallyAvailable Status = "partially available"
|
||||
StatusUnavailable Status = "unavailable"
|
||||
StatusTimeout Status = "timeout"
|
||||
)
|
||||
|
||||
type (
|
||||
// CheckFunc is the func which executes the check.
|
||||
CheckFunc func(context.Context) error
|
||||
|
||||
// InfoFunc is the func which executes to return check info
|
||||
InfoFunc func(system *System)
|
||||
|
||||
// Config carries the parameters to run the check.
|
||||
Config struct {
|
||||
// Name is the name of the resource to be checked.
|
||||
Name string
|
||||
// Timeout is the timeout defined for every check.
|
||||
Timeout time.Duration
|
||||
// SkipOnErr if set to true, it will retrieve StatusOK providing the error message from the failed resource.
|
||||
SkipOnErr bool
|
||||
// Check is the func which executes the check.
|
||||
Check CheckFunc
|
||||
// Info func sets System information
|
||||
Info InfoFunc
|
||||
// If Vital is set to true, it means that the service won't work without this resource.
|
||||
Vital bool
|
||||
}
|
||||
|
||||
// Check represents the health check response.
|
||||
Check struct {
|
||||
// Status is the check status.
|
||||
Status Status `json:"status"`
|
||||
// Timestamp is the time in which the check occurred.
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
// Failures holds the failed checks along with their messages.
|
||||
Failures map[string]string `json:"failures,omitempty"`
|
||||
// System holds information of the go process.
|
||||
System *System `json:"system"`
|
||||
}
|
||||
|
||||
// System runtime variables about the go process.
|
||||
System struct {
|
||||
// Version is the go version.
|
||||
Version string `json:"version"`
|
||||
// GoroutinesCount is the number of the current goroutines.
|
||||
GoroutinesCount int `json:"goroutines_count"`
|
||||
// TotalAllocBytes is the total bytes allocated.
|
||||
TotalAllocBytes int `json:"total_alloc_bytes"`
|
||||
// HeapObjectsCount is the number of objects in the go heap.
|
||||
HeapObjectsCount int `json:"heap_objects_count"`
|
||||
// TotalAllocBytes is the bytes allocated and not yet freed.
|
||||
AllocBytes int `json:"alloc_bytes"`
|
||||
// RedisPoolCount is the current Redis connection pool count
|
||||
RedisStats *redis.PoolStats `json:"redis_stats,omitempty"`
|
||||
}
|
||||
|
||||
// Health is the health-checks container
|
||||
Health struct {
|
||||
mu sync.Mutex
|
||||
checks map[string]Config
|
||||
}
|
||||
|
||||
checkResponse struct {
|
||||
config Config
|
||||
err error
|
||||
}
|
||||
|
||||
filterChecks func(checks map[string]Config) map[string]Config
|
||||
)
|
||||
|
||||
// New instantiates and build new health check container
|
||||
func New(opts ...Option) (*Health, error) {
|
||||
h := &Health{
|
||||
checks: make(map[string]Config),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
if err := o(h); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// Register registers a check config to be performed.
|
||||
func (h *Health) Register(c Config) error {
|
||||
if c.Timeout == 0 {
|
||||
c.Timeout = time.Second * 1
|
||||
}
|
||||
|
||||
if c.Name == "" {
|
||||
return errors.New("health check must have a name to be registered")
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if _, ok := h.checks[c.Name]; ok {
|
||||
return fmt.Errorf("health check %q is already registered", c.Name)
|
||||
}
|
||||
|
||||
h.checks[c.Name] = c
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadinessHandler returns an HTTP handler (http.HandlerFunc).
|
||||
func (h *Health) ReadinessHandler() http.Handler {
|
||||
return http.HandlerFunc(h.ReadinessFunc)
|
||||
}
|
||||
|
||||
// LivenessHandler returns an HTTP handler (http.HandlerFunc).
|
||||
func (h *Health) LivenessHandler() http.Handler {
|
||||
return http.HandlerFunc(h.LivenessFunc)
|
||||
}
|
||||
|
||||
// LivenessFunc is the HTTP handler function.
|
||||
func (h *Health) LivenessFunc(w http.ResponseWriter, r *http.Request) {
|
||||
c := h.Measure(r.Context(), getLivenessCheck)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
data, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
code := http.StatusOK
|
||||
if c.Status == StatusUnavailable {
|
||||
code = http.StatusServiceUnavailable
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
// ReadinessFunc is the HTTP handler function.
|
||||
func (h *Health) ReadinessFunc(w http.ResponseWriter, r *http.Request) {
|
||||
c := h.Measure(r.Context(), getReadinessCheck)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
data, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
code := http.StatusOK
|
||||
if c.Status == StatusUnavailable {
|
||||
code = http.StatusServiceUnavailable
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (h *Health) info(system *System) {
|
||||
for _, c := range h.checks {
|
||||
if c.Info != nil {
|
||||
c.Info(system)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Measure runs all the registered health checks and returns summary status
|
||||
func (h *Health) Measure(ctx context.Context, getChecks filterChecks) Check {
|
||||
errTimeout := errors.New("timeout error")
|
||||
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
checksList := getChecks(h.checks)
|
||||
total := len(checksList)
|
||||
|
||||
checkRespChan := make(chan checkResponse, total)
|
||||
|
||||
var wgRes sync.WaitGroup
|
||||
wgRes.Add(total)
|
||||
|
||||
go func() {
|
||||
wgRes.Wait()
|
||||
close(checkRespChan)
|
||||
}()
|
||||
|
||||
for _, c := range checksList {
|
||||
go func(ctx context.Context, c Config, respChan chan<- checkResponse) {
|
||||
defer wgRes.Done()
|
||||
|
||||
locResp := make(chan error)
|
||||
go func(ctx context.Context, locResp chan<- error) {
|
||||
defer close(locResp)
|
||||
locResp <- c.Check(ctx)
|
||||
}(ctx, locResp)
|
||||
|
||||
select {
|
||||
case <-time.After(c.Timeout):
|
||||
respChan <- checkResponse{config: c, err: errTimeout}
|
||||
case err := <-locResp:
|
||||
respChan <- checkResponse{config: c, err: err}
|
||||
}
|
||||
}(ctx, c, checkRespChan)
|
||||
}
|
||||
|
||||
status := StatusOK
|
||||
checks := make(map[string]string)
|
||||
for resp := range checkRespChan {
|
||||
if resp.err == errTimeout {
|
||||
checks[resp.config.Name] = string(StatusTimeout)
|
||||
status = getAvailability(status, resp.config)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.err != nil {
|
||||
checks[resp.config.Name] = resp.err.Error()
|
||||
status = getAvailability(status, resp.config)
|
||||
logger.Error().Err(errors.WithMessage(resp.err, resp.config.Name)).Send()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
checks[resp.config.Name] = string(StatusOK)
|
||||
}
|
||||
|
||||
system := newSystemMetrics()
|
||||
h.info(&system)
|
||||
|
||||
return newCheck(status, checks, &system)
|
||||
}
|
||||
|
||||
func getReadinessCheck(checks map[string]Config) map[string]Config {
|
||||
return checks
|
||||
}
|
||||
|
||||
func getLivenessCheck(checks map[string]Config) map[string]Config {
|
||||
rez := make(map[string]Config)
|
||||
for key, conf := range checks {
|
||||
if conf.Vital {
|
||||
rez[key] = conf
|
||||
}
|
||||
}
|
||||
|
||||
return rez
|
||||
}
|
||||
|
||||
func newCheck(s Status, failures map[string]string, system *System) Check {
|
||||
return Check{
|
||||
Status: s,
|
||||
Timestamp: time.Now(),
|
||||
Failures: failures,
|
||||
System: system,
|
||||
}
|
||||
}
|
||||
|
||||
func newSystemMetrics() System {
|
||||
s := runtime.MemStats{}
|
||||
runtime.ReadMemStats(&s)
|
||||
return System{
|
||||
Version: runtime.Version(),
|
||||
GoroutinesCount: runtime.NumGoroutine(),
|
||||
TotalAllocBytes: int(s.TotalAlloc),
|
||||
HeapObjectsCount: int(s.HeapObjects),
|
||||
AllocBytes: int(s.Alloc),
|
||||
}
|
||||
}
|
||||
|
||||
func getAvailability(s Status, c Config) Status {
|
||||
if c.SkipOnErr && s != StatusUnavailable {
|
||||
return StatusPartiallyAvailable
|
||||
}
|
||||
|
||||
return StatusUnavailable
|
||||
}
|
||||
166
pkg/health/health_test.go
Normal file
166
pkg/health/health_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
const (
|
||||
checkErr = "failed during RabbitMQ health check"
|
||||
)
|
||||
|
||||
func TestRegisterWithNoName(t *testing.T) {
|
||||
h, err := New()
|
||||
testhelper.NoError(t, "New", err)
|
||||
|
||||
err = h.Register(Config{
|
||||
Name: "",
|
||||
Check: func(context.Context) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
testhelper.Error(t, "Register", err)
|
||||
}
|
||||
|
||||
func TestDoubleRegister(t *testing.T) {
|
||||
h, err := New()
|
||||
testhelper.NoError(t, "New", err)
|
||||
|
||||
healthCheckName := "health-check"
|
||||
|
||||
conf := Config{
|
||||
Name: healthCheckName,
|
||||
Check: func(context.Context) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err = h.Register(conf)
|
||||
testhelper.NoError(t, "Register", err)
|
||||
|
||||
err = h.Register(conf)
|
||||
testhelper.Error(t, "the second registration of a health check config should return an error, but did not", err)
|
||||
|
||||
err = h.Register(Config{
|
||||
Name: healthCheckName,
|
||||
Check: func(context.Context) error {
|
||||
return errors.New("health checks registered")
|
||||
},
|
||||
})
|
||||
testhelper.Error(t, "registration with same name, but different details should still return an error, but did not", err)
|
||||
}
|
||||
|
||||
func TestReadinessHandler(t *testing.T) {
|
||||
h, err := New()
|
||||
testhelper.NoError(t, "New", err)
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost/readiness", nil)
|
||||
testhelper.NoError(t, "NewRequest", err)
|
||||
|
||||
err = h.Register(Config{
|
||||
Name: "rabbitmq",
|
||||
SkipOnErr: true,
|
||||
Check: func(context.Context) error { return errors.New(checkErr) },
|
||||
})
|
||||
testhelper.NoError(t, "Register rabbitmq", err)
|
||||
|
||||
err = h.Register(Config{
|
||||
Name: "mongodb",
|
||||
Check: func(context.Context) error { return nil },
|
||||
})
|
||||
testhelper.NoError(t, "Register mongodb", err)
|
||||
|
||||
err = h.Register(Config{
|
||||
Name: "snail-service",
|
||||
SkipOnErr: true,
|
||||
Timeout: time.Second * 1,
|
||||
Check: func(context.Context) error {
|
||||
time.Sleep(time.Second * 2)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
testhelper.NoError(t, "Register snail-service", err)
|
||||
|
||||
handler := h.ReadinessHandler()
|
||||
handler.ServeHTTP(res, req)
|
||||
|
||||
testhelper.Equal(t, "status handler returned wrong status code", http.StatusOK, res.Code)
|
||||
|
||||
body := make(map[string]interface{})
|
||||
err = json.NewDecoder(res.Body).Decode(&body)
|
||||
testhelper.NoError(t, "NewDecoder", err)
|
||||
|
||||
testhelper.Equal(t, "body returned wrong status", string(StatusPartiallyAvailable), body["status"])
|
||||
|
||||
failure, ok := body["failures"]
|
||||
testhelper.True(t, "body returned nil failures field", ok)
|
||||
|
||||
f, ok := failure.(map[string]interface{})
|
||||
testhelper.True(t, "body returned nil failures.rabbitmq field", ok)
|
||||
|
||||
testhelper.Equal(t, "body returned wrong status for rabbitmq", checkErr, f["rabbitmq"])
|
||||
testhelper.Equal(t, "body returned wrong status for snail-service", string(StatusTimeout), f["snail-service"])
|
||||
}
|
||||
|
||||
func TestLivenessHandler(t *testing.T) {
|
||||
h, err := New()
|
||||
testhelper.NoError(t, "New", err)
|
||||
|
||||
res := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost/liveness", nil)
|
||||
testhelper.NoError(t, "NewRequest", err)
|
||||
|
||||
err = h.Register(Config{
|
||||
Name: "rabbitmq",
|
||||
SkipOnErr: true,
|
||||
Check: func(context.Context) error { return errors.New(checkErr) },
|
||||
Vital: true,
|
||||
})
|
||||
testhelper.NoError(t, "Register rabbitmq", err)
|
||||
|
||||
err = h.Register(Config{
|
||||
Name: "mongodb",
|
||||
Check: func(context.Context) error { return nil },
|
||||
})
|
||||
testhelper.NoError(t, "Register mongodb", err)
|
||||
|
||||
err = h.Register(Config{
|
||||
Name: "snail-service",
|
||||
SkipOnErr: true,
|
||||
Timeout: time.Second * 1,
|
||||
Check: func(context.Context) error {
|
||||
time.Sleep(time.Second * 2)
|
||||
return nil
|
||||
},
|
||||
Vital: true,
|
||||
})
|
||||
testhelper.NoError(t, "Register snail-service", err)
|
||||
|
||||
handler := h.LivenessHandler()
|
||||
handler.ServeHTTP(res, req)
|
||||
|
||||
testhelper.Equal(t, "status handler returned wrong status code", http.StatusOK, res.Code)
|
||||
|
||||
body := make(map[string]interface{})
|
||||
err = json.NewDecoder(res.Body).Decode(&body)
|
||||
testhelper.NoError(t, "NewDecoder", err)
|
||||
|
||||
testhelper.Equal(t, "body returned wrong status", string(StatusPartiallyAvailable), body["status"])
|
||||
|
||||
failure, ok := body["failures"]
|
||||
testhelper.True(t, "body returned nil failures field", ok)
|
||||
|
||||
f, ok := failure.(map[string]interface{})
|
||||
testhelper.True(t, "body returned nil failures.rabbitmq field", ok)
|
||||
|
||||
testhelper.Equal(t, "body returned wrong status for rabbitmq", checkErr, f["rabbitmq"])
|
||||
testhelper.Equal(t, "body returned wrong status for snail-service", string(StatusTimeout), f["snail-service"])
|
||||
}
|
||||
56
pkg/health/http.go
Normal file
56
pkg/health/http.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultRequestTimeout = 5 * time.Second
|
||||
|
||||
// Config is the HTTP checker configuration settings container.
|
||||
type HTTPConfig struct {
|
||||
// URL is the remote service health check URL.
|
||||
URL string
|
||||
// RequestTimeout is the duration that health check will try to consume published test message.
|
||||
// If not set - 5 seconds
|
||||
RequestTimeout time.Duration
|
||||
}
|
||||
|
||||
// New creates new HTTP service health check that verifies the following:
|
||||
// - connection establishing
|
||||
// - getting response status from defined URL
|
||||
// - verifying that status code is less than 500
|
||||
func NewHTTPCheck(config HTTPConfig) CheckFunc {
|
||||
if config.RequestTimeout == 0 {
|
||||
config.RequestTimeout = defaultRequestTimeout
|
||||
}
|
||||
|
||||
return func(ctx context.Context) error {
|
||||
req, err := http.NewRequest(http.MethodGet, config.URL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating the request for the health check failed: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, config.RequestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Inform remote service to close the connection after the transaction is complete
|
||||
req.Header.Set("Connection", "close")
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("making the request for the health check failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode >= http.StatusInternalServerError {
|
||||
return errors.New("remote service is not available at the moment")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
20
pkg/health/http_test.go
Normal file
20
pkg/health/http_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package health_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/health"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
func TestHTTPCheck(t *testing.T) {
|
||||
check := health.NewHTTPCheck(health.HTTPConfig{
|
||||
URL: "http://0.0.0.0:9876",
|
||||
RequestTimeout: time.Second,
|
||||
})
|
||||
|
||||
err := check(context.Background())
|
||||
testhelper.Error(t, "has error", err)
|
||||
}
|
||||
41
pkg/health/kafka.go
Normal file
41
pkg/health/kafka.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type GetKafkaConsumerFunc func() (KafkaConnCheckInterface, error)
|
||||
|
||||
func NewKafkaCheck(fn GetKafkaConsumerFunc) CheckFunc {
|
||||
return func(ctx context.Context) error {
|
||||
conn, err := fn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return conn.Check(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
type KafkaConnCheckInterface interface {
|
||||
Check(ctx context.Context) error
|
||||
}
|
||||
|
||||
type GetMultiKafkaConsumerFunc func() ([]KafkaConnCheckInterface, error)
|
||||
|
||||
func NewKafkaMultiCheck(fn GetMultiKafkaConsumerFunc) CheckFunc {
|
||||
return func(ctx context.Context) error {
|
||||
conns, err := fn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, connection := range conns {
|
||||
err = connection.Check(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
22
pkg/health/mongodb.go
Normal file
22
pkg/health/mongodb.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type GetMongoClientFunc func() (MongoConnCheckInterface, error)
|
||||
|
||||
func NewMongoDBCheck(fn GetMongoClientFunc) CheckFunc {
|
||||
return func(ctx context.Context) error {
|
||||
conn, err := fn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return conn.Ping(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
type MongoConnCheckInterface interface {
|
||||
Ping(ctx context.Context) error
|
||||
}
|
||||
33
pkg/health/mongodb_test.go
Normal file
33
pkg/health/mongodb_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package health_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/health"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestMongoDBCheck(t *testing.T) {
|
||||
mock := MockMongoDB{}
|
||||
fn := func() (health.MongoConnCheckInterface, error) {
|
||||
return &mock, nil
|
||||
}
|
||||
check := health.NewMongoDBCheck(fn)
|
||||
|
||||
err := check(context.Background())
|
||||
testhelper.NoError(t, "No error", err)
|
||||
|
||||
mock.Error = errors.New("ping error")
|
||||
err = check(context.Background())
|
||||
testhelper.Error(t, "Ping error", err)
|
||||
}
|
||||
|
||||
type MockMongoDB struct {
|
||||
Error error
|
||||
}
|
||||
|
||||
func (m *MockMongoDB) Ping(ctx context.Context) error {
|
||||
return m.Error
|
||||
}
|
||||
21
pkg/health/options.go
Normal file
21
pkg/health/options.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Option is the health-container options type
|
||||
type Option func(*Health) error
|
||||
|
||||
// WithChecks adds checks to newly instantiated health-container
|
||||
func WithChecks(checks ...Config) Option {
|
||||
return func(h *Health) error {
|
||||
for _, c := range checks {
|
||||
if err := h.Register(c); err != nil {
|
||||
return fmt.Errorf("could not register check %q: %w", c.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
28
pkg/health/options_test.go
Normal file
28
pkg/health/options_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
func TestWithChecks(t *testing.T) {
|
||||
h1, err := New()
|
||||
testhelper.NoError(t, "New", err)
|
||||
testhelper.Len(t, "New", h1.checks, 0)
|
||||
|
||||
h2, err := New(WithChecks(Config{
|
||||
Name: "foo",
|
||||
}, Config{
|
||||
Name: "bar",
|
||||
}))
|
||||
testhelper.NoError(t, "New WithChecks", err)
|
||||
testhelper.Len(t, "New WithChecks", h2.checks, 2)
|
||||
|
||||
_, err = New(WithChecks(Config{
|
||||
Name: "foo",
|
||||
}, Config{
|
||||
Name: "foo",
|
||||
}))
|
||||
testhelper.Error(t, "New WithChecks", err)
|
||||
}
|
||||
19
pkg/health/postgres.go
Normal file
19
pkg/health/postgres.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-pg/pg/v10/orm"
|
||||
)
|
||||
|
||||
type CheckDBInterface interface {
|
||||
Ping(ctx context.Context) error
|
||||
Exec(query interface{}, params ...interface{}) (res orm.Result, err error)
|
||||
}
|
||||
|
||||
func NewPostgresCheck(conn CheckDBInterface) CheckFunc {
|
||||
return func(ctx context.Context) error {
|
||||
err := conn.Ping(ctx)
|
||||
return err
|
||||
}
|
||||
}
|
||||
38
pkg/health/postgres_test.go
Normal file
38
pkg/health/postgres_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package health_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/health"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
|
||||
"github.com/go-pg/pg/v10/orm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestPostgresCheck(t *testing.T) {
|
||||
mock := MockPostgres{}
|
||||
check := health.NewPostgresCheck(&mock)
|
||||
|
||||
err := check(context.Background())
|
||||
testhelper.NoError(t, "No error", err)
|
||||
|
||||
mock.PingError = errors.New("ping error")
|
||||
err = check(context.Background())
|
||||
testhelper.Error(t, "Ping error", err)
|
||||
}
|
||||
|
||||
type MockPostgres struct {
|
||||
OrmResult orm.Result
|
||||
PingError error
|
||||
ExecError error
|
||||
}
|
||||
|
||||
func (m *MockPostgres) Ping(ctx context.Context) error {
|
||||
return m.PingError
|
||||
}
|
||||
|
||||
func (m *MockPostgres) Exec(query interface{}, params ...interface{}) (res orm.Result, err error) {
|
||||
return m.OrmResult, m.ExecError
|
||||
}
|
||||
53
pkg/health/redis.go
Normal file
53
pkg/health/redis.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/redisv2"
|
||||
)
|
||||
|
||||
type RedisHealth struct {
|
||||
pool redis.ClientPoolInterface
|
||||
}
|
||||
|
||||
func NewRedisHealth(pool redis.ClientPoolInterface) *RedisHealth {
|
||||
return &RedisHealth{
|
||||
pool: pool,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RedisHealth) Check(ctx context.Context) error {
|
||||
client := h.pool.GetFromPool()
|
||||
defer client.Close()
|
||||
|
||||
return client.Ping()
|
||||
}
|
||||
|
||||
|
||||
func (h *RedisHealth) RedisStatus(system *System) {
|
||||
stats := h.pool.GetPool().Stats()
|
||||
system.RedisStats = &stats
|
||||
}
|
||||
|
||||
type RedisHealthV2 struct {
|
||||
redisClient redisv2.ClientInterface
|
||||
}
|
||||
|
||||
func NewRedisHealthV2(redisClient redisv2.ClientInterface) *RedisHealthV2 {
|
||||
return &RedisHealthV2{
|
||||
redisClient: redisClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RedisHealthV2) Check(ctx context.Context) error {
|
||||
|
||||
|
||||
return h.redisClient.Ping()
|
||||
}
|
||||
|
||||
func (h *RedisHealthV2) RedisStatus(system *System) {
|
||||
// Im not so sure what
|
||||
// stats := h.redisClient.GetClient().PoolStats()
|
||||
system.RedisStats = nil
|
||||
}
|
||||
19
pkg/health/redis_test.go
Normal file
19
pkg/health/redis_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package health_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/health"
|
||||
"fiskerinc.com/modules/redis"
|
||||
"fiskerinc.com/modules/redis/tester"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
func TestRedisCheck(t *testing.T) {
|
||||
redis.MockRedisConnection()
|
||||
health := health.NewRedisHealth(tester.NewMockClientPool())
|
||||
|
||||
err := health.Check(context.Background())
|
||||
testhelper.NoError(t, "No error", err)
|
||||
}
|
||||
34
pkg/health/server.go
Normal file
34
pkg/health/server.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/utils/envtool"
|
||||
)
|
||||
|
||||
type HealthCheckServer struct {
|
||||
}
|
||||
|
||||
func (h *HealthCheckServer) Serve(configs []Config) error {
|
||||
port := envtool.GetEnv("HEALTHCHECK_PORT", "11011")
|
||||
server, err := New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
err = server.Register(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info().Msgf("Health check listening on http://0.0.0.0:%s", port)
|
||||
http.Handle("/readiness", server.ReadinessHandler())
|
||||
http.Handle("/liveness", server.LivenessHandler())
|
||||
logger.Fatal().Err(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)).Send()
|
||||
|
||||
return nil
|
||||
}
|
||||
12
pkg/health/server_test.go
Normal file
12
pkg/health/server_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package health_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/health"
|
||||
)
|
||||
|
||||
func TestHealthCheckServer(t *testing.T) {
|
||||
server := health.HealthCheckServer{}
|
||||
go server.Serve([]health.Config{})
|
||||
}
|
||||
23
pkg/health/tmobile.go
Normal file
23
pkg/health/tmobile.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type GetTmobileConnCheckFunc func() (TmobileConnCheckInterface, error)
|
||||
|
||||
func NewTmobileCheck(fn GetTmobileConnCheckFunc) CheckFunc {
|
||||
return func(ctx context.Context) error {
|
||||
client, err := fn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = client.Ping(ctx)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type TmobileConnCheckInterface interface {
|
||||
Ping(ctx context.Context) error
|
||||
}
|
||||
33
pkg/health/tmobile_test.go
Normal file
33
pkg/health/tmobile_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package health_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/health"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestTmobileCheck(t *testing.T) {
|
||||
mock := MockTmobile{}
|
||||
fn := func() (health.TmobileConnCheckInterface, error) {
|
||||
return &mock, nil
|
||||
}
|
||||
check := health.NewTmobileCheck(fn)
|
||||
|
||||
err := check(context.Background())
|
||||
testhelper.NoError(t, "No error", err)
|
||||
|
||||
mock.PingError = errors.New("ping error")
|
||||
err = check(context.Background())
|
||||
testhelper.Error(t, "Ping error", err)
|
||||
}
|
||||
|
||||
type MockTmobile struct {
|
||||
PingError error
|
||||
}
|
||||
|
||||
func (m *MockTmobile) Ping(ctx context.Context) error {
|
||||
return m.PingError
|
||||
}
|
||||
Reference in New Issue
Block a user