From e35ef1cba5f2b7dc07af896f41bbdd988f26f8f7 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Mon, 8 Mar 2021 14:36:51 -0300 Subject: [PATCH] feat: Add health check service and http handler (#17) --- Makefile | 4 +-- cmd/postmand/main.go | 21 ++++++++++++++++ go.mod | 2 +- go.sum | 4 +-- http/handler/ping.go | 45 +++++++++++++++++++++++++++++++++ http/handler/ping_test.go | 53 +++++++++++++++++++++++++++++++++++++++ local.env | 1 + mocks/PingRepository.go | 28 +++++++++++++++++++++ mocks/PingService.go | 28 +++++++++++++++++++++ repository.go | 5 ++++ repository/ping.go | 22 ++++++++++++++++ repository/ping_test.go | 17 +++++++++++++ repository/util_test.go | 2 ++ service.go | 5 ++++ service/ping.go | 22 ++++++++++++++++ service/ping_test.go | 20 +++++++++++++++ 16 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 http/handler/ping.go create mode 100644 http/handler/ping_test.go create mode 100644 mocks/PingRepository.go create mode 100644 mocks/PingService.go create mode 100644 repository/ping.go create mode 100644 repository/ping_test.go create mode 100644 service/ping.go create mode 100644 service/ping_test.go diff --git a/Makefile b/Makefile index 07047a1..13b1124 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ PLATFORM := $(shell uname | tr A-Z a-z) lint: if [ ! -f ./bin/golangci-lint ] ; \ then \ - curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.37.1; \ + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.38.0; \ fi; ./bin/golangci-lint run @@ -26,4 +26,4 @@ db-migrate: download-golang-migrate-binary db-test-migrate: download-golang-migrate-binary ./migrate.$(PLATFORM)-amd64 -source file://db/migrations -database ${POSTMAND_TEST_DATABASE_URL} up -.PHONY: lint test download-golang-migrate-binary db-migrate db-test-migrate +.PHONY: lint test mock download-golang-migrate-binary db-migrate db-test-migrate diff --git a/cmd/postmand/main.go b/cmd/postmand/main.go index a789de3..910a2bc 100644 --- a/cmd/postmand/main.go +++ b/cmd/postmand/main.go @@ -17,6 +17,16 @@ import ( "go.uber.org/zap" ) +func healthcheckServer(db *sqlx.DB, logger *zap.Logger) { + pingRepository := repository.NewPing(db) + pingService := service.NewPing(pingRepository) + pingHandler := handler.NewPing(pingService, logger) + mux := http.NewRouter(logger) + mux.Get("/healthz", pingHandler.Healthz) + server := http.NewServer(mux, env.GetInt("POSTMAND_HEALTH_CHECK_HTTP_PORT", 8000), logger) + server.Run() +} + func main() { // Setup logger logger, err := zap.NewProduction() @@ -65,6 +75,9 @@ func main() { Aliases: []string{"w"}, Usage: "executes worker to dispatch webhooks", Action: func(c *cli.Context) error { + // Start health check + go healthcheckServer(db, logger) + deliveryRepository := repository.NewDelivery(db) pollingInterval := time.Duration(env.GetInt("POSTMAND_POLLING_INTERVAL", 1000)) * time.Millisecond workerService := service.NewWorker(deliveryRepository, logger, pollingInterval) @@ -77,12 +90,20 @@ func main() { Aliases: []string{"s"}, Usage: "executes http server", Action: func(c *cli.Context) error { + // Start health check + go healthcheckServer(db, logger) + + // Create repositories webhookRepository := repository.NewWebhook(db) deliveryRepository := repository.NewDelivery(db) deliveryAttemptRepository := repository.NewDeliveryAttempt(db) + + // Create services webhookService := service.NewWebhook(webhookRepository) deliveryService := service.NewDelivery(deliveryRepository) deliveryAttemptService := service.NewDeliveryAttempt(deliveryAttemptRepository) + + // Create http handlers webhookHandler := handler.NewWebhook(webhookService, logger) deliveryHandler := handler.NewDelivery(deliveryService, logger) deliveryAttemptHandler := handler.NewDeliveryAttempt(deliveryAttemptService, logger) diff --git a/go.mod b/go.mod index d1d2bbb..b4c9a67 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/jmoiron/sqlx v1.3.1 github.com/joho/godotenv v1.3.0 github.com/jpillora/backoff v1.0.0 - github.com/lib/pq v1.9.0 + github.com/lib/pq v1.10.0 github.com/steinfletcher/apitest v1.5.2 github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 diff --git a/go.sum b/go.sum index 1defe7e..d0aef4b 100644 --- a/go.sum +++ b/go.sum @@ -241,8 +241,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= -github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= diff --git a/http/handler/ping.go b/http/handler/ping.go new file mode 100644 index 0000000..8144fab --- /dev/null +++ b/http/handler/ping.go @@ -0,0 +1,45 @@ +package handler + +import ( + "net/http" + + "github.com/allisson/postmand" + "go.uber.org/zap" +) + +type pingResponse struct { + Success bool `json:"success"` +} + +// Ping implements interface for health check. +type Ping struct { + pingService postmand.PingService + logger *zap.Logger +} + +// Healthz returns health check response. +func (p Ping) Healthz(w http.ResponseWriter, r *http.Request) { + pr := pingResponse{} + + if err := p.pingService.Run(r.Context()); err != nil { + p.logger.Error( + "service-error", + zap.String("name", "PingService"), + zap.String("method", "Run"), + zap.Error(err), + ) + makeJSONResponse(w, http.StatusInternalServerError, &pr, p.logger) + return + } + + pr.Success = true + makeJSONResponse(w, http.StatusOK, &pr, p.logger) +} + +// NewPing creates a new Ping. +func NewPing(pingService postmand.PingService, logger *zap.Logger) *Ping { + return &Ping{ + pingService: pingService, + logger: logger, + } +} diff --git a/http/handler/ping_test.go b/http/handler/ping_test.go new file mode 100644 index 0000000..740b9b1 --- /dev/null +++ b/http/handler/ping_test.go @@ -0,0 +1,53 @@ +package handler + +import ( + "errors" + nethttp "net/http" + "testing" + + "github.com/allisson/postmand/http" + "github.com/allisson/postmand/mocks" + "github.com/steinfletcher/apitest" + "github.com/stretchr/testify/mock" + "go.uber.org/zap" +) + +func TestPing(t *testing.T) { + logger, _ := zap.NewDevelopment() + + t.Run("With success", func(t *testing.T) { + pingService := &mocks.PingService{} + pingHandler := NewPing(pingService, logger) + router := http.NewRouter(logger) + router.Get("/healthz", pingHandler.Healthz) + + pingService.On("Run", mock.Anything).Return(nil) + apitest.New(). + Handler(router). + Get("/healthz"). + Expect(t). + Body(`{"success":true}`). + Status(nethttp.StatusOK). + End() + + pingService.AssertExpectations(t) + }) + + t.Run("With error", func(t *testing.T) { + pingService := &mocks.PingService{} + pingHandler := NewPing(pingService, logger) + router := http.NewRouter(logger) + router.Get("/healthz", pingHandler.Healthz) + + pingService.On("Run", mock.Anything).Return(errors.New("BOOM")) + apitest.New(). + Handler(router). + Get("/healthz"). + Expect(t). + Body(`{"success":false}`). + Status(nethttp.StatusInternalServerError). + End() + + pingService.AssertExpectations(t) + }) +} diff --git a/local.env b/local.env index 9e3f0dd..b32ca59 100644 --- a/local.env +++ b/local.env @@ -4,3 +4,4 @@ POSTMAND_DATABASE_MIGRATION_DIR='file://db/migrations' POSTMAND_DATABASE_MAX_OPEN_CONNS='2' POSTMAND_POLLING_INTERVAL='1000' POSTMAND_HTTP_PORT='8000' +POSTMAND_HEALTH_CHECK_HTTP_PORT='8001' diff --git a/mocks/PingRepository.go b/mocks/PingRepository.go new file mode 100644 index 0000000..3f28be3 --- /dev/null +++ b/mocks/PingRepository.go @@ -0,0 +1,28 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// PingRepository is an autogenerated mock type for the PingRepository type +type PingRepository struct { + mock.Mock +} + +// Run provides a mock function with given fields: ctx +func (_m *PingRepository) Run(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/PingService.go b/mocks/PingService.go new file mode 100644 index 0000000..26c751c --- /dev/null +++ b/mocks/PingService.go @@ -0,0 +1,28 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// PingService is an autogenerated mock type for the PingService type +type PingService struct { + mock.Mock +} + +// Run provides a mock function with given fields: ctx +func (_m *PingService) Run(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/repository.go b/repository.go index 3febad5..5e122c4 100644 --- a/repository.go +++ b/repository.go @@ -46,3 +46,8 @@ type DeliveryAttemptRepository interface { type MigrationRepository interface { Run(ctx context.Context) error } + +// PingRepository is the interface that will be used to run ping against database. +type PingRepository interface { + Run(ctx context.Context) error +} diff --git a/repository/ping.go b/repository/ping.go new file mode 100644 index 0000000..9c3ae4c --- /dev/null +++ b/repository/ping.go @@ -0,0 +1,22 @@ +package repository + +import ( + "context" + + "github.com/jmoiron/sqlx" +) + +// Ping implements postmand.PingRepository interface. +type Ping struct { + db *sqlx.DB +} + +// Run ping operation against the database. +func (p Ping) Run(ctx context.Context) error { + return p.db.PingContext(ctx) +} + +// NewPing will create an implementation of postmand.PingRepository. +func NewPing(db *sqlx.DB) *Ping { + return &Ping{db: db} +} diff --git a/repository/ping_test.go b/repository/ping_test.go new file mode 100644 index 0000000..efd1948 --- /dev/null +++ b/repository/ping_test.go @@ -0,0 +1,17 @@ +package repository + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPing(t *testing.T) { + ctx := context.Background() + th := newTestHelper() + defer th.db.Close() + + err := th.pingRepository.Run(ctx) + assert.Nil(t, err) +} diff --git a/repository/util_test.go b/repository/util_test.go index be4e437..85d2046 100644 --- a/repository/util_test.go +++ b/repository/util_test.go @@ -21,6 +21,7 @@ type testHelper struct { webhookRepository *Webhook deliveryRepository *Delivery deliveryAttemptRepository *DeliveryAttempt + pingRepository *Ping } func newTestHelper() testHelper { @@ -31,5 +32,6 @@ func newTestHelper() testHelper { webhookRepository: NewWebhook(db), deliveryRepository: NewDelivery(db), deliveryAttemptRepository: NewDeliveryAttempt(db), + pingRepository: NewPing(db), } } diff --git a/service.go b/service.go index db7e1a0..928c49b 100644 --- a/service.go +++ b/service.go @@ -36,3 +36,8 @@ type DeliveryAttemptService interface { Get(ctx context.Context, getOptions RepositoryGetOptions) (*DeliveryAttempt, error) List(ctx context.Context, listOptions RepositoryListOptions) ([]*DeliveryAttempt, error) } + +// PingService is the interface that will be used to perform ping operation against database. +type PingService interface { + Run(ctx context.Context) error +} diff --git a/service/ping.go b/service/ping.go new file mode 100644 index 0000000..9dff2a5 --- /dev/null +++ b/service/ping.go @@ -0,0 +1,22 @@ +package service + +import ( + "context" + + "github.com/allisson/postmand" +) + +// Ping implements postmand.PingService interface. +type Ping struct { + pingRepository postmand.PingRepository +} + +// Run ping operation against the database. +func (p Ping) Run(ctx context.Context) error { + return p.pingRepository.Run(ctx) +} + +// NewPing will create an implementation of postmand.PingService. +func NewPing(pingRepository postmand.PingRepository) *Ping { + return &Ping{pingRepository: pingRepository} +} diff --git a/service/ping_test.go b/service/ping_test.go new file mode 100644 index 0000000..bfb89b6 --- /dev/null +++ b/service/ping_test.go @@ -0,0 +1,20 @@ +package service + +import ( + "context" + "testing" + + "github.com/allisson/postmand/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPing(t *testing.T) { + pingRepository := &mocks.PingRepository{} + pingService := NewPing(pingRepository) + pingRepository.On("Run", mock.Anything).Return(nil) + ctx := context.Background() + err := pingService.Run(ctx) + assert.Nil(t, err) + pingRepository.AssertExpectations(t) +}