- Fix metrics double-counting: track deltas for WaitCount/WaitDuration instead of adding cumulative values each tick - Replace fmt.Printf with structured logging in pool monitor - Add PoolOptions validation (MaxConns > 0, MinConns >= 0) - Warn when DATABASE_URI overrides non-default PoolOptions - Improve findAndParseConfig to report all tried files and errors - Remove dead code in pgdb/config.go (unreachable host default) - Fix errcheck lint issues for file.Close() calls - Add context parameter to OpenDBMonitor() (breaking change)
188 lines
5.0 KiB
Go
188 lines
5.0 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql/driver"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/go-sql-driver/mysql"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/stdlib"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// from https://github.com/Boostport/dynamic-database-config
|
|
|
|
// CreateConnectorFunc is a function that creates a database connector
|
|
type CreateConnectorFunc func() (driver.Connector, error)
|
|
|
|
// Driver implements the sql/driver interface with dynamic configuration
|
|
type Driver struct {
|
|
CreateConnectorFunc CreateConnectorFunc
|
|
}
|
|
|
|
// Driver returns the driver instance
|
|
func (d Driver) Driver() driver.Driver {
|
|
return d
|
|
}
|
|
|
|
// Connect creates a new database connection using the dynamic connector
|
|
func (d Driver) Connect(ctx context.Context) (driver.Conn, error) {
|
|
connector, err := d.CreateConnectorFunc()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating connector from function: %w", err)
|
|
}
|
|
|
|
return connector.Connect(ctx)
|
|
}
|
|
|
|
// Open is not supported for dynamic configuration
|
|
func (d Driver) Open(name string) (driver.Conn, error) {
|
|
return nil, errors.New("open is not supported")
|
|
}
|
|
|
|
// createConnector creates a connector function that reads configuration from a file
|
|
func createConnector(configFile string) CreateConnectorFunc {
|
|
return func() (driver.Connector, error) {
|
|
dbFile, err := os.Open(configFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = dbFile.Close() }()
|
|
|
|
dec := yaml.NewDecoder(dbFile)
|
|
cfg := Config{}
|
|
|
|
err = dec.Decode(&cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate configuration
|
|
if err := cfg.Validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid configuration: %w", err)
|
|
}
|
|
|
|
// Determine database type and create appropriate connector
|
|
if cfg.MySQL != nil {
|
|
return createMySQLConnector(cfg.MySQL)
|
|
} else if cfg.Postgres != nil {
|
|
return createPostgresConnector(cfg.Postgres)
|
|
} else if cfg.User != "" && cfg.Name != "" {
|
|
// Legacy flat PostgreSQL format (requires at minimum user and dbname)
|
|
return createPostgresConnectorFromFlat(&cfg)
|
|
}
|
|
|
|
return nil, fmt.Errorf("no valid database configuration found (mysql or postgres section required)")
|
|
}
|
|
}
|
|
|
|
// createMySQLConnector creates a MySQL connector from configuration
|
|
func createMySQLConnector(cfg *MySQLConfig) (driver.Connector, error) {
|
|
dsn := cfg.DSN
|
|
if len(dsn) == 0 {
|
|
dsn = os.Getenv("DATABASE_DSN")
|
|
if len(dsn) == 0 {
|
|
return nil, fmt.Errorf("dsn config in database.yaml or DATABASE_DSN environment variable required")
|
|
}
|
|
}
|
|
|
|
dbcfg, err := mysql.ParseDSN(dsn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user := cfg.User; len(user) > 0 {
|
|
dbcfg.User = user
|
|
}
|
|
|
|
if pass := cfg.Pass; len(pass) > 0 {
|
|
dbcfg.Passwd = pass
|
|
}
|
|
|
|
if name := cfg.DBName; len(name) > 0 {
|
|
dbcfg.DBName = name
|
|
}
|
|
|
|
return mysql.NewConnector(dbcfg)
|
|
}
|
|
|
|
// createPostgresConnector creates a PostgreSQL connector from configuration
|
|
func createPostgresConnector(cfg *PostgresConfig) (driver.Connector, error) {
|
|
// Validate required fields
|
|
if cfg.Host == "" {
|
|
return nil, fmt.Errorf("postgres: host is required")
|
|
}
|
|
if cfg.User == "" {
|
|
return nil, fmt.Errorf("postgres: user is required")
|
|
}
|
|
if cfg.Name == "" {
|
|
return nil, fmt.Errorf("postgres: database name is required")
|
|
}
|
|
|
|
// Validate SSLMode
|
|
validSSLModes := map[string]bool{
|
|
"disable": true, "allow": true, "prefer": true,
|
|
"require": true, "verify-ca": true, "verify-full": true,
|
|
}
|
|
if cfg.SSLMode != "" && !validSSLModes[cfg.SSLMode] {
|
|
return nil, fmt.Errorf("postgres: invalid sslmode: %s", cfg.SSLMode)
|
|
}
|
|
|
|
// Build config directly (security: no DSN string with password)
|
|
connConfig, err := pgx.ParseConfig("")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("postgres: failed to create pgx config: %w", err)
|
|
}
|
|
|
|
connConfig.Host = cfg.Host
|
|
connConfig.Port = cfg.Port
|
|
connConfig.User = cfg.User
|
|
connConfig.Password = cfg.Pass
|
|
connConfig.Database = cfg.Name
|
|
|
|
// Map SSLMode to pgx configuration
|
|
// Note: pgx uses different SSL handling than libpq
|
|
// For now, we'll construct a minimal DSN with sslmode for ParseConfig
|
|
if cfg.SSLMode != "" {
|
|
// Reconstruct with sslmode only (no password in DSN)
|
|
dsnWithoutPassword := fmt.Sprintf("host=%s port=%d user=%s dbname=%s sslmode=%s",
|
|
cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.SSLMode)
|
|
connConfig, err = pgx.ParseConfig(dsnWithoutPassword)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("postgres: failed to parse config with sslmode: %w", err)
|
|
}
|
|
// Set password separately after parsing
|
|
connConfig.Password = cfg.Pass
|
|
}
|
|
|
|
return stdlib.GetConnector(*connConfig), nil
|
|
}
|
|
|
|
// createPostgresConnectorFromFlat creates a PostgreSQL connector from flat config format
|
|
func createPostgresConnectorFromFlat(cfg *Config) (driver.Connector, error) {
|
|
pgCfg := &PostgresConfig{
|
|
User: cfg.User,
|
|
Pass: cfg.Pass,
|
|
Host: cfg.Host,
|
|
Port: cfg.Port,
|
|
Name: cfg.Name,
|
|
SSLMode: cfg.SSLMode,
|
|
}
|
|
|
|
// Set defaults for PostgreSQL
|
|
if pgCfg.Host == "" {
|
|
pgCfg.Host = "localhost"
|
|
}
|
|
if pgCfg.Port == 0 {
|
|
pgCfg.Port = 5432
|
|
}
|
|
if pgCfg.SSLMode == "" {
|
|
pgCfg.SSLMode = "prefer"
|
|
}
|
|
|
|
return createPostgresConnector(pgCfg)
|
|
}
|