Files
ResolveSpec/pkg/dbmanager/README.md
2026-01-02 16:19:33 +02:00

532 lines
14 KiB
Markdown

# Database Connection Manager (dbmanager)
A comprehensive database connection manager for Go that provides centralized management of multiple named database connections with support for PostgreSQL, SQLite, MSSQL, and MongoDB.
## Features
- **Multiple Named Connections**: Manage multiple database connections with names like `primary`, `analytics`, `cache-db`
- **Multi-Database Support**: PostgreSQL, SQLite, Microsoft SQL Server, and MongoDB
- **Multi-ORM Access**: Each SQL connection provides access through:
- **Bun ORM** - Modern, lightweight ORM
- **GORM** - Popular Go ORM
- **Native** - Standard library `*sql.DB`
- All three share the same underlying connection pool
- **Configuration-Driven**: YAML configuration with Viper integration
- **Production-Ready Features**:
- Automatic health checks and reconnection
- Prometheus metrics
- Connection pooling with configurable limits
- Retry logic with exponential backoff
- Graceful shutdown
- OpenTelemetry tracing support
## Installation
```bash
go get github.com/bitechdev/ResolveSpec/pkg/dbmanager
```
## Quick Start
### 1. Configuration
Create a configuration file (e.g., `config.yaml`):
```yaml
dbmanager:
default_connection: "primary"
# Global connection pool defaults
max_open_conns: 25
max_idle_conns: 5
conn_max_lifetime: 30m
conn_max_idle_time: 5m
# Retry configuration
retry_attempts: 3
retry_delay: 1s
retry_max_delay: 10s
# Health checks
health_check_interval: 30s
enable_auto_reconnect: true
connections:
# Primary PostgreSQL connection
primary:
type: postgres
host: localhost
port: 5432
user: myuser
password: mypassword
database: myapp
sslmode: disable
default_orm: bun
enable_metrics: true
enable_tracing: true
enable_logging: true
# Read replica for analytics
analytics:
type: postgres
dsn: "postgres://readonly:pass@analytics:5432/analytics"
default_orm: bun
enable_metrics: true
# SQLite cache
cache-db:
type: sqlite
filepath: /var/lib/app/cache.db
max_open_conns: 1
# MongoDB for documents
documents:
type: mongodb
host: localhost
port: 27017
database: documents
user: mongouser
password: mongopass
auth_source: admin
enable_metrics: true
```
### 2. Initialize Manager
```go
package main
import (
"context"
"log"
"github.com/bitechdev/ResolveSpec/pkg/config"
"github.com/bitechdev/ResolveSpec/pkg/dbmanager"
)
func main() {
// Load configuration
cfgMgr := config.NewManager()
if err := cfgMgr.Load(); err != nil {
log.Fatal(err)
}
cfg, _ := cfgMgr.GetConfig()
// Create database manager
mgr, err := dbmanager.NewManager(cfg.DBManager)
if err != nil {
log.Fatal(err)
}
defer mgr.Close()
// Connect all databases
ctx := context.Background()
if err := mgr.Connect(ctx); err != nil {
log.Fatal(err)
}
// Your application code here...
}
```
### 3. Use Database Connections
#### Get Default Database
```go
// Get the default database (as configured common.Database interface)
db, err := mgr.GetDefaultDatabase()
if err != nil {
log.Fatal(err)
}
// Use it with any query
var users []User
err = db.NewSelect().
Model(&users).
Where("active = ?", true).
Scan(ctx, &users)
```
#### Get Named Connection with Specific ORM
```go
// Get primary connection
primary, err := mgr.Get("primary")
if err != nil {
log.Fatal(err)
}
// Use with Bun
bunDB, err := primary.Bun()
if err != nil {
log.Fatal(err)
}
err = bunDB.NewSelect().Model(&users).Scan(ctx)
// Use with GORM (same underlying connection!)
gormDB, err := primary.GORM()
if err != nil {
log.Fatal(err)
}
gormDB.Where("active = ?", true).Find(&users)
// Use native *sql.DB
nativeDB, err := primary.Native()
if err != nil {
log.Fatal(err)
}
rows, err := nativeDB.QueryContext(ctx, "SELECT * FROM users WHERE active = $1", true)
```
#### Use MongoDB
```go
// Get MongoDB connection
docs, err := mgr.Get("documents")
if err != nil {
log.Fatal(err)
}
mongoClient, err := docs.MongoDB()
if err != nil {
log.Fatal(err)
}
collection := mongoClient.Database("documents").Collection("articles")
// Use MongoDB driver...
```
#### Change Default Database
```go
// Switch to analytics database as default
err := mgr.SetDefaultDatabase("analytics")
if err != nil {
log.Fatal(err)
}
// Now GetDefaultDatabase() returns the analytics connection
db, _ := mgr.GetDefaultDatabase()
```
## Configuration Reference
### Manager Configuration
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `default_connection` | string | "" | Name of the default connection |
| `connections` | map | {} | Map of connection name to ConnectionConfig |
| `max_open_conns` | int | 25 | Global default for max open connections |
| `max_idle_conns` | int | 5 | Global default for max idle connections |
| `conn_max_lifetime` | duration | 30m | Global default for connection max lifetime |
| `conn_max_idle_time` | duration | 5m | Global default for connection max idle time |
| `retry_attempts` | int | 3 | Number of connection retry attempts |
| `retry_delay` | duration | 1s | Initial retry delay |
| `retry_max_delay` | duration | 10s | Maximum retry delay |
| `health_check_interval` | duration | 30s | Interval between health checks |
| `enable_auto_reconnect` | bool | true | Auto-reconnect on health check failure |
### Connection Configuration
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Unique connection name |
| `type` | string | Database type: `postgres`, `sqlite`, `mssql`, `mongodb` |
| `dsn` | string | Complete connection string (overrides individual params) |
| `host` | string | Database host |
| `port` | int | Database port |
| `user` | string | Username |
| `password` | string | Password |
| `database` | string | Database name |
| `sslmode` | string | SSL mode (postgres/mssql): `disable`, `require`, etc. |
| `schema` | string | Default schema (postgres/mssql) |
| `filepath` | string | File path (sqlite only) |
| `auth_source` | string | Auth source (mongodb) |
| `replica_set` | string | Replica set name (mongodb) |
| `read_preference` | string | Read preference (mongodb): `primary`, `secondary`, etc. |
| `max_open_conns` | int | Override global max open connections |
| `max_idle_conns` | int | Override global max idle connections |
| `conn_max_lifetime` | duration | Override global connection max lifetime |
| `conn_max_idle_time` | duration | Override global connection max idle time |
| `connect_timeout` | duration | Connection timeout (default: 10s) |
| `query_timeout` | duration | Query timeout (default: 30s) |
| `enable_tracing` | bool | Enable OpenTelemetry tracing |
| `enable_metrics` | bool | Enable Prometheus metrics |
| `enable_logging` | bool | Enable connection logging |
| `default_orm` | string | Default ORM for Database(): `bun`, `gorm`, `native` |
| `tags` | map[string]string | Custom tags for filtering/organization |
## Advanced Usage
### Health Checks
```go
// Manual health check
if err := mgr.HealthCheck(ctx); err != nil {
log.Printf("Health check failed: %v", err)
}
// Per-connection health check
primary, _ := mgr.Get("primary")
if err := primary.HealthCheck(ctx); err != nil {
log.Printf("Primary connection unhealthy: %v", err)
// Manual reconnect
if err := primary.Reconnect(ctx); err != nil {
log.Printf("Reconnection failed: %v", err)
}
}
```
### Connection Statistics
```go
// Get overall statistics
stats := mgr.Stats()
fmt.Printf("Total connections: %d\n", stats.TotalConnections)
fmt.Printf("Healthy: %d, Unhealthy: %d\n", stats.HealthyCount, stats.UnhealthyCount)
// Per-connection stats
for name, connStats := range stats.ConnectionStats {
fmt.Printf("%s: %d open, %d in use, %d idle\n",
name,
connStats.OpenConnections,
connStats.InUse,
connStats.Idle)
}
// Individual connection stats
primary, _ := mgr.Get("primary")
stats := primary.Stats()
fmt.Printf("Wait count: %d, Wait duration: %v\n",
stats.WaitCount,
stats.WaitDuration)
```
### Prometheus Metrics
The package automatically exports Prometheus metrics:
- `dbmanager_connections_total` - Total configured connections by type
- `dbmanager_connection_status` - Connection health status (1=healthy, 0=unhealthy)
- `dbmanager_connection_pool_size` - Connection pool statistics by state
- `dbmanager_connection_wait_count` - Times connections waited for availability
- `dbmanager_connection_wait_duration_seconds` - Total wait duration
- `dbmanager_health_check_duration_seconds` - Health check execution time
- `dbmanager_reconnect_attempts_total` - Reconnection attempts and results
- `dbmanager_connection_lifetime_closed_total` - Connections closed due to max lifetime
- `dbmanager_connection_idle_closed_total` - Connections closed due to max idle time
Metrics are automatically updated during health checks. To manually publish metrics:
```go
if mgr, ok := mgr.(*connectionManager); ok {
mgr.PublishMetrics()
}
```
## Architecture
### Single Connection Pool, Multiple ORMs
A key design principle is that Bun, GORM, and Native all wrap the **same underlying `*sql.DB`** connection pool:
```
┌─────────────────────────────────────┐
│ SQL Connection │
├─────────────────────────────────────┤
│ ┌─────────┐ ┌──────┐ ┌────────┐ │
│ │ Bun │ │ GORM │ │ Native │ │
│ └────┬────┘ └───┬──┘ └───┬────┘ │
│ │ │ │ │
│ └───────────┴─────────┘ │
│ *sql.DB │
│ (single pool) │
└─────────────────────────────────────┘
```
**Benefits:**
- No connection duplication
- Consistent pool limits across all ORMs
- Unified connection statistics
- Lower resource usage
### Provider Pattern
Each database type has a dedicated provider:
- **PostgresProvider** - Uses `pgx` driver
- **SQLiteProvider** - Uses `glebarez/sqlite` (pure Go)
- **MSSQLProvider** - Uses `go-mssqldb`
- **MongoProvider** - Uses official `mongo-driver`
Providers handle:
- Connection establishment with retry logic
- Health checking
- Connection statistics
- Connection cleanup
## Best Practices
1. **Use Named Connections**: Be explicit about which database you're accessing
```go
primary, _ := mgr.Get("primary") // Good
db, _ := mgr.GetDefaultDatabase() // Risky if default changes
```
2. **Configure Connection Pools**: Tune based on your workload
```yaml
connections:
primary:
max_open_conns: 100 # High traffic API
max_idle_conns: 25
analytics:
max_open_conns: 10 # Background analytics
max_idle_conns: 2
```
3. **Enable Health Checks**: Catch connection issues early
```yaml
health_check_interval: 30s
enable_auto_reconnect: true
```
4. **Use Appropriate ORM**: Choose based on your needs
- **Bun**: Modern, fast, type-safe - recommended for new code
- **GORM**: Mature, feature-rich - good for existing GORM code
- **Native**: Maximum control - use for performance-critical queries
5. **Monitor Metrics**: Watch connection pool utilization
- If `wait_count` is high, increase `max_open_conns`
- If `idle` is always high, decrease `max_idle_conns`
## Troubleshooting
### Connection Failures
If connections fail to establish:
1. Check configuration:
```bash
# Test connection manually
psql -h localhost -U myuser -d myapp
```
2. Enable logging:
```yaml
connections:
primary:
enable_logging: true
```
3. Check retry attempts:
```yaml
retry_attempts: 5 # Increase retries
retry_max_delay: 30s
```
### Pool Exhaustion
If you see "too many connections" errors:
1. Increase pool size:
```yaml
max_open_conns: 50 # Increase from default 25
```
2. Reduce connection lifetime:
```yaml
conn_max_lifetime: 15m # Recycle faster
```
3. Monitor wait stats:
```go
stats := primary.Stats()
if stats.WaitCount > 1000 {
log.Warn("High connection wait count")
}
```
### MongoDB vs SQL Confusion
MongoDB connections don't support SQL ORMs:
```go
docs, _ := mgr.Get("documents")
// ✓ Correct
mongoClient, _ := docs.MongoDB()
// ✗ Error: ErrNotSQLDatabase
bunDB, err := docs.Bun() // Won't work!
```
SQL connections don't support MongoDB:
```go
primary, _ := mgr.Get("primary")
// ✓ Correct
bunDB, _ := primary.Bun()
// ✗ Error: ErrNotMongoDB
mongoClient, err := primary.MongoDB() // Won't work!
```
## Migration Guide
### From Raw `database/sql`
Before:
```go
db, err := sql.Open("postgres", dsn)
defer db.Close()
rows, err := db.Query("SELECT * FROM users")
```
After:
```go
mgr, _ := dbmanager.NewManager(cfg.DBManager)
mgr.Connect(ctx)
defer mgr.Close()
primary, _ := mgr.Get("primary")
nativeDB, _ := primary.Native()
rows, err := nativeDB.Query("SELECT * FROM users")
```
### From Direct Bun/GORM
Before:
```go
sqldb, _ := sql.Open("pgx", dsn)
bunDB := bun.NewDB(sqldb, pgdialect.New())
var users []User
bunDB.NewSelect().Model(&users).Scan(ctx)
```
After:
```go
mgr, _ := dbmanager.NewManager(cfg.DBManager)
mgr.Connect(ctx)
primary, _ := mgr.Get("primary")
bunDB, _ := primary.Bun()
var users []User
bunDB.NewSelect().Model(&users).Scan(ctx)
```
## License
Same as the parent project.
## Contributing
Contributions are welcome! Please submit issues and pull requests to the main repository.