Add service boilerplate
This commit is contained in:
parent
6586e1f86d
commit
834d8da84f
|
@ -0,0 +1,45 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
// MongoDB contains configuration settings for a MongoDB database.
|
||||
type MongoDB struct {
|
||||
MongodbURI string `split_words:"true" required:"true"`
|
||||
MongodbDatabase string `split_words:"true" required:"true"`
|
||||
}
|
||||
|
||||
// Logger contains configuration settings for a logger.
|
||||
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
|
||||
// populate the environment variables.
|
||||
func LoadFromEnv[T any]() (*T, error) {
|
||||
|
||||
// Load .env file (if it exists)
|
||||
_ = godotenv.Load()
|
||||
|
||||
// Load environment variables into a struct
|
||||
var settings T
|
||||
err := envconfig.Process("", &settings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config from environment: %w", err)
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
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/settings"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/event-watcher/config"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/event-watcher/http"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// Load config
|
||||
cfg, err := settings.LoadFromEnv[config.ServiceSettings]()
|
||||
if err != nil {
|
||||
log.Fatal("Error loading config: ", err)
|
||||
}
|
||||
|
||||
// Build rootLogger
|
||||
rootLogger := logger.New("wormhole-explorer-core-contract-watcher", logger.WithLevel(cfg.LogLevel))
|
||||
|
||||
// Create top-level context
|
||||
rootCtx, rootCtxCancel := context.WithCancel(context.Background())
|
||||
|
||||
//TODO: this requires merging https://github.com/wormhole-foundation/wormhole-explorer/pull/590,
|
||||
// which is currently under code review.
|
||||
//
|
||||
//// Connect to MongoDB
|
||||
//rootLogger.Info("connecting to MongoDB...")
|
||||
//db, err := dbutil.Connect(rootCtx, cfg.MongodbURI, cfg.MongodbDatabase)
|
||||
//if err != nil {
|
||||
// rootLogger.Fatal("Error connecting to MongoDB", zap.Error(err))
|
||||
//}
|
||||
|
||||
// Start serving the monitoring endpoints.
|
||||
plugins := []health.Check{ /*health.Mongo(db.Database)*/ } //TODO blocked on https://github.com/wormhole-foundation/wormhole-explorer/pull/590
|
||||
server := http.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) //TODO blocked on https://github.com/wormhole-foundation/wormhole-explorer/pull/590
|
||||
rootLogger.Info("cancelling root context...")
|
||||
rootCtxCancel()
|
||||
rootLogger.Info("terminated")
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/settings"
|
||||
)
|
||||
|
||||
// ServiceSettings models the configuration settings for the event-watcher service.
|
||||
type ServiceSettings struct {
|
||||
settings.Logger
|
||||
settings.MongoDB
|
||||
settings.Monitoring
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/pprof"
|
||||
"github.com/wormhole-foundation/wormhole-explorer/common/health"
|
||||
"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 ...health.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 []health.Check
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// newController creates a Controller instance.
|
||||
func newController(checks []health.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
|
||||
}
|
Loading…
Reference in New Issue