mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-01-06 03:44:25 +00:00
532 lines
14 KiB
Markdown
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.
|