Skip to content

Commit

Permalink
feat: Add health check service and http handler (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
allisson committed Mar 8, 2021
1 parent 467ece7 commit e35ef1c
Show file tree
Hide file tree
Showing 16 changed files with 274 additions and 5 deletions.
4 changes: 2 additions & 2 deletions Makefile
Expand Up @@ -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

Expand All @@ -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
21 changes: 21 additions & 0 deletions cmd/postmand/main.go
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Expand Up @@ -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=
Expand Down
45 changes: 45 additions & 0 deletions 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,
}
}
53 changes: 53 additions & 0 deletions 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)
})
}
1 change: 1 addition & 0 deletions local.env
Expand Up @@ -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'
28 changes: 28 additions & 0 deletions mocks/PingRepository.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions mocks/PingService.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions repository.go
Expand Up @@ -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
}
22 changes: 22 additions & 0 deletions 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}
}
17 changes: 17 additions & 0 deletions 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)
}
2 changes: 2 additions & 0 deletions repository/util_test.go
Expand Up @@ -21,6 +21,7 @@ type testHelper struct {
webhookRepository *Webhook
deliveryRepository *Delivery
deliveryAttemptRepository *DeliveryAttempt
pingRepository *Ping
}

func newTestHelper() testHelper {
Expand All @@ -31,5 +32,6 @@ func newTestHelper() testHelper {
webhookRepository: NewWebhook(db),
deliveryRepository: NewDelivery(db),
deliveryAttemptRepository: NewDeliveryAttempt(db),
pingRepository: NewPing(db),
}
}
5 changes: 5 additions & 0 deletions service.go
Expand Up @@ -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
}
22 changes: 22 additions & 0 deletions 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}
}
20 changes: 20 additions & 0 deletions 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)
}

0 comments on commit e35ef1c

Please sign in to comment.