initial commit
This commit is contained in:
99
internal/pkg/database/database.go
Normal file
99
internal/pkg/database/database.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"base/config"
|
||||
"base/pkg/metrics"
|
||||
)
|
||||
|
||||
// NewRWDatabaseConnection creates a new database connection
|
||||
func NewRWDatabaseConnection(cfg *config.AppConfig, logger zerolog.Logger, metric *metrics.Metrics) (*gorm.DB, error) {
|
||||
start := time.Now()
|
||||
|
||||
lg := logger.
|
||||
With().
|
||||
Str("module", "database").
|
||||
Int("maxOpenConnection", cfg.Database.MaxOpenConns).
|
||||
Int("maxIdleConnection", cfg.Database.MaxIdleConns).
|
||||
Logger()
|
||||
|
||||
gormConfig := &gorm.Config{Logger: NewGormLogger(logger, time.Second*5)}
|
||||
|
||||
wrDB, sqlDB, err := wr(cfg, gormConfig)
|
||||
if err != nil {
|
||||
fmt.Println("[DATABASE CONNECTION ERROR]Failed to connect to database", err.Error())
|
||||
metric.RecordDatabaseQuery("ConnectWR", "database", time.Since(start), err)
|
||||
|
||||
lg.Error().
|
||||
Err(err).
|
||||
Msg("failed to connect to database")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metric.RecordDatabaseQuery("ConnectWR", "database", time.Since(start), nil)
|
||||
|
||||
// Start monitoring connection pool metrics
|
||||
go monitorConnectionPool(sqlDB, metric, logger)
|
||||
|
||||
duration := time.Since(start)
|
||||
metric.RecordDatabaseQuery("Connect", "database", duration, nil)
|
||||
|
||||
lg.Info().Msg("Database connection established")
|
||||
|
||||
return wrDB, nil
|
||||
}
|
||||
|
||||
func wr(config *config.AppConfig, gormConfig *gorm.Config) (*gorm.DB, *sql.DB, error) {
|
||||
// PostgreSQL DSN format: postgres://user:password@host:port/dbname?sslmode=disable
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=UTC",
|
||||
config.PgDatabaseConfig.Host,
|
||||
config.PgDatabaseConfig.User,
|
||||
config.PgDatabaseConfig.Password,
|
||||
config.PgDatabaseConfig.Name,
|
||||
config.PgDatabaseConfig.Port,
|
||||
config.PgDatabaseConfig.SSLMode,
|
||||
)
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), gormConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Get the underlying sql.DB
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sqlDB.SetMaxIdleConns(int(config.PgDatabaseConfig.PoolConfig.MinConn))
|
||||
sqlDB.SetMaxOpenConns(int(config.PgDatabaseConfig.PoolConfig.MaxConn))
|
||||
|
||||
// Parse and set connection timeouts from config
|
||||
// TODO: this is not type safe
|
||||
if config.PgDatabaseConfig.PoolConfig.MaxConnIdleTime.String() != "" {
|
||||
if idleTime, parseDurationErr := time.ParseDuration(config.PgDatabaseConfig.PoolConfig.MaxConnIdleTime.String()); parseDurationErr == nil {
|
||||
sqlDB.SetConnMaxIdleTime(idleTime)
|
||||
} else {
|
||||
sqlDB.SetConnMaxIdleTime(5 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
if config.PgDatabaseConfig.PoolConfig.MaxConnLifetime.String() != "" {
|
||||
if lifetime, parseDurationErr := time.ParseDuration(config.PgDatabaseConfig.PoolConfig.MaxConnLifetime.String()); parseDurationErr == nil {
|
||||
sqlDB.SetConnMaxLifetime(lifetime)
|
||||
} else {
|
||||
sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
return db, sqlDB, nil
|
||||
}
|
||||
94
internal/pkg/database/logger.go
Normal file
94
internal/pkg/database/logger.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type GormLogger struct {
|
||||
logger zerolog.Logger
|
||||
slowThreshold time.Duration
|
||||
logLevel logger.LogLevel
|
||||
}
|
||||
|
||||
func (l *GormLogger) LogMode(level logger.LogLevel) logger.Interface {
|
||||
newLogger := *l
|
||||
newLogger.logLevel = level
|
||||
return &newLogger
|
||||
}
|
||||
|
||||
func (l *GormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||
if l.logLevel >= logger.Info {
|
||||
l.logger.Info().Msgf(msg, data...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||
if l.logLevel >= logger.Warn {
|
||||
l.logger.Warn().Msgf(msg, data...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||
if l.logLevel >= logger.Error {
|
||||
l.logger.Error().Msgf(msg, data...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
||||
if l.logLevel <= logger.Silent {
|
||||
return
|
||||
}
|
||||
|
||||
elapsed := time.Since(begin)
|
||||
sql, rows := fc()
|
||||
|
||||
switch {
|
||||
// cache miss / record not found - expected, don't log as error
|
||||
case err != nil && errors.Is(err, gorm.ErrRecordNotFound):
|
||||
if l.logLevel >= logger.Info {
|
||||
l.logger.Debug().
|
||||
Str("sql", sql).
|
||||
Int64("rows", rows).
|
||||
Dur("elapsed", elapsed).
|
||||
Msg("QueryCacheMiss")
|
||||
}
|
||||
// error query
|
||||
case err != nil && l.logLevel >= logger.Error:
|
||||
l.logger.Error().
|
||||
Err(err).
|
||||
Str("sql", sql).
|
||||
Int64("rows", rows).
|
||||
Dur("elapsed", elapsed).
|
||||
Msg("QueryError")
|
||||
|
||||
// slow query
|
||||
case elapsed > l.slowThreshold && l.logLevel >= logger.Warn:
|
||||
l.logger.Warn().
|
||||
Str("sql", sql).
|
||||
Int64("rows", rows).
|
||||
Dur("elapsed", elapsed).
|
||||
Msg("SlowQuery")
|
||||
|
||||
// normal query
|
||||
case l.logLevel >= logger.Info:
|
||||
l.logger.Debug().
|
||||
Str("sql", sql).
|
||||
Int64("rows", rows).
|
||||
Dur("elapsed", elapsed).
|
||||
Msg("Query")
|
||||
}
|
||||
}
|
||||
|
||||
func NewGormLogger(serviceLogger zerolog.Logger, threshold time.Duration) *GormLogger {
|
||||
return &GormLogger{
|
||||
logger: serviceLogger.With().Str("module", "gorm").Logger(),
|
||||
slowThreshold: threshold,
|
||||
logLevel: logger.Warn, // default
|
||||
}
|
||||
}
|
||||
56
internal/pkg/database/utils.go
Normal file
56
internal/pkg/database/utils.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"base/pkg/metrics"
|
||||
)
|
||||
|
||||
// monitorConnectionPool periodically monitors and records connection pool metrics
|
||||
func monitorConnectionPool(sqlDB *sql.DB, metric *metrics.Metrics, logger zerolog.Logger) {
|
||||
ticker := time.NewTicker(30 * time.Second) // Monitor every 30 seconds
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
stats := sqlDB.Stats()
|
||||
|
||||
// Record connection pool metrics using available methods
|
||||
// Note: Connection pool size metrics are not available in current metrics package
|
||||
// Consider adding them if needed for monitoring
|
||||
|
||||
// Record wait time if there are any waits
|
||||
if stats.WaitCount > 0 {
|
||||
avgWaitTime := time.Duration(stats.WaitDuration.Nanoseconds() / stats.WaitCount)
|
||||
metric.RecordDatabaseQuery("WaitTime", "database", avgWaitTime, nil)
|
||||
}
|
||||
|
||||
// Log connection pool stats at info level for better visibility
|
||||
logger.Info().
|
||||
Int("open_connections", stats.OpenConnections).
|
||||
Int("in_use", stats.InUse).
|
||||
Int("idle", stats.Idle).
|
||||
Int("max_open", stats.MaxOpenConnections).
|
||||
Int64("wait_count", stats.WaitCount).
|
||||
Int64("wait_duration_ms", stats.WaitDuration.Milliseconds()).
|
||||
Msg("Database connection pool stats")
|
||||
|
||||
// Alert if we're approaching connection limits
|
||||
if stats.OpenConnections >= 7 { // 7 out of 8 max connections
|
||||
logger.Warn().
|
||||
Int("open_connections", stats.OpenConnections).
|
||||
Int("max_open", stats.MaxOpenConnections).
|
||||
Msg("Database connection pool approaching limit - consider reducing concurrent operations")
|
||||
}
|
||||
|
||||
// Alert if there are connection waits
|
||||
if stats.WaitCount > 0 {
|
||||
logger.Warn().
|
||||
Int64("wait_count", stats.WaitCount).
|
||||
Int64("wait_duration_ms", stats.WaitDuration.Milliseconds()).
|
||||
Msg("Database connections are being waited for - possible connection pool exhaustion")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user