Extract ~200 lines of duplicate database connection code from api/ntpdb/ and monitor/ntpdb/ into common/database/ package. Creates foundation for database consolidation while maintaining zero breaking changes. Files added: - config.go: Unified configuration with package-specific defaults - connector.go: Dynamic connector pattern from Boostport - pool.go: Configurable connection pool management - metrics.go: Optional Prometheus metrics integration - interfaces.go: Shared database interfaces for consistent patterns Key features: - Configuration-driven approach (API: 25/10 connections + metrics, Monitor: 10/5 connections, no metrics) - Optional Prometheus metrics when registerer provided - Backward compatibility via convenience functions - Flexible config file loading (explicit paths + search-based) Dependencies: Added mysql driver and yaml parsing for database configuration.
94 lines
2.8 KiB
Go
94 lines
2.8 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
// DatabaseMetrics holds the Prometheus metrics for database connection pool monitoring
|
|
type DatabaseMetrics struct {
|
|
ConnectionsOpen prometheus.Gauge
|
|
ConnectionsIdle prometheus.Gauge
|
|
ConnectionsInUse prometheus.Gauge
|
|
ConnectionsWaitCount prometheus.Counter
|
|
ConnectionsWaitDuration prometheus.Histogram
|
|
}
|
|
|
|
// NewDatabaseMetrics creates a new set of database metrics and registers them
|
|
func NewDatabaseMetrics(registerer prometheus.Registerer) *DatabaseMetrics {
|
|
metrics := &DatabaseMetrics{
|
|
ConnectionsOpen: prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "database_connections_open",
|
|
Help: "Number of open database connections",
|
|
}),
|
|
ConnectionsIdle: prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "database_connections_idle",
|
|
Help: "Number of idle database connections",
|
|
}),
|
|
ConnectionsInUse: prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "database_connections_in_use",
|
|
Help: "Number of database connections in use",
|
|
}),
|
|
ConnectionsWaitCount: prometheus.NewCounter(prometheus.CounterOpts{
|
|
Name: "database_connections_wait_count_total",
|
|
Help: "Total number of times a connection had to wait",
|
|
}),
|
|
ConnectionsWaitDuration: prometheus.NewHistogram(prometheus.HistogramOpts{
|
|
Name: "database_connections_wait_duration_seconds",
|
|
Help: "Time spent waiting for a database connection",
|
|
Buckets: prometheus.DefBuckets,
|
|
}),
|
|
}
|
|
|
|
if registerer != nil {
|
|
registerer.MustRegister(
|
|
metrics.ConnectionsOpen,
|
|
metrics.ConnectionsIdle,
|
|
metrics.ConnectionsInUse,
|
|
metrics.ConnectionsWaitCount,
|
|
metrics.ConnectionsWaitDuration,
|
|
)
|
|
}
|
|
|
|
return metrics
|
|
}
|
|
|
|
// monitorConnectionPool runs a background goroutine to collect connection pool metrics
|
|
func monitorConnectionPool(ctx context.Context, db *sql.DB, registerer prometheus.Registerer) {
|
|
if registerer == nil {
|
|
return // No metrics collection if no registerer provided
|
|
}
|
|
|
|
metrics := NewDatabaseMetrics(registerer)
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
stats := db.Stats()
|
|
|
|
metrics.ConnectionsOpen.Set(float64(stats.OpenConnections))
|
|
metrics.ConnectionsIdle.Set(float64(stats.Idle))
|
|
metrics.ConnectionsInUse.Set(float64(stats.InUse))
|
|
metrics.ConnectionsWaitCount.Add(float64(stats.WaitCount))
|
|
|
|
if stats.WaitDuration > 0 {
|
|
metrics.ConnectionsWaitDuration.Observe(stats.WaitDuration.Seconds())
|
|
}
|
|
|
|
// Log connection pool stats for high usage or waiting
|
|
if stats.OpenConnections > 20 || stats.WaitCount > 0 {
|
|
fmt.Printf("Connection pool stats: open=%d idle=%d in_use=%d wait_count=%d wait_duration=%s\n",
|
|
stats.OpenConnections, stats.Idle, stats.InUse, stats.WaitCount, stats.WaitDuration)
|
|
}
|
|
}
|
|
}
|
|
}
|