diff --git a/common/health/http.go b/common/health/http.go new file mode 100644 index 00000000..cf8a7017 --- /dev/null +++ b/common/health/http.go @@ -0,0 +1,119 @@ +package health + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/pprof" + "go.uber.org/zap" +) + +type Server struct { + app *fiber.App + port string + logger *zap.Logger +} + +func NewServer(logger *zap.Logger, port string, pprofEnabled bool, checks ...Check) *Server { + + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + // config use of middlware. + if pprofEnabled { + app.Use(pprof.New()) + } + + ctrl := NewController(checks, logger) + api := app.Group("/api") + api.Get("/health", ctrl.HealthCheck) + api.Get("/ready", ctrl.ReadinessCheck) + + return &Server{ + app: app, + port: port, + logger: logger, + } +} + +// Start initiates the serving of HTTP requests. +func (s *Server) Start() { + + addr := ":" + s.port + s.logger.Info("Monitoring server starting", zap.String("bindAddress", addr)) + + go func() { + err := s.app.Listen(addr) + if err != nil { + s.logger.Error("Failed to start monitoring server", zap.Error(err), zap.String("bindAddress", addr)) + } + }() +} + +// Stop gracefully shuts down the server. +// +// Blocks until all active connections are closed. +func (s *Server) Stop() { + _ = s.app.Shutdown() +} + +type controller struct { + checks []Check + logger *zap.Logger +} + +// NewController creates a Controller instance. +func NewController(checks []Check, logger *zap.Logger) *controller { + return &controller{checks: checks, logger: logger} +} + +// HealthCheck is the HTTP handler for the route `GET /health`. +func (c *controller) HealthCheck(ctx *fiber.Ctx) error { + + response := ctx.JSON(struct { + Status string `json:"status"` + }{ + Status: "OK", + }) + + return response +} + +// ReadinessCheck is the HTTP handler for the route `GET /ready`. +func (c *controller) ReadinessCheck(ctx *fiber.Ctx) error { + + requestCtx := ctx.Context() + requestID := fmt.Sprintf("%v", requestCtx.Value("requestid")) + + // For every callback, check whether it is passing + for _, check := range c.checks { + if err := check(requestCtx); err != nil { + + c.logger.Error( + "Readiness check failed", + zap.Error(err), + zap.String("requestID", requestID), + ) + + // Return error information to the caller + response := ctx. + Status(fiber.StatusInternalServerError). + JSON(struct { + Ready string `json:"ready"` + Error string `json:"error"` + }{ + Ready: "NO", + Error: err.Error(), + }) + return response + } + } + + // All checks passed + response := ctx.Status(fiber.StatusOK). + JSON(struct { + Ready string `json:"ready"` + }{ + Ready: "OK", + }) + return response +} diff --git a/common/settings/structs.go b/common/settings/structs.go index dac2553b..4ac00cb8 100644 --- a/common/settings/structs.go +++ b/common/settings/structs.go @@ -18,6 +18,13 @@ type Logger struct { LogLevel string `split_words:"true" default:"INFO"` } +// Monitoring contains configuration settings for the monitoring endpoints. +type Monitoring struct { + // MonitoringPort defines the TCP port for the monitoring endpoints. + MonitoringPort string `split_words:"true" default:"8000"` + PprofEnabled bool `split_words:"true" default:"false"` +} + // LoadFromEnv loads the configuration settings from environment variables. // // If there is a .env file in the current directory, it will be used to diff --git a/core-contract-watcher/cmd/service/main.go b/core-contract-watcher/cmd/service/main.go index 6fc1ce4c..9e5441a5 100644 --- a/core-contract-watcher/cmd/service/main.go +++ b/core-contract-watcher/cmd/service/main.go @@ -3,7 +3,11 @@ package main import ( "context" "log" + "os" + "os/signal" + "syscall" + "github.com/wormhole-foundation/wormhole-explorer/common/health" "github.com/wormhole-foundation/wormhole-explorer/common/logger" "github.com/wormhole-foundation/wormhole-explorer/common/mongohelpers" "github.com/wormhole-foundation/wormhole-explorer/common/settings" @@ -32,6 +36,27 @@ func main() { rootLogger.Fatal("Error connecting to MongoDB", zap.Error(err)) } + // Start serving the monitoring endpoints. + plugins := []health.Check{health.Mongo(db.Database)} + server := health.NewServer( + rootLogger, + cfg.MonitoringPort, + cfg.PprofEnabled, + plugins..., + ) + server.Start() + + // Block until we get a termination signal or the context is cancelled + rootLogger.Info("waiting for termination signal or context cancellation...") + sigterm := make(chan os.Signal, 1) + signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM) + select { + case <-rootCtx.Done(): + rootLogger.Warn("terminating (root context cancelled)") + case signal := <-sigterm: + rootLogger.Info("terminating (signal received)", zap.String("signal", signal.String())) + } + // Shut down gracefully rootLogger.Info("disconnecting from MongoDB...") db.Disconnect(rootCtx) diff --git a/core-contract-watcher/config/structs.go b/core-contract-watcher/config/structs.go index 0247ecfd..db7826ea 100644 --- a/core-contract-watcher/config/structs.go +++ b/core-contract-watcher/config/structs.go @@ -8,4 +8,5 @@ import ( type ServiceSettings struct { settings.Logger settings.MongoDB + settings.Monitoring }