package main import ( "context" "net/http" "os" "os/signal" "strings" "syscall" "time" _ "github.com/ez-api/ez-api/docs" "github.com/ez-api/ez-api/internal/api" "github.com/ez-api/ez-api/internal/config" "github.com/ez-api/ez-api/internal/middleware" "github.com/ez-api/ez-api/internal/model" "github.com/ez-api/ez-api/internal/service" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "github.com/rs/zerolog" "github.com/rs/zerolog/log" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" "gorm.io/driver/postgres" "gorm.io/gorm" ) // @title EZ-API Control Plane // @version 0.0.1 // @description Management API for EZ-API Gateway system. // @termsOfService http://swagger.io/terms/ // @contact.name API Support // @contact.url http://www.swagger.io/support // @contact.email support@swagger.io // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @host localhost:8080 // @BasePath / // @securityDefinitions.apikey AdminAuth // @in header // @name Authorization // @securityDefinitions.apikey MasterAuth // @in header // @name Authorization func main() { logger := setupLogger() // 1. Load Configuration cfg, err := config.Load() if err != nil { logger.Fatal().Err(err).Msg("Failed to load config") } // 2. Initialize Redis Client rdb := redis.NewClient(&redis.Options{ Addr: cfg.Redis.Addr, Password: cfg.Redis.Password, DB: cfg.Redis.DB, }) // Verify Redis connection ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := rdb.Ping(ctx).Err(); err != nil { logger.Fatal().Err(err).Msg("Failed to connect to Redis") } logger.Info().Msg("Connected to Redis successfully") // 3. Initialize GORM (PostgreSQL) db, err := gorm.Open(postgres.Open(cfg.Postgres.DSN), &gorm.Config{}) if err != nil { logger.Fatal().Err(err).Msg("Failed to connect to PostgreSQL") } sqlDB, err := db.DB() if err != nil { logger.Fatal().Err(err).Msg("Failed to get generic database object") } // Verify DB connection if err := sqlDB.Ping(); err != nil { logger.Fatal().Err(err).Msg("Failed to ping PostgreSQL") } logger.Info().Msg("Connected to PostgreSQL successfully") // Auto Migrate if err := db.AutoMigrate(&model.Master{}, &model.Key{}, &model.Provider{}, &model.Model{}, &model.LogRecord{}); err != nil { logger.Fatal().Err(err).Msg("Failed to auto migrate") } // 4. Setup Services and Handlers syncService := service.NewSyncService(rdb) logWriter := service.NewLogWriter(db, cfg.Log.QueueCapacity, cfg.Log.BatchSize, cfg.Log.FlushInterval) logCtx, cancelLogs := context.WithCancel(context.Background()) defer cancelLogs() logWriter.Start(logCtx) adminService, err := service.NewAdminService() if err != nil { logger.Fatal().Err(err).Msg("Failed to create admin service") } masterService := service.NewMasterService(db) healthService := service.NewHealthCheckService(db, rdb) handler := api.NewHandler(db, syncService, logWriter) adminHandler := api.NewAdminHandler(masterService, syncService) masterHandler := api.NewMasterHandler(masterService, syncService) // 4.1 Prime Redis snapshots so DP can start with data if err := syncService.SyncAll(db); err != nil { logger.Warn().Err(err).Msg("Initial sync warning") } // 5. Setup Gin Router r := gin.Default() // CORS Middleware r.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // TODO: Restrict this in production c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() }) // Health Check Endpoint r.GET("/health", func(c *gin.Context) { status := healthService.Check(c.Request.Context()) httpStatus := http.StatusOK if status.Status == "down" { httpStatus = http.StatusServiceUnavailable } c.JSON(httpStatus, status) }) // Swagger Documentation r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // API Routes // Admin Routes adminGroup := r.Group("/admin") adminGroup.Use(middleware.AdminAuthMiddleware(adminService)) { adminGroup.POST("/masters", adminHandler.CreateMaster) // Other admin routes for managing providers, models, etc. adminGroup.POST("/providers", handler.CreateProvider) adminGroup.POST("/models", handler.CreateModel) adminGroup.GET("/models", handler.ListModels) adminGroup.POST("/sync/snapshot", handler.SyncSnapshot) } // Master Routes masterGroup := r.Group("/v1") masterGroup.Use(middleware.MasterAuthMiddleware(masterService)) { masterGroup.POST("/tokens", masterHandler.IssueChildKey) } // Public/General Routes (if any) r.POST("/logs", handler.IngestLog) srv := &http.Server{ Addr: ":" + cfg.Server.Port, Handler: r, } // 6. Start Server with Graceful Shutdown go func() { logger.Info().Str("port", cfg.Server.Port).Msg("Starting ez-api") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatal().Err(err).Msg("Server failed") } }() // Wait for interrupt signal quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info().Msg("Shutting down server...") // Shutdown with timeout ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { logger.Fatal().Err(err).Msg("Server forced to shutdown") } logger.Info().Msg("Server exited properly") } func setupLogger() zerolog.Logger { level := zerolog.InfoLevel if raw := strings.TrimSpace(os.Getenv("EZ_LOG_LEVEL")); raw != "" { if parsed, err := zerolog.ParseLevel(strings.ToLower(raw)); err == nil { level = parsed } } zerolog.SetGlobalLevel(level) output := zerolog.ConsoleWriter{ Out: os.Stdout, TimeFormat: time.RFC3339, } logger := zerolog.New(output).Level(level).With(). Timestamp(). Str("service", "ez-api"). Logger() log.Logger = logger return logger }