initial commit

This commit is contained in:
m.zare
2026-04-10 18:25:21 +03:30
commit 77ca6c34a3
263 changed files with 34470 additions and 0 deletions

View 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
}

View 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
}
}

View 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")
}
}
}