From 2e9bd4b5ded8401beb8ec5e252fbb8da89294e18 Mon Sep 17 00:00:00 2001 From: gllm-dev Date: Thu, 12 Dec 2024 12:08:09 +0100 Subject: [PATCH 1/2] feat: add health endpoint --- CHANGELOG.md | 4 ++ di/wire.go | 11 ++++ di/wire_gen.go | 16 ++++- .../handlers/rest/healthzhdl/handler.go | 59 +++++++++++++++++++ internal/adapters/handlers/rest/server.go | 8 ++- internal/applications/healthzapp/app.go | 36 +++++++++++ internal/core/domain/errors/infra.go | 7 +++ 7 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 internal/adapters/handlers/rest/healthzhdl/handler.go create mode 100644 internal/applications/healthzapp/app.go create mode 100644 internal/core/domain/errors/infra.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 884f071..6989121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.1.24] +### Added +- Health check endpoint + ## [v0.1.23] ### Fixed - Update crypto to v0.31.0 package because of CVE-2024-45337 diff --git a/di/wire.go b/di/wire.go index 8f691e4..337a518 100644 --- a/di/wire.go +++ b/di/wire.go @@ -17,6 +17,7 @@ import ( "go.openfort.xyz/shield/internal/adapters/repositories/sql/providerrepo" "go.openfort.xyz/shield/internal/adapters/repositories/sql/sharerepo" "go.openfort.xyz/shield/internal/adapters/repositories/sql/userrepo" + "go.openfort.xyz/shield/internal/applications/healthzapp" "go.openfort.xyz/shield/internal/applications/projectapp" "go.openfort.xyz/shield/internal/applications/shamirjob" "go.openfort.xyz/shield/internal/applications/shareapp" @@ -196,12 +197,22 @@ func ProvideIdentityFactory() (f factories.IdentityFactory, err error) { return } +func ProvideHealthzApplication() (a *healthzapp.Application, err error) { + wire.Build( + ProvideSQL, + healthzapp.New, + ) + + return +} + func ProvideRESTServer() (s *rest.Server, err error) { wire.Build( rest.New, rest.GetConfigFromEnv, ProvideShareApplication, ProvideProjectApplication, + ProvideHealthzApplication, ProvideUserService, ProvideAuthenticationFactory, ProvideIdentityFactory, diff --git a/di/wire_gen.go b/di/wire_gen.go index 3eea368..68bbb9c 100644 --- a/di/wire_gen.go +++ b/di/wire_gen.go @@ -19,6 +19,7 @@ import ( "go.openfort.xyz/shield/internal/adapters/repositories/sql/providerrepo" "go.openfort.xyz/shield/internal/adapters/repositories/sql/sharerepo" "go.openfort.xyz/shield/internal/adapters/repositories/sql/userrepo" + "go.openfort.xyz/shield/internal/applications/healthzapp" "go.openfort.xyz/shield/internal/applications/projectapp" "go.openfort.xyz/shield/internal/applications/shamirjob" "go.openfort.xyz/shield/internal/applications/shareapp" @@ -248,6 +249,15 @@ func ProvideIdentityFactory() (factories.IdentityFactory, error) { return identityFactory, nil } +func ProvideHealthzApplication() (*healthzapp.Application, error) { + client, err := ProvideSQL() + if err != nil { + return nil, err + } + application := healthzapp.New(client) + return application, nil +} + func ProvideRESTServer() (*rest.Server, error) { config, err := rest.GetConfigFromEnv() if err != nil { @@ -273,6 +283,10 @@ func ProvideRESTServer() (*rest.Server, error) { if err != nil { return nil, err } - server := rest.New(config, projectApplication, shareApplication, authenticationFactory, identityFactory, userService) + application, err := ProvideHealthzApplication() + if err != nil { + return nil, err + } + server := rest.New(config, projectApplication, shareApplication, authenticationFactory, identityFactory, userService, application) return server, nil } diff --git a/internal/adapters/handlers/rest/healthzhdl/handler.go b/internal/adapters/handlers/rest/healthzhdl/handler.go new file mode 100644 index 0000000..06e4367 --- /dev/null +++ b/internal/adapters/handlers/rest/healthzhdl/handler.go @@ -0,0 +1,59 @@ +package healthzhdl + +import ( + "encoding/json" + "errors" + "go.openfort.xyz/shield/internal/applications/healthzapp" + domainErrors "go.openfort.xyz/shield/internal/core/domain/errors" + "net/http" + "time" +) + +type Handler struct { + app *healthzapp.Application +} + +func New(app *healthzapp.Application) *Handler { + return &Handler{ + app: app, + } +} + +type Status struct { + Status string `json:"status"` + At string `json:"at"` + Checks []Check `json:"checks"` +} + +type Check struct { + Name string `json:"name"` + Status string `json:"status"` +} + +func (h *Handler) Healthz(w http.ResponseWriter, r *http.Request) { + status := Status{ + Status: "healthy", + At: time.Now().UTC().Format(time.RFC3339), + Checks: []Check{}, + } + + err := h.app.Healthz(r.Context()) + if err != nil { + status.Status = "unhealthy" + if errors.Is(err, domainErrors.ErrDatabaseUnavailable) { + status.Checks = append(status.Checks, Check{ + Name: "database", + Status: "unhealthy", + }) + } + } else { + status.Checks = append(status.Checks, Check{ + Name: "database", + Status: "healthy", + }) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(status) +} diff --git a/internal/adapters/handlers/rest/server.go b/internal/adapters/handlers/rest/server.go index b44894e..011cfdc 100644 --- a/internal/adapters/handlers/rest/server.go +++ b/internal/adapters/handlers/rest/server.go @@ -3,6 +3,8 @@ package rest import ( "context" "fmt" + "go.openfort.xyz/shield/internal/adapters/handlers/rest/healthzhdl" + "go.openfort.xyz/shield/internal/applications/healthzapp" "log/slog" "net/http" "strings" @@ -27,6 +29,7 @@ import ( type Server struct { projectApp *projectapp.ProjectApplication shareApp *shareapp.ShareApplication + healthzApp *healthzapp.Application server *http.Server logger *slog.Logger config *Config @@ -36,10 +39,11 @@ type Server struct { } // New creates a new REST server -func New(cfg *Config, projectApp *projectapp.ProjectApplication, shareApp *shareapp.ShareApplication, authenticationFactory factories.AuthenticationFactory, identityFactory factories.IdentityFactory, userService services.UserService) *Server { +func New(cfg *Config, projectApp *projectapp.ProjectApplication, shareApp *shareapp.ShareApplication, authenticationFactory factories.AuthenticationFactory, identityFactory factories.IdentityFactory, userService services.UserService, healthzApp *healthzapp.Application) *Server { return &Server{ projectApp: projectApp, shareApp: shareApp, + healthzApp: healthzApp, server: new(http.Server), logger: logger.New("rest_server"), config: cfg, @@ -51,6 +55,7 @@ func New(cfg *Config, projectApp *projectapp.ProjectApplication, shareApp *share // Start starts the REST server func (s *Server) Start(ctx context.Context) error { + healthzHdl := healthzhdl.New(s.healthzApp) projectHdl := projecthdl.New(s.projectApp) shareHdl := sharehdl.New(s.shareApp) authMdw := authmdw.New(s.authenticationFactory, s.identityFactory, s.userService) @@ -60,6 +65,7 @@ func (s *Server) Start(ctx context.Context) error { r.Use(rateLimiterMdw.RateLimitMiddleware) r.Use(requestmdw.RequestIDMiddleware) r.Use(responsemdw.ResponseMiddleware) + r.HandleFunc("/healthz", healthzHdl.Healthz).Methods(http.MethodGet) r.HandleFunc("/register", projectHdl.CreateProject).Methods(http.MethodPost) p := r.PathPrefix("/project").Subrouter() p.Use(authMdw.AuthenticateAPISecret) diff --git a/internal/applications/healthzapp/app.go b/internal/applications/healthzapp/app.go new file mode 100644 index 0000000..3ad29e9 --- /dev/null +++ b/internal/applications/healthzapp/app.go @@ -0,0 +1,36 @@ +package healthzapp + +import ( + "context" + "go.openfort.xyz/shield/internal/adapters/repositories/sql" + "go.openfort.xyz/shield/internal/core/domain/errors" + "go.openfort.xyz/shield/pkg/logger" + "log/slog" +) + +type Application struct { + db *sql.Client + logger *slog.Logger +} + +func New(db *sql.Client) *Application { + return &Application{ + db: db, + logger: logger.New("health_application"), + } +} + +func (a *Application) Healthz(ctx context.Context) error { + db, err := a.db.DB.DB() + if err != nil { + a.logger.ErrorContext(ctx, "failed to get database connection", logger.Error(err)) + return errors.ErrDatabaseUnavailable + } + + if err = db.PingContext(ctx); err != nil { + a.logger.ErrorContext(ctx, "failed to ping database", logger.Error(err)) + return errors.ErrDatabaseUnavailable + } + + return nil +} diff --git a/internal/core/domain/errors/infra.go b/internal/core/domain/errors/infra.go new file mode 100644 index 0000000..5a7f505 --- /dev/null +++ b/internal/core/domain/errors/infra.go @@ -0,0 +1,7 @@ +package errors + +import "errors" + +var ( + ErrDatabaseUnavailable = errors.New("database unavailable") +) From ad98b44c9dbbe4ab5f60be29253be69f0114c01a Mon Sep 17 00:00:00 2001 From: gllm-dev Date: Thu, 12 Dec 2024 12:10:07 +0100 Subject: [PATCH 2/2] fix: linter --- internal/adapters/handlers/rest/healthzhdl/handler.go | 5 +++-- internal/adapters/handlers/rest/server.go | 5 +++-- internal/applications/healthzapp/app.go | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/adapters/handlers/rest/healthzhdl/handler.go b/internal/adapters/handlers/rest/healthzhdl/handler.go index 06e4367..b568781 100644 --- a/internal/adapters/handlers/rest/healthzhdl/handler.go +++ b/internal/adapters/handlers/rest/healthzhdl/handler.go @@ -3,10 +3,11 @@ package healthzhdl import ( "encoding/json" "errors" - "go.openfort.xyz/shield/internal/applications/healthzapp" - domainErrors "go.openfort.xyz/shield/internal/core/domain/errors" "net/http" "time" + + "go.openfort.xyz/shield/internal/applications/healthzapp" + domainErrors "go.openfort.xyz/shield/internal/core/domain/errors" ) type Handler struct { diff --git a/internal/adapters/handlers/rest/server.go b/internal/adapters/handlers/rest/server.go index 011cfdc..7e0dd54 100644 --- a/internal/adapters/handlers/rest/server.go +++ b/internal/adapters/handlers/rest/server.go @@ -3,12 +3,13 @@ package rest import ( "context" "fmt" - "go.openfort.xyz/shield/internal/adapters/handlers/rest/healthzhdl" - "go.openfort.xyz/shield/internal/applications/healthzapp" "log/slog" "net/http" "strings" + "go.openfort.xyz/shield/internal/adapters/handlers/rest/healthzhdl" + "go.openfort.xyz/shield/internal/applications/healthzapp" + "go.openfort.xyz/shield/internal/core/ports/factories" "go.openfort.xyz/shield/internal/core/ports/services" diff --git a/internal/applications/healthzapp/app.go b/internal/applications/healthzapp/app.go index 3ad29e9..1d49f11 100644 --- a/internal/applications/healthzapp/app.go +++ b/internal/applications/healthzapp/app.go @@ -2,10 +2,11 @@ package healthzapp import ( "context" + "log/slog" + "go.openfort.xyz/shield/internal/adapters/repositories/sql" "go.openfort.xyz/shield/internal/core/domain/errors" "go.openfort.xyz/shield/pkg/logger" - "log/slog" ) type Application struct {