package bootstrap import ( "context" "fmt" "time" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgxpool" goredis "github.com/redis/go-redis/v9" "go.uber.org/zap" "github.com/geo-platform/tenant-api/internal/shared/auditlog" "github.com/geo-platform/tenant-api/internal/shared/auth" "github.com/geo-platform/tenant-api/internal/shared/cache" "github.com/geo-platform/tenant-api/internal/shared/config" "github.com/geo-platform/tenant-api/internal/shared/llm" "github.com/geo-platform/tenant-api/internal/shared/middleware" "github.com/geo-platform/tenant-api/internal/shared/objectstorage" "github.com/geo-platform/tenant-api/internal/shared/observability" "github.com/geo-platform/tenant-api/internal/shared/repository/postgres" "github.com/geo-platform/tenant-api/internal/shared/repository/redis" "github.com/geo-platform/tenant-api/internal/shared/response" "github.com/geo-platform/tenant-api/internal/shared/retrieval" "github.com/geo-platform/tenant-api/internal/shared/stream" ) type App struct { Config *config.Config Logger *zap.Logger DB *pgxpool.Pool Redis *goredis.Client Engine *gin.Engine JWT *auth.Manager Sessions *auth.SessionStore LLM llm.Client RetrievalProvider retrieval.Provider VectorStore retrieval.VectorStore ObjectStorage objectstorage.Client AuditLogs *auditlog.AsyncWriter GenerationStreams *stream.GenerationHub Cache cache.Cache } func New(configPath string) (*App, error) { cfg, err := config.Load(configPath) if err != nil { return nil, fmt.Errorf("load config: %w", err) } logger, err := observability.NewLogger(cfg.Log.Level, cfg.Log.Format) if err != nil { return nil, fmt.Errorf("init logger: %w", err) } ctx := context.Background() pool, err := postgres.NewPool(ctx, cfg.Database) if err != nil { return nil, fmt.Errorf("init postgres: %w", err) } rdb, err := redis.NewClient(ctx, cfg.Redis) if err != nil { pool.Close() return nil, fmt.Errorf("init redis: %w", err) } jwtMgr := auth.NewManager(cfg.JWT.Secret, cfg.JWT.AccessTTL, cfg.JWT.RefreshTTL) sessions := auth.NewSessionStore(rdb) llmClient := llm.New(cfg.LLM) retrievalProvider := retrieval.NewProvider(cfg.Retrieval, logger) vectorStore := retrieval.NewQdrantStore(cfg.Qdrant, logger) objectStorageClient := objectstorage.New(cfg.ObjectStorage, logger) auditLogs := auditlog.NewAsyncWriter(pool, logger) generationStreams := stream.NewGenerationHub() appCache := cache.New(cfg.Cache.Driver, rdb) if cfg.Server.Mode == "release" { gin.SetMode(gin.ReleaseMode) } engine := gin.New() engine.Use( middleware.Recovery(logger), middleware.RequestID(), middleware.CORS(), ) engine.GET("/api/health/live", func(c *gin.Context) { response.Success(c, gin.H{"status": "alive"}) }) engine.GET("/api/health/ready", func(c *gin.Context) { if err := pool.Ping(c.Request.Context()); err != nil { response.Error(c, response.ErrServiceUnavailable(50301, "postgres_unavailable", "database is not reachable")) return } if err := rdb.Ping(c.Request.Context()).Err(); err != nil { response.Error(c, response.ErrServiceUnavailable(50302, "redis_unavailable", "redis is not reachable")) return } response.Success(c, gin.H{"status": "ready"}) }) return &App{ Config: cfg, Logger: logger, DB: pool, Redis: rdb, Engine: engine, JWT: jwtMgr, Sessions: sessions, LLM: llmClient, RetrievalProvider: retrievalProvider, VectorStore: vectorStore, ObjectStorage: objectStorageClient, AuditLogs: auditLogs, GenerationStreams: generationStreams, Cache: appCache, }, nil } func (a *App) Close() { if a.AuditLogs != nil { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) if err := a.AuditLogs.Close(ctx); err != nil { a.Logger.Warn("shutdown with pending audit logs", zap.Error(err)) } cancel() } a.DB.Close() _ = a.Redis.Close() _ = a.Logger.Sync() }