package database import ( "database/sql" "fmt" "os" "strconv" "time" "veza-backend-api/internal/metrics" "gorm.io/driver/postgres" "gorm.io/gorm" ) // NewDB crée une nouvelle connexion GORM avec pool de connexions optimisé // Prend les paramètres de connexion individuels pour plus de flexibilité func NewDB(host string, port int, user, password, dbname string) (*gorm.DB, error) { dsn := fmt.Sprintf( "host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", host, user, password, dbname, port, ) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } sqlDB, err := db.DB() if err != nil { return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err) } // BE-DB-015: Configuration optimale du pool de connexions pour la production // MaxOpenConns: Nombre maximum de connexions ouvertes // Recommandation: (2 * CPU cores) + effective_spindle_count // Default: 50 pour applications moyenne-grande charge, 25 pour petite charge maxOpenConns := 50 if envMaxOpen := os.Getenv("DB_MAX_OPEN_CONNS"); envMaxOpen != "" { if parsed, err := strconv.Atoi(envMaxOpen); err == nil && parsed > 0 { maxOpenConns = parsed } } sqlDB.SetMaxOpenConns(maxOpenConns) // MaxIdleConns: Nombre maximum de connexions inactives // Recommandation: ~25% de MaxOpenConns pour maintenir des connexions chaudes maxIdleConns := 12 if envMaxIdle := os.Getenv("DB_MAX_IDLE_CONNS"); envMaxIdle != "" { if parsed, err := strconv.Atoi(envMaxIdle); err == nil && parsed > 0 { maxIdleConns = parsed } } sqlDB.SetMaxIdleConns(maxIdleConns) // ConnMaxLifetime: Durée maximale de vie d'une connexion // Recommandation: 5-15 minutes pour éviter les timeouts de connexion // PostgreSQL idle_in_transaction_session_timeout peut être configuré côté serveur maxLifetime := 10 * time.Minute if envMaxLifetime := os.Getenv("DB_MAX_LIFETIME"); envMaxLifetime != "" { if parsed, err := time.ParseDuration(envMaxLifetime); err == nil && parsed > 0 { maxLifetime = parsed } } sqlDB.SetConnMaxLifetime(maxLifetime) // ConnMaxIdleTime: Durée maximale d'inactivité d'une connexion avant fermeture // Recommandation: 5-10 minutes pour libérer les ressources inutilisées maxIdleTime := 5 * time.Minute if envMaxIdleTime := os.Getenv("DB_MAX_IDLE_TIME"); envMaxIdleTime != "" { if parsed, err := time.ParseDuration(envMaxIdleTime); err == nil && parsed > 0 { maxIdleTime = parsed } } sqlDB.SetConnMaxIdleTime(maxIdleTime) // Test de la connexion if err := sqlDB.Ping(); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } return db, nil } // NewDBFromEnvConfig crée une nouvelle connexion GORM à partir d'un EnvConfig // Cette fonction facilite l'intégration avec le package config func NewDBFromEnvConfig(host string, port int, user, password, dbname string) (*gorm.DB, error) { return NewDB(host, port, user, password, dbname) } // CloseDB ferme proprement la connexion à la base de données func CloseDB(db *gorm.DB) error { if db == nil { return nil } sqlDB, err := db.DB() if err != nil { return fmt.Errorf("failed to get underlying sql.DB: %w", err) } // Fermeture gracieuse de toutes les connexions return sqlDB.Close() } // GetPoolStats retourne les statistiques du pool de connexions // Met également à jour les métriques Prometheus (T0023) func GetPoolStats(db *gorm.DB) (sql.DBStats, error) { if db == nil { return sql.DBStats{}, fmt.Errorf("database connection is nil") } sqlDB, err := db.DB() if err != nil { return sql.DBStats{}, fmt.Errorf("failed to get underlying sql.DB: %w", err) } stats := sqlDB.Stats() // Mettre à jour les métriques Prometheus (T0023) // open: nombre total de connexions ouvertes // idle: nombre de connexions inactives (OpenConnections - InUse) // in_use: nombre de connexions en cours d'utilisation open := stats.OpenConnections idle := open - stats.InUse inUse := stats.InUse metrics.UpdateDBConnections(open, idle, inUse) return stats, nil } // MeasureQuery mesure la durée d'une requête DB et l'enregistre dans Prometheus // Cette fonction helper peut être utilisée pour wrapper les opérations DB // operation: type d'opération (SELECT, INSERT, UPDATE, DELETE, etc.) // table: nom de la table (ou "unknown" si non disponible) // fn: fonction à exécuter et mesurer func MeasureQuery(operation, table string, fn func() error) error { start := time.Now() err := fn() duration := time.Since(start) // Enregistrer la métrique indépendamment de l'erreur metrics.RecordDBQuery(operation, table, duration) return err } // IsConnectionHealthy vérifie si la connexion à la base de données est saine func IsConnectionHealthy(db *gorm.DB, timeout time.Duration) error { if db == nil { return fmt.Errorf("database connection is nil") } sqlDB, err := db.DB() if err != nil { return fmt.Errorf("failed to get underlying sql.DB: %w", err) } // Utiliser Ping avec un timeout personnalisé pingChan := make(chan error, 1) go func() { pingChan <- sqlDB.Ping() }() select { case err := <-pingChan: return err case <-time.After(timeout): return fmt.Errorf("database ping timeout after %v", timeout) } }