Skip to content

Commit

Permalink
Add GET /_health endpoint
Browse files Browse the repository at this point in the history
Return 500 if not ok and 200 if ok.
The response will contains a map of string/string for each checks.
The value is 'OK' if the check passed, otherwise, is is the error.
  • Loading branch information
gfyrag committed Mar 15, 2022
1 parent 0b5a1c2 commit 28ff9a8
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 25 deletions.
4 changes: 2 additions & 2 deletions cmd/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cmd

import (
"context"
"github.com/numary/ledger/pkg/ledgertesting"
"github.com/numary/ledger/internal/pgtesting"
"github.com/numary/ledger/pkg/opentelemetry/opentelemetrytraces"
"github.com/numary/ledger/pkg/storage"
"github.com/numary/ledger/pkg/storage/sqlstorage"
Expand All @@ -19,7 +19,7 @@ import (

func TestContainers(t *testing.T) {

pgServer, err := ledgertesting.PostgresServer()
pgServer, err := pgtesting.PostgresServer()
assert.NoError(t, err)
defer pgServer.Close()

Expand Down
4 changes: 2 additions & 2 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cmd
import (
"bytes"
"context"
"github.com/numary/ledger/pkg/ledgertesting"
"github.com/numary/ledger/internal/pgtesting"
"github.com/pborman/uuid"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
Expand All @@ -19,7 +19,7 @@ func TestServer(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
}

pgServer, err := ledgertesting.PostgresServer()
pgServer, err := pgtesting.PostgresServer()
assert.NoError(t, err)
defer pgServer.Close()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ledgertesting
package pgtesting

import (
"context"
Expand Down
18 changes: 11 additions & 7 deletions pkg/api/controllers/controllers.go
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
package controllers

import (
"github.com/numary/ledger/pkg/health"
"go.uber.org/fx"
)

const (
VersionKey = `name:"_apiVersion"`
StorageDriverKey = `name:"_apiStorageDriver"`
LedgerListerKey = `name:"_apiLedgerLister"`
versionKey = `name:"_apiVersion"`
storageDriverKey = `name:"_apiStorageDriver"`
ledgerListerKey = `name:"_apiLedgerLister"`
)

func ProvideVersion(provider interface{}) fx.Option {
return fx.Provide(
fx.Annotate(provider, fx.ResultTags(VersionKey)),
fx.Annotate(provider, fx.ResultTags(versionKey)),
)
}

func ProvideStorageDriver(provider interface{}) fx.Option {
return fx.Provide(
fx.Annotate(provider, fx.ResultTags(StorageDriverKey)),
fx.Annotate(provider, fx.ResultTags(storageDriverKey)),
)
}

func ProvideLedgerLister(provider interface{}) fx.Option {
return fx.Provide(
fx.Annotate(provider, fx.ResultTags(LedgerListerKey)),
fx.Annotate(provider, fx.ResultTags(ledgerListerKey)),
)
}

var Module = fx.Options(
fx.Provide(
fx.Annotate(NewConfigController, fx.ParamTags(VersionKey, StorageDriverKey, LedgerListerKey)),
fx.Annotate(NewConfigController, fx.ParamTags(versionKey, storageDriverKey, ledgerListerKey)),
),
fx.Provide(NewLedgerController),
fx.Provide(NewScriptController),
fx.Provide(NewAccountController),
fx.Provide(NewTransactionController),
fx.Provide(NewMappingController),
fx.Provide(
fx.Annotate(NewHealthController, fx.ParamTags(health.HealthCheckKey)),
),
)
58 changes: 58 additions & 0 deletions pkg/api/controllers/health_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package controllers

import (
"github.com/gin-gonic/gin"
"github.com/numary/ledger/pkg/health"
"net/http"
"sync"
)

type HealthController struct {
Checks []health.NamedCheck
}

func (ctrl *HealthController) Check(c *gin.Context) {
w := sync.WaitGroup{}
w.Add(len(ctrl.Checks))
type R struct {
Check health.NamedCheck
Err error
}
results := make(chan R, len(ctrl.Checks))
for _, ch := range ctrl.Checks {
go func(ch health.NamedCheck) {
defer w.Done()
select {
case <-c.Request.Context().Done():
return
case results <- R{
Check: ch,
Err: ch.Do(c.Request.Context()),
}:
}
}(ch)
}
w.Wait()
close(results)
response := map[string]string{}
hasError := false
for r := range results {
if r.Err != nil {
hasError = true
response[r.Check.Name()] = r.Err.Error()
} else {
response[r.Check.Name()] = "OK"
}
}
status := http.StatusOK
if hasError {
status = http.StatusInternalServerError
}
c.JSON(status, response)
}

func NewHealthController(checks []health.NamedCheck) HealthController {
return HealthController{
Checks: checks,
}
}
96 changes: 96 additions & 0 deletions pkg/api/controllers/health_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package controllers_test

import (
"context"
"encoding/json"
"errors"
"github.com/numary/ledger/pkg/api"
"github.com/numary/ledger/pkg/api/internal"
"github.com/numary/ledger/pkg/health"
"github.com/stretchr/testify/assert"
"go.uber.org/fx"
"net/http"
"net/http/httptest"
"testing"
)

func TestHealthController(t *testing.T) {

type testCase struct {
name string
healthChecksProvider []interface{}
expectedStatus int
expectedResult map[string]string
}

var tests = []testCase{
{
name: "all-ok",
healthChecksProvider: []interface{}{
func() health.NamedCheck {
return health.NewNamedCheck("test1", health.CheckFn(func(ctx context.Context) error {
return nil
}))
},
func() health.NamedCheck {
return health.NewNamedCheck("test2", health.CheckFn(func(ctx context.Context) error {
return nil
}))
},
},
expectedStatus: http.StatusOK,
expectedResult: map[string]string{
"test1": "OK",
"test2": "OK",
},
},
{
name: "one-failing",
healthChecksProvider: []interface{}{
func() health.NamedCheck {
return health.NewNamedCheck("test1", health.CheckFn(func(ctx context.Context) error {
return nil
}))
},
func() health.NamedCheck {
return health.NewNamedCheck("test2", health.CheckFn(func(ctx context.Context) error {
return errors.New("failure")
}))
},
},
expectedStatus: http.StatusInternalServerError,
expectedResult: map[string]string{
"test1": "OK",
"test2": "failure",
},
},
}

for _, tc := range tests {

options := make([]fx.Option, 0)
for _, p := range tc.healthChecksProvider {
options = append(options, health.ProvideHealthCheck(p))
}

internal.RunSubTest(t, tc.name, func(h *api.API) {
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/_health", nil)

h.ServeHTTP(rec, req)

if !assert.Equal(t, tc.expectedStatus, rec.Result().StatusCode) {
return
}
ret := make(map[string]string)
err := json.NewDecoder(rec.Result().Body).Decode(&ret)
if !assert.NoError(t, err) {
return
}
if !assert.Equal(t, tc.expectedResult, ret) {
return
}
}, options...)
}

}
3 changes: 2 additions & 1 deletion pkg/api/controllers/transaction_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"encoding/json"
"github.com/numary/ledger/internal/pgtesting"
"github.com/numary/ledger/pkg/api"
"github.com/numary/ledger/pkg/api/controllers"
"github.com/numary/ledger/pkg/api/internal"
Expand Down Expand Up @@ -311,7 +312,7 @@ func TestTooManyClient(t *testing.T) {
assert.NoError(t, err)

// Grab all potential connections
for i := 0; i < ledgertesting.MaxConnections; i++ {
for i := 0; i < pgtesting.MaxConnections; i++ {
tx, err := store.(*sqlstorage.Store).DB().BeginTx(context.Background(), &sql.TxOptions{})
assert.NoError(t, err)
defer tx.Rollback()
Expand Down
8 changes: 4 additions & 4 deletions pkg/api/internal/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,12 @@ func WithNewModule(t *testing.T, options ...fx.Option) {
}
}

func RunSubTest(t *testing.T, name string, fn interface{}) {
func RunSubTest(t *testing.T, name string, fn interface{}, opts ...fx.Option) {
t.Run(name, func(t *testing.T) {
RunTest(t, fn)
RunTest(t, fn, opts...)
})
}

func RunTest(t *testing.T, fn interface{}) {
WithNewModule(t, fx.Invoke(fn))
func RunTest(t *testing.T, fn interface{}, opts ...fx.Option) {
WithNewModule(t, append(opts, fx.Invoke(fn))...)
}
4 changes: 4 additions & 0 deletions pkg/api/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func ProvidePerLedgerMiddleware(provider interface{}, additionalAnnotations ...f
type Routes struct {
resolver *ledger.Resolver
ledgerMiddleware middlewares.LedgerMiddleware
healthController controllers.HealthController
configController controllers.ConfigController
ledgerController controllers.LedgerController
scriptController controllers.ScriptController
Expand All @@ -57,6 +58,7 @@ func NewRoutes(
accountController controllers.AccountController,
transactionController controllers.TransactionController,
mappingController controllers.MappingController,
healthController controllers.HealthController,
) *Routes {
return &Routes{
globalMiddlewares: globalMiddlewares,
Expand All @@ -69,6 +71,7 @@ func NewRoutes(
accountController: accountController,
transactionController: transactionController,
mappingController: mappingController,
healthController: healthController,
}
}

Expand All @@ -79,6 +82,7 @@ func (r *Routes) Engine() *gin.Engine {
// Default Middlewares
engine.Use(r.globalMiddlewares...)

engine.GET("/_health", r.healthController.Check)
engine.GET("/swagger.yaml", r.configController.GetDocsAsYaml)
engine.GET("/swagger.json", r.configController.GetDocsAsJSON)

Expand Down
33 changes: 33 additions & 0 deletions pkg/health/health.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package health

import "context"

type Check interface {
Do(ctx context.Context) error
}
type CheckFn func(ctx context.Context) error

func (fn CheckFn) Do(ctx context.Context) error {
return fn(ctx)
}

type NamedCheck interface {
Check
Name() string
}

type simpleNamedCheck struct {
Check
name string
}

func (c *simpleNamedCheck) Name() string {
return c.name
}

func NewNamedCheck(name string, check Check) *simpleNamedCheck {
return &simpleNamedCheck{
Check: check,
name: name,
}
}
11 changes: 11 additions & 0 deletions pkg/health/module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package health

import "go.uber.org/fx"

const HealthCheckKey = `group:"_healthCheck"`

func ProvideHealthCheck(provider interface{}) fx.Option {
return fx.Provide(
fx.Annotate(provider, fx.ResultTags(HealthCheckKey)),
)
}
6 changes: 2 additions & 4 deletions pkg/ledger/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package ledger
import (
"context"
"github.com/numary/ledger/pkg/storage"
"github.com/numary/ledger/pkg/storage/sqlstorage"
"github.com/pkg/errors"
"go.uber.org/fx"
"sync"
Expand Down Expand Up @@ -33,7 +32,6 @@ func WithLocker(locker Locker) ResolveOptionFn {
}

var DefaultResolverOptions = []ResolverOption{
WithStorageFactory(storage.NewDefaultFactory(sqlstorage.NewInMemorySQLiteDriver())),
WithLocker(NewInMemoryLocker()),
}

Expand All @@ -44,7 +42,7 @@ type Resolver struct {
initializedStores map[string]struct{}
}

func NewResolver(options ...ResolverOption) *Resolver {
func NewResolver(storageFactory storage.Factory, options ...ResolverOption) *Resolver {
options = append(DefaultResolverOptions, options...)
r := &Resolver{
initializedStores: map[string]struct{}{},
Expand Down Expand Up @@ -100,7 +98,7 @@ func ProvideResolverOption(provider interface{}) fx.Option {
func ResolveModule() fx.Option {
return fx.Options(
fx.Provide(
fx.Annotate(NewResolver, fx.ParamTags(ResolverOptionsKey)),
fx.Annotate(NewResolver, fx.ParamTags("", ResolverOptionsKey)),
),
ProvideResolverOption(WithStorageFactory),
)
Expand Down
Loading

0 comments on commit 28ff9a8

Please sign in to comment.