179 lines
4.3 KiB
Go
179 lines
4.3 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"github.com/rs/zerolog"
|
|
swaggerfiles "github.com/swaggo/files"
|
|
ginSwagger "github.com/swaggo/gin-swagger"
|
|
"go.uber.org/fx"
|
|
"gorm.io/gorm"
|
|
|
|
"base/config"
|
|
"base/internal/delivery/http/platform"
|
|
"base/internal/dto"
|
|
"base/internal/server/middleware"
|
|
"base/pkg/health"
|
|
)
|
|
|
|
type Params struct {
|
|
fx.In
|
|
|
|
Engine *gin.Engine
|
|
Config *config.AppConfig
|
|
Logger zerolog.Logger
|
|
Public *platform.Controller
|
|
DB *gorm.DB
|
|
}
|
|
|
|
// StartHTTPServer starts the HTTP server
|
|
func StartHTTPServer(lifecycle fx.Lifecycle, params Params) {
|
|
server := &http.Server{
|
|
Addr: fmt.Sprintf("%s:%s", params.Config.Server.WebHost, params.Config.Server.WebPort),
|
|
Handler: params.Engine,
|
|
}
|
|
|
|
lifecycle.Append(
|
|
fx.Hook{
|
|
OnStart: func(ctx context.Context) error {
|
|
params.Logger.Info().Str("module", "http").Msg("Starting HTTP server")
|
|
go func() {
|
|
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
params.Logger.Error().Err(err).Msg("HTTP server failed to start")
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
},
|
|
OnStop: func(ctx context.Context) error {
|
|
params.Logger.Info().Str("module", "http").Msg("Stopping HTTP server")
|
|
return server.Shutdown(ctx)
|
|
},
|
|
})
|
|
}
|
|
|
|
// NewGinEngine creates a new Gin HTTP engine
|
|
func NewGinEngine(logger zerolog.Logger) *gin.Engine {
|
|
// Set Gin mode
|
|
gin.SetMode(gin.ReleaseMode)
|
|
|
|
r := gin.New()
|
|
|
|
// Use custom middlewares
|
|
r.Use(gin.CustomRecovery(CustomRecoveryWithLogger(logger)))
|
|
|
|
// Use logger middleware
|
|
r.Use(
|
|
func(c *gin.Context) {
|
|
start := time.Now()
|
|
path := c.Request.URL.Path
|
|
|
|
c.Next()
|
|
|
|
end := time.Now()
|
|
latency := end.Sub(start)
|
|
|
|
if path == "/health" || path == "/health/live" || path == "/metrics" {
|
|
return
|
|
}
|
|
|
|
logger.Info().
|
|
Str("method", c.Request.Method).
|
|
Str("path", path).
|
|
Str("ip", c.ClientIP()).
|
|
Int("status", c.Writer.Status()).
|
|
Dur("latency", latency).
|
|
Msg("request completed")
|
|
})
|
|
|
|
return r
|
|
}
|
|
|
|
func registerRoutes(engine *gin.Engine, mid middleware.Middleware) {
|
|
// Prometheus metrics endpoint
|
|
engine.GET("/metrics", gin.WrapH(promhttp.Handler()))
|
|
|
|
engine.Use(mid.Metrics())
|
|
|
|
engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
|
}
|
|
|
|
func healthCheckers(db *gorm.DB) []health.Checker {
|
|
return []health.Checker{
|
|
health.DatabaseHealthChecker(db),
|
|
}
|
|
}
|
|
|
|
func registerHealthRoute(engine *gin.Engine, params Params) {
|
|
engine.GET("/health", func(c *gin.Context) {
|
|
checkers := healthCheckers(params.DB)
|
|
response := health.Health(c.Request.Context(), "1.0.0", checkers...)
|
|
|
|
statusCode := http.StatusOK
|
|
if response.Status == health.StatusUnhealthy {
|
|
statusCode = http.StatusServiceUnavailable
|
|
} else if response.Status == health.StatusDegraded {
|
|
statusCode = http.StatusOK // Degraded is still considered OK for HTTP
|
|
}
|
|
|
|
c.JSON(statusCode, response)
|
|
})
|
|
|
|
// Simple health check endpoint for load balancers
|
|
engine.GET("/health/ready", func(c *gin.Context) {
|
|
checkers := healthCheckers(params.DB)
|
|
response := health.Health(c.Request.Context(), "1.0.0", checkers...)
|
|
|
|
// For readiness, we only care if the service is healthy or degraded
|
|
// Unhealthy means the service is not ready to serve traffic
|
|
if response.Status == health.StatusUnhealthy {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{
|
|
"status": "not ready",
|
|
"timestamp": time.Now(),
|
|
"message": "Service is not ready to serve traffic",
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "ready",
|
|
"timestamp": time.Now(),
|
|
"message": "Service is ready to serve traffic",
|
|
})
|
|
})
|
|
|
|
// Liveness check endpoint
|
|
engine.GET("/health/live", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "alive",
|
|
"timestamp": time.Now(),
|
|
"message": "Service is alive",
|
|
})
|
|
})
|
|
}
|
|
|
|
var Server = fx.Module(
|
|
"server",
|
|
fx.Provide(NewGinEngine),
|
|
fx.Invoke(StartHTTPServer, registerRoutes, registerHealthRoute),
|
|
)
|
|
|
|
func CustomRecoveryWithLogger(logger zerolog.Logger) gin.RecoveryFunc {
|
|
return func(c *gin.Context, err interface{}) {
|
|
logger.Error().
|
|
Interface("error", err).
|
|
Str("path", c.Request.URL.Path).
|
|
Str("method", c.Request.Method).
|
|
Msg("panic recovered")
|
|
|
|
c.JSON(http.StatusInternalServerError, dto.InternalServerError())
|
|
c.Abort()
|
|
}
|
|
}
|