mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-01-09 13:04:24 +00:00
feat(dbmanager): ✨ Database connection Manager
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -54,6 +54,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"conventionalCommits.scopes": [
|
"conventionalCommits.scopes": [
|
||||||
"spectypes"
|
"spectypes",
|
||||||
|
"dbmanager"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/config"
|
"github.com/bitechdev/ResolveSpec/pkg/config"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/dbmanager"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
|
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/server"
|
"github.com/bitechdev/ResolveSpec/pkg/server"
|
||||||
@@ -15,7 +17,6 @@ import (
|
|||||||
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
|
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
gormlog "gorm.io/gorm/logger"
|
gormlog "gorm.io/gorm/logger"
|
||||||
)
|
)
|
||||||
@@ -40,12 +41,14 @@ func main() {
|
|||||||
logger.Info("ResolveSpec test server starting")
|
logger.Info("ResolveSpec test server starting")
|
||||||
logger.Info("Configuration loaded - Server will listen on: %s", cfg.Server.Addr)
|
logger.Info("Configuration loaded - Server will listen on: %s", cfg.Server.Addr)
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database manager
|
||||||
db, err := initDB(cfg)
|
ctx := context.Background()
|
||||||
|
dbMgr, db, err := initDB(ctx, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to initialize database: %+v", err)
|
logger.Error("Failed to initialize database: %+v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
defer dbMgr.Close()
|
||||||
|
|
||||||
// Create router
|
// Create router
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
@@ -117,7 +120,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDB(cfg *config.Config) (*gorm.DB, error) {
|
func initDB(ctx context.Context, cfg *config.Config) (dbmanager.Manager, *gorm.DB, error) {
|
||||||
// Configure GORM logger based on config
|
// Configure GORM logger based on config
|
||||||
logLevel := gormlog.Info
|
logLevel := gormlog.Info
|
||||||
if !cfg.Logger.Dev {
|
if !cfg.Logger.Dev {
|
||||||
@@ -135,25 +138,41 @@ func initDB(cfg *config.Config) (*gorm.DB, error) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Use database URL from config if available, otherwise use default SQLite
|
// Create database manager from config
|
||||||
dbURL := cfg.Database.URL
|
mgr, err := dbmanager.NewManager(dbmanager.FromConfig(cfg.DBManager))
|
||||||
if dbURL == "" {
|
if err != nil {
|
||||||
dbURL = "test.db"
|
return nil, nil, fmt.Errorf("failed to create database manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create SQLite database
|
// Connect all databases
|
||||||
db, err := gorm.Open(sqlite.Open(dbURL), &gorm.Config{Logger: newLogger, FullSaveAssociations: false})
|
if err := mgr.Connect(ctx); err != nil {
|
||||||
if err != nil {
|
return nil, nil, fmt.Errorf("failed to connect databases: %w", err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get default connection
|
||||||
|
conn, err := mgr.GetDefault()
|
||||||
|
if err != nil {
|
||||||
|
mgr.Close()
|
||||||
|
return nil, nil, fmt.Errorf("failed to get default connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get GORM database
|
||||||
|
gormDB, err := conn.GORM()
|
||||||
|
if err != nil {
|
||||||
|
mgr.Close()
|
||||||
|
return nil, nil, fmt.Errorf("failed to get GORM database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update GORM logger
|
||||||
|
gormDB.Logger = newLogger
|
||||||
|
|
||||||
modelList := testmodels.GetTestModels()
|
modelList := testmodels.GetTestModels()
|
||||||
|
|
||||||
// Auto migrate schemas
|
// Auto migrate schemas
|
||||||
err = db.AutoMigrate(modelList...)
|
if err := gormDB.AutoMigrate(modelList...); err != nil {
|
||||||
if err != nil {
|
mgr.Close()
|
||||||
return nil, err
|
return nil, nil, fmt.Errorf("failed to auto migrate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return db, nil
|
return mgr, gormDB, nil
|
||||||
}
|
}
|
||||||
|
|||||||
24
config.yaml
24
config.yaml
@@ -37,5 +37,25 @@ cors:
|
|||||||
tracing:
|
tracing:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
database:
|
# Database Manager Configuration
|
||||||
url: "" # Empty means use default SQLite (test.db)
|
dbmanager:
|
||||||
|
default_connection: "primary"
|
||||||
|
max_open_conns: 25
|
||||||
|
max_idle_conns: 5
|
||||||
|
conn_max_lifetime: 30m
|
||||||
|
conn_max_idle_time: 5m
|
||||||
|
retry_attempts: 3
|
||||||
|
retry_delay: 1s
|
||||||
|
health_check_interval: 30s
|
||||||
|
enable_auto_reconnect: true
|
||||||
|
|
||||||
|
connections:
|
||||||
|
primary:
|
||||||
|
name: "primary"
|
||||||
|
type: "sqlite"
|
||||||
|
filepath: "test.db"
|
||||||
|
default_orm: "gorm"
|
||||||
|
enable_logging: true
|
||||||
|
enable_metrics: false
|
||||||
|
connect_timeout: 10s
|
||||||
|
query_timeout: 30s
|
||||||
|
|||||||
15
go.mod
15
go.mod
@@ -16,6 +16,7 @@ require (
|
|||||||
github.com/jackc/pgx/v5 v5.6.0
|
github.com/jackc/pgx/v5 v5.6.0
|
||||||
github.com/klauspost/compress v1.18.0
|
github.com/klauspost/compress v1.18.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
|
github.com/microsoft/go-mssqldb v1.9.5
|
||||||
github.com/mochi-mqtt/server/v2 v2.7.9
|
github.com/mochi-mqtt/server/v2 v2.7.9
|
||||||
github.com/nats-io/nats.go v1.48.0
|
github.com/nats-io/nats.go v1.48.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
@@ -26,9 +27,12 @@ require (
|
|||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/tidwall/sjson v1.2.5
|
github.com/tidwall/sjson v1.2.5
|
||||||
github.com/uptrace/bun v1.2.16
|
github.com/uptrace/bun v1.2.16
|
||||||
|
github.com/uptrace/bun/dialect/mssqldialect v1.2.16
|
||||||
|
github.com/uptrace/bun/dialect/pgdialect v1.2.16
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16
|
||||||
github.com/uptrace/bun/driver/sqliteshim v1.2.16
|
github.com/uptrace/bun/driver/sqliteshim v1.2.16
|
||||||
github.com/uptrace/bunrouter v1.0.23
|
github.com/uptrace/bunrouter v1.0.23
|
||||||
|
go.mongodb.org/mongo-driver v1.17.6
|
||||||
go.opentelemetry.io/otel v1.38.0
|
go.opentelemetry.io/otel v1.38.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0
|
||||||
@@ -39,6 +43,7 @@ require (
|
|||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.14.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
|
gorm.io/driver/sqlserver v1.6.3
|
||||||
gorm.io/gorm v1.30.0
|
gorm.io/gorm v1.30.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,6 +75,9 @@ require (
|
|||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
@@ -86,6 +94,7 @@ require (
|
|||||||
github.com/moby/sys/user v0.4.0 // indirect
|
github.com/moby/sys/user v0.4.0 // indirect
|
||||||
github.com/moby/sys/userns v0.1.0 // indirect
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.0 // indirect
|
github.com/moby/term v0.5.0 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||||
@@ -105,6 +114,7 @@ require (
|
|||||||
github.com/rs/xid v1.4.0 // indirect
|
github.com/rs/xid v1.4.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
|
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
@@ -119,6 +129,10 @@ require (
|
|||||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||||
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||||
@@ -128,6 +142,7 @@ require (
|
|||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
||||||
|
golang.org/x/mod v0.30.0 // indirect
|
||||||
golang.org/x/net v0.45.0 // indirect
|
golang.org/x/net v0.45.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
|||||||
190
go.sum
190
go.sum
@@ -2,8 +2,32 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
|||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
@@ -32,6 +56,7 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS
|
|||||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -41,6 +66,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
|||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||||
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
@@ -76,21 +103,37 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
|||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
|
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
@@ -101,6 +144,12 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
|||||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
|
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||||
|
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||||
|
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||||
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
|
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
|
||||||
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
@@ -110,8 +159,11 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
|
|||||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
@@ -124,6 +176,9 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
|
||||||
|
github.com/microsoft/go-mssqldb v1.9.5 h1:orwya0X/5bsL1o+KasupTkk2eNTNFkTQG0BEe/HxCn0=
|
||||||
|
github.com/microsoft/go-mssqldb v1.9.5/go.mod h1:VCP2a0KEZZtGLRHd1PsLavLFYy/3xX2yJUPycv3Sr2Q=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||||
@@ -142,6 +197,10 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
|||||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||||
github.com/mochi-mqtt/server/v2 v2.7.9 h1:y0g4vrSLAag7T07l2oCzOa/+nKVLoazKEWAArwqBNYI=
|
github.com/mochi-mqtt/server/v2 v2.7.9 h1:y0g4vrSLAag7T07l2oCzOa/+nKVLoazKEWAArwqBNYI=
|
||||||
github.com/mochi-mqtt/server/v2 v2.7.9/go.mod h1:lZD3j35AVNqJL5cezlnSkuG05c0FCHSsfAKSPBOSbqc=
|
github.com/mochi-mqtt/server/v2 v2.7.9/go.mod h1:lZD3j35AVNqJL5cezlnSkuG05c0FCHSsfAKSPBOSbqc=
|
||||||
|
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||||
|
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
|
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||||
|
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
@@ -162,6 +221,10 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||||
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -182,6 +245,8 @@ github.com/redis/go-redis/v9 v9.17.1 h1:7tl732FjYPRT9H9aNfyTwKg9iTETjWjGKEJ2t/5i
|
|||||||
github.com/redis/go-redis/v9 v9.17.1/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
github.com/redis/go-redis/v9 v9.17.1/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
||||||
@@ -190,6 +255,8 @@ github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDc
|
|||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
@@ -203,10 +270,18 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
|
|||||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
@@ -228,6 +303,10 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
|
|||||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||||
|
github.com/uptrace/bun/dialect/mssqldialect v1.2.16 h1:rKv0cKPNBviXadB/+2Y/UedA/c1JnwGzUWZkdN5FdSQ=
|
||||||
|
github.com/uptrace/bun/dialect/mssqldialect v1.2.16/go.mod h1:J5U7tGKWDsx2Q7MwDZF2417jCdpD6yD/ZMFJcCR80bk=
|
||||||
|
github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI9/Z5tUv3DE1NVL4=
|
||||||
|
github.com/uptrace/bun/dialect/pgdialect v1.2.16/go.mod h1:IJdMeV4sLfh0LDUZl7TIxLI0LipF1vwTK3hBC7p5qLo=
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 h1:6wVAiYLj1pMibRthGwy4wDLa3D5AQo32Y8rvwPd8CQ0=
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 h1:6wVAiYLj1pMibRthGwy4wDLa3D5AQo32Y8rvwPd8CQ0=
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16/go.mod h1:Z7+5qK8CGZkDQiPMu+LSdVuDuR1I5jcwtkB1Pi3F82E=
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16/go.mod h1:Z7+5qK8CGZkDQiPMu+LSdVuDuR1I5jcwtkB1Pi3F82E=
|
||||||
github.com/uptrace/bun/driver/sqliteshim v1.2.16 h1:M6Dh5kkDWFbUWBrOsIE1g1zdZ5JbSytTD4piFRBOUAI=
|
github.com/uptrace/bun/driver/sqliteshim v1.2.16 h1:M6Dh5kkDWFbUWBrOsIE1g1zdZ5JbSytTD4piFRBOUAI=
|
||||||
@@ -240,8 +319,19 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh
|
|||||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
github.com/warkanum/bun v1.2.17 h1:HP8eTuKSNcqMDhhIPFxEbgV/yct6RR0/c3qHH3PNZUA=
|
github.com/warkanum/bun v1.2.17 h1:HP8eTuKSNcqMDhhIPFxEbgV/yct6RR0/c3qHH3PNZUA=
|
||||||
github.com/warkanum/bun v1.2.17/go.mod h1:jMoNg2n56ckaawi/O/J92BHaECmrz6IRjuMWqlMaMTM=
|
github.com/warkanum/bun v1.2.17/go.mod h1:jMoNg2n56ckaawi/O/J92BHaECmrz6IRjuMWqlMaMTM=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||||
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||||
|
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||||
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||||
|
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||||
@@ -274,33 +364,127 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
|||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||||
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||||
|
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||||
|
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
@@ -315,6 +499,10 @@ google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXn
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -322,6 +510,8 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
|||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
|
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
|
||||||
|
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
|
||||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/uptrace/bun/dialect/mssqldialect"
|
||||||
|
"github.com/uptrace/bun/dialect/pgdialect"
|
||||||
|
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/driver/sqlserver"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// parseTableName splits a table name that may contain schema into separate schema and table
|
// parseTableName splits a table name that may contain schema into separate schema and table
|
||||||
@@ -14,3 +23,39 @@ func parseTableName(fullTableName string) (schema, table string) {
|
|||||||
}
|
}
|
||||||
return "", fullTableName
|
return "", fullTableName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPostgresDialect returns a Bun PostgreSQL dialect
|
||||||
|
func GetPostgresDialect() *pgdialect.Dialect {
|
||||||
|
return pgdialect.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSQLiteDialect returns a Bun SQLite dialect
|
||||||
|
func GetSQLiteDialect() *sqlitedialect.Dialect {
|
||||||
|
return sqlitedialect.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMSSQLDialect returns a Bun MSSQL dialect
|
||||||
|
func GetMSSQLDialect() *mssqldialect.Dialect {
|
||||||
|
return mssqldialect.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPostgresDialector returns a GORM PostgreSQL dialector
|
||||||
|
func GetPostgresDialector(db *sql.DB) gorm.Dialector {
|
||||||
|
return postgres.New(postgres.Config{
|
||||||
|
Conn: db,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSQLiteDialector returns a GORM SQLite dialector
|
||||||
|
func GetSQLiteDialector(db *sql.DB) gorm.Dialector {
|
||||||
|
return sqlite.Dialector{
|
||||||
|
Conn: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMSSQLDialector returns a GORM MSSQL dialector
|
||||||
|
func GetMSSQLDialector(db *sql.DB) gorm.Dialector {
|
||||||
|
return sqlserver.New(sqlserver.Config{
|
||||||
|
Conn: db,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ type Config struct {
|
|||||||
ErrorTracking ErrorTrackingConfig `mapstructure:"error_tracking"`
|
ErrorTracking ErrorTrackingConfig `mapstructure:"error_tracking"`
|
||||||
Middleware MiddlewareConfig `mapstructure:"middleware"`
|
Middleware MiddlewareConfig `mapstructure:"middleware"`
|
||||||
CORS CORSConfig `mapstructure:"cors"`
|
CORS CORSConfig `mapstructure:"cors"`
|
||||||
Database DatabaseConfig `mapstructure:"database"`
|
|
||||||
EventBroker EventBrokerConfig `mapstructure:"event_broker"`
|
EventBroker EventBrokerConfig `mapstructure:"event_broker"`
|
||||||
|
DBManager DBManagerConfig `mapstructure:"dbmanager"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig holds server-related configuration
|
// ServerConfig holds server-related configuration
|
||||||
@@ -76,11 +76,6 @@ type CORSConfig struct {
|
|||||||
MaxAge int `mapstructure:"max_age"`
|
MaxAge int `mapstructure:"max_age"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DatabaseConfig holds database configuration (primarily for testing)
|
|
||||||
type DatabaseConfig struct {
|
|
||||||
URL string `mapstructure:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorTrackingConfig holds error tracking configuration
|
// ErrorTrackingConfig holds error tracking configuration
|
||||||
type ErrorTrackingConfig struct {
|
type ErrorTrackingConfig struct {
|
||||||
Enabled bool `mapstructure:"enabled"`
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
|||||||
107
pkg/config/dbmanager.go
Normal file
107
pkg/config/dbmanager.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DBManagerConfig contains configuration for the database connection manager
|
||||||
|
type DBManagerConfig struct {
|
||||||
|
// DefaultConnection is the name of the default connection to use
|
||||||
|
DefaultConnection string `mapstructure:"default_connection"`
|
||||||
|
|
||||||
|
// Connections is a map of connection name to connection configuration
|
||||||
|
Connections map[string]DBConnectionConfig `mapstructure:"connections"`
|
||||||
|
|
||||||
|
// Global connection pool defaults
|
||||||
|
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||||
|
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||||
|
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
|
||||||
|
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"`
|
||||||
|
|
||||||
|
// Retry policy
|
||||||
|
RetryAttempts int `mapstructure:"retry_attempts"`
|
||||||
|
RetryDelay time.Duration `mapstructure:"retry_delay"`
|
||||||
|
RetryMaxDelay time.Duration `mapstructure:"retry_max_delay"`
|
||||||
|
|
||||||
|
// Health checks
|
||||||
|
HealthCheckInterval time.Duration `mapstructure:"health_check_interval"`
|
||||||
|
EnableAutoReconnect bool `mapstructure:"enable_auto_reconnect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DBConnectionConfig defines configuration for a single database connection
|
||||||
|
type DBConnectionConfig struct {
|
||||||
|
// Name is the unique name of this connection
|
||||||
|
Name string `mapstructure:"name"`
|
||||||
|
|
||||||
|
// Type is the database type (postgres, sqlite, mssql, mongodb)
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
|
||||||
|
// DSN is the complete Data Source Name / connection string
|
||||||
|
// If provided, this takes precedence over individual connection parameters
|
||||||
|
DSN string `mapstructure:"dsn"`
|
||||||
|
|
||||||
|
// Connection parameters (used if DSN is not provided)
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
User string `mapstructure:"user"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
Database string `mapstructure:"database"`
|
||||||
|
|
||||||
|
// PostgreSQL/MSSQL specific
|
||||||
|
SSLMode string `mapstructure:"sslmode"` // disable, require, verify-ca, verify-full
|
||||||
|
Schema string `mapstructure:"schema"` // Default schema
|
||||||
|
|
||||||
|
// SQLite specific
|
||||||
|
FilePath string `mapstructure:"filepath"`
|
||||||
|
|
||||||
|
// MongoDB specific
|
||||||
|
AuthSource string `mapstructure:"auth_source"`
|
||||||
|
ReplicaSet string `mapstructure:"replica_set"`
|
||||||
|
ReadPreference string `mapstructure:"read_preference"` // primary, secondary, etc.
|
||||||
|
|
||||||
|
// Connection pool settings (overrides global defaults)
|
||||||
|
MaxOpenConns *int `mapstructure:"max_open_conns"`
|
||||||
|
MaxIdleConns *int `mapstructure:"max_idle_conns"`
|
||||||
|
ConnMaxLifetime *time.Duration `mapstructure:"conn_max_lifetime"`
|
||||||
|
ConnMaxIdleTime *time.Duration `mapstructure:"conn_max_idle_time"`
|
||||||
|
|
||||||
|
// Timeouts
|
||||||
|
ConnectTimeout time.Duration `mapstructure:"connect_timeout"`
|
||||||
|
QueryTimeout time.Duration `mapstructure:"query_timeout"`
|
||||||
|
|
||||||
|
// Features
|
||||||
|
EnableTracing bool `mapstructure:"enable_tracing"`
|
||||||
|
EnableMetrics bool `mapstructure:"enable_metrics"`
|
||||||
|
EnableLogging bool `mapstructure:"enable_logging"`
|
||||||
|
|
||||||
|
// DefaultORM specifies which ORM to use for the Database() method
|
||||||
|
// Options: "bun", "gorm", "native"
|
||||||
|
DefaultORM string `mapstructure:"default_orm"`
|
||||||
|
|
||||||
|
// Tags for organization and filtering
|
||||||
|
Tags map[string]string `mapstructure:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToManagerConfig converts config.DBManagerConfig to dbmanager.ManagerConfig
|
||||||
|
// This is used to avoid circular dependencies
|
||||||
|
func (c *DBManagerConfig) ToManagerConfig() interface{} {
|
||||||
|
// This will be implemented in the dbmanager package
|
||||||
|
// to convert from config types to dbmanager types
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the DBManager configuration
|
||||||
|
func (c *DBManagerConfig) Validate() error {
|
||||||
|
if len(c.Connections) == 0 {
|
||||||
|
return fmt.Errorf("at least one connection must be configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DefaultConnection != "" {
|
||||||
|
if _, ok := c.Connections[c.DefaultConnection]; !ok {
|
||||||
|
return fmt.Errorf("default connection '%s' not found in connections", c.DefaultConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
531
pkg/dbmanager/README.md
Normal file
531
pkg/dbmanager/README.md
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
# 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.
|
||||||
448
pkg/dbmanager/config.go
Normal file
448
pkg/dbmanager/config.go
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
package dbmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DatabaseType represents the type of database
|
||||||
|
type DatabaseType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DatabaseTypePostgreSQL represents PostgreSQL database
|
||||||
|
DatabaseTypePostgreSQL DatabaseType = "postgres"
|
||||||
|
|
||||||
|
// DatabaseTypeSQLite represents SQLite database
|
||||||
|
DatabaseTypeSQLite DatabaseType = "sqlite"
|
||||||
|
|
||||||
|
// DatabaseTypeMSSQL represents Microsoft SQL Server database
|
||||||
|
DatabaseTypeMSSQL DatabaseType = "mssql"
|
||||||
|
|
||||||
|
// DatabaseTypeMongoDB represents MongoDB database
|
||||||
|
DatabaseTypeMongoDB DatabaseType = "mongodb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ORMType represents the ORM to use for database operations
|
||||||
|
type ORMType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ORMTypeBun represents Bun ORM
|
||||||
|
ORMTypeBun ORMType = "bun"
|
||||||
|
|
||||||
|
// ORMTypeGORM represents GORM
|
||||||
|
ORMTypeGORM ORMType = "gorm"
|
||||||
|
|
||||||
|
// ORMTypeNative represents native database/sql
|
||||||
|
ORMTypeNative ORMType = "native"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ManagerConfig contains configuration for the database connection manager
|
||||||
|
type ManagerConfig struct {
|
||||||
|
// DefaultConnection is the name of the default connection to use
|
||||||
|
DefaultConnection string `mapstructure:"default_connection"`
|
||||||
|
|
||||||
|
// Connections is a map of connection name to connection configuration
|
||||||
|
Connections map[string]ConnectionConfig `mapstructure:"connections"`
|
||||||
|
|
||||||
|
// Global connection pool defaults
|
||||||
|
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||||
|
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||||
|
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
|
||||||
|
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"`
|
||||||
|
|
||||||
|
// Retry policy
|
||||||
|
RetryAttempts int `mapstructure:"retry_attempts"`
|
||||||
|
RetryDelay time.Duration `mapstructure:"retry_delay"`
|
||||||
|
RetryMaxDelay time.Duration `mapstructure:"retry_max_delay"`
|
||||||
|
|
||||||
|
// Health checks
|
||||||
|
HealthCheckInterval time.Duration `mapstructure:"health_check_interval"`
|
||||||
|
EnableAutoReconnect bool `mapstructure:"enable_auto_reconnect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionConfig defines configuration for a single database connection
|
||||||
|
type ConnectionConfig struct {
|
||||||
|
// Name is the unique name of this connection
|
||||||
|
Name string `mapstructure:"name"`
|
||||||
|
|
||||||
|
// Type is the database type (postgres, sqlite, mssql, mongodb)
|
||||||
|
Type DatabaseType `mapstructure:"type"`
|
||||||
|
|
||||||
|
// DSN is the complete Data Source Name / connection string
|
||||||
|
// If provided, this takes precedence over individual connection parameters
|
||||||
|
DSN string `mapstructure:"dsn"`
|
||||||
|
|
||||||
|
// Connection parameters (used if DSN is not provided)
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
User string `mapstructure:"user"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
Database string `mapstructure:"database"`
|
||||||
|
|
||||||
|
// PostgreSQL/MSSQL specific
|
||||||
|
SSLMode string `mapstructure:"sslmode"` // disable, require, verify-ca, verify-full
|
||||||
|
Schema string `mapstructure:"schema"` // Default schema
|
||||||
|
|
||||||
|
// SQLite specific
|
||||||
|
FilePath string `mapstructure:"filepath"`
|
||||||
|
|
||||||
|
// MongoDB specific
|
||||||
|
AuthSource string `mapstructure:"auth_source"`
|
||||||
|
ReplicaSet string `mapstructure:"replica_set"`
|
||||||
|
ReadPreference string `mapstructure:"read_preference"` // primary, secondary, etc.
|
||||||
|
|
||||||
|
// Connection pool settings (overrides global defaults)
|
||||||
|
MaxOpenConns *int `mapstructure:"max_open_conns"`
|
||||||
|
MaxIdleConns *int `mapstructure:"max_idle_conns"`
|
||||||
|
ConnMaxLifetime *time.Duration `mapstructure:"conn_max_lifetime"`
|
||||||
|
ConnMaxIdleTime *time.Duration `mapstructure:"conn_max_idle_time"`
|
||||||
|
|
||||||
|
// Timeouts
|
||||||
|
ConnectTimeout time.Duration `mapstructure:"connect_timeout"`
|
||||||
|
QueryTimeout time.Duration `mapstructure:"query_timeout"`
|
||||||
|
|
||||||
|
// Features
|
||||||
|
EnableTracing bool `mapstructure:"enable_tracing"`
|
||||||
|
EnableMetrics bool `mapstructure:"enable_metrics"`
|
||||||
|
EnableLogging bool `mapstructure:"enable_logging"`
|
||||||
|
|
||||||
|
// DefaultORM specifies which ORM to use for the Database() method
|
||||||
|
// Options: "bun", "gorm", "native"
|
||||||
|
DefaultORM string `mapstructure:"default_orm"`
|
||||||
|
|
||||||
|
// Tags for organization and filtering
|
||||||
|
Tags map[string]string `mapstructure:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultManagerConfig returns a ManagerConfig with sensible defaults
|
||||||
|
func DefaultManagerConfig() ManagerConfig {
|
||||||
|
return ManagerConfig{
|
||||||
|
DefaultConnection: "",
|
||||||
|
Connections: make(map[string]ConnectionConfig),
|
||||||
|
MaxOpenConns: 25,
|
||||||
|
MaxIdleConns: 5,
|
||||||
|
ConnMaxLifetime: 30 * time.Minute,
|
||||||
|
ConnMaxIdleTime: 5 * time.Minute,
|
||||||
|
RetryAttempts: 3,
|
||||||
|
RetryDelay: 1 * time.Second,
|
||||||
|
RetryMaxDelay: 10 * time.Second,
|
||||||
|
HealthCheckInterval: 30 * time.Second,
|
||||||
|
EnableAutoReconnect: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyDefaults applies default values to the manager configuration
|
||||||
|
func (c *ManagerConfig) ApplyDefaults() {
|
||||||
|
defaults := DefaultManagerConfig()
|
||||||
|
|
||||||
|
if c.MaxOpenConns == 0 {
|
||||||
|
c.MaxOpenConns = defaults.MaxOpenConns
|
||||||
|
}
|
||||||
|
if c.MaxIdleConns == 0 {
|
||||||
|
c.MaxIdleConns = defaults.MaxIdleConns
|
||||||
|
}
|
||||||
|
if c.ConnMaxLifetime == 0 {
|
||||||
|
c.ConnMaxLifetime = defaults.ConnMaxLifetime
|
||||||
|
}
|
||||||
|
if c.ConnMaxIdleTime == 0 {
|
||||||
|
c.ConnMaxIdleTime = defaults.ConnMaxIdleTime
|
||||||
|
}
|
||||||
|
if c.RetryAttempts == 0 {
|
||||||
|
c.RetryAttempts = defaults.RetryAttempts
|
||||||
|
}
|
||||||
|
if c.RetryDelay == 0 {
|
||||||
|
c.RetryDelay = defaults.RetryDelay
|
||||||
|
}
|
||||||
|
if c.RetryMaxDelay == 0 {
|
||||||
|
c.RetryMaxDelay = defaults.RetryMaxDelay
|
||||||
|
}
|
||||||
|
if c.HealthCheckInterval == 0 {
|
||||||
|
c.HealthCheckInterval = defaults.HealthCheckInterval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the manager configuration
|
||||||
|
func (c *ManagerConfig) Validate() error {
|
||||||
|
if len(c.Connections) == 0 {
|
||||||
|
return NewConfigurationError("connections", fmt.Errorf("at least one connection must be configured"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DefaultConnection != "" {
|
||||||
|
if _, ok := c.Connections[c.DefaultConnection]; !ok {
|
||||||
|
return NewConfigurationError("default_connection", fmt.Errorf("default connection '%s' not found in connections", c.DefaultConnection))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each connection
|
||||||
|
for name := range c.Connections {
|
||||||
|
conn := c.Connections[name]
|
||||||
|
if err := conn.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("connection '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyDefaults applies default values and global settings to the connection configuration
|
||||||
|
func (cc *ConnectionConfig) ApplyDefaults(global *ManagerConfig) {
|
||||||
|
// Set name if not already set
|
||||||
|
if cc.Name == "" {
|
||||||
|
cc.Name = "unnamed"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply global pool settings if not overridden
|
||||||
|
if cc.MaxOpenConns == nil && global != nil {
|
||||||
|
maxOpen := global.MaxOpenConns
|
||||||
|
cc.MaxOpenConns = &maxOpen
|
||||||
|
}
|
||||||
|
if cc.MaxIdleConns == nil && global != nil {
|
||||||
|
maxIdle := global.MaxIdleConns
|
||||||
|
cc.MaxIdleConns = &maxIdle
|
||||||
|
}
|
||||||
|
if cc.ConnMaxLifetime == nil && global != nil {
|
||||||
|
lifetime := global.ConnMaxLifetime
|
||||||
|
cc.ConnMaxLifetime = &lifetime
|
||||||
|
}
|
||||||
|
if cc.ConnMaxIdleTime == nil && global != nil {
|
||||||
|
idleTime := global.ConnMaxIdleTime
|
||||||
|
cc.ConnMaxIdleTime = &idleTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default timeouts
|
||||||
|
if cc.ConnectTimeout == 0 {
|
||||||
|
cc.ConnectTimeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
if cc.QueryTimeout == 0 {
|
||||||
|
cc.QueryTimeout = 30 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default ORM
|
||||||
|
if cc.DefaultORM == "" {
|
||||||
|
cc.DefaultORM = string(ORMTypeBun)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default PostgreSQL port
|
||||||
|
if cc.Type == DatabaseTypePostgreSQL && cc.Port == 0 && cc.DSN == "" {
|
||||||
|
cc.Port = 5432
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default MSSQL port
|
||||||
|
if cc.Type == DatabaseTypeMSSQL && cc.Port == 0 && cc.DSN == "" {
|
||||||
|
cc.Port = 1433
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default MongoDB port
|
||||||
|
if cc.Type == DatabaseTypeMongoDB && cc.Port == 0 && cc.DSN == "" {
|
||||||
|
cc.Port = 27017
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default MongoDB auth source
|
||||||
|
if cc.Type == DatabaseTypeMongoDB && cc.AuthSource == "" {
|
||||||
|
cc.AuthSource = "admin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the connection configuration
|
||||||
|
func (cc *ConnectionConfig) Validate() error {
|
||||||
|
// Validate database type
|
||||||
|
switch cc.Type {
|
||||||
|
case DatabaseTypePostgreSQL, DatabaseTypeSQLite, DatabaseTypeMSSQL, DatabaseTypeMongoDB:
|
||||||
|
// Valid types
|
||||||
|
default:
|
||||||
|
return NewConfigurationError("type", fmt.Errorf("unsupported database type: %s", cc.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that either DSN or connection parameters are provided
|
||||||
|
if cc.DSN == "" {
|
||||||
|
switch cc.Type {
|
||||||
|
case DatabaseTypePostgreSQL, DatabaseTypeMSSQL, DatabaseTypeMongoDB:
|
||||||
|
if cc.Host == "" {
|
||||||
|
return NewConfigurationError("host", fmt.Errorf("host is required when DSN is not provided"))
|
||||||
|
}
|
||||||
|
if cc.Database == "" {
|
||||||
|
return NewConfigurationError("database", fmt.Errorf("database is required when DSN is not provided"))
|
||||||
|
}
|
||||||
|
case DatabaseTypeSQLite:
|
||||||
|
if cc.FilePath == "" {
|
||||||
|
return NewConfigurationError("filepath", fmt.Errorf("filepath is required for SQLite when DSN is not provided"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ORM type
|
||||||
|
if cc.DefaultORM != "" {
|
||||||
|
switch ORMType(cc.DefaultORM) {
|
||||||
|
case ORMTypeBun, ORMTypeGORM, ORMTypeNative:
|
||||||
|
// Valid ORM types
|
||||||
|
default:
|
||||||
|
return NewConfigurationError("default_orm", fmt.Errorf("unsupported ORM type: %s", cc.DefaultORM))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildDSN builds a connection string from individual parameters
|
||||||
|
func (cc *ConnectionConfig) BuildDSN() (string, error) {
|
||||||
|
// If DSN is already provided, use it
|
||||||
|
if cc.DSN != "" {
|
||||||
|
return cc.DSN, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cc.Type {
|
||||||
|
case DatabaseTypePostgreSQL:
|
||||||
|
return cc.buildPostgresDSN(), nil
|
||||||
|
case DatabaseTypeSQLite:
|
||||||
|
return cc.buildSQLiteDSN(), nil
|
||||||
|
case DatabaseTypeMSSQL:
|
||||||
|
return cc.buildMSSQLDSN(), nil
|
||||||
|
case DatabaseTypeMongoDB:
|
||||||
|
return cc.buildMongoDSN(), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("cannot build DSN for database type: %s", cc.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *ConnectionConfig) buildPostgresDSN() string {
|
||||||
|
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s",
|
||||||
|
cc.Host, cc.Port, cc.User, cc.Password, cc.Database)
|
||||||
|
|
||||||
|
if cc.SSLMode != "" {
|
||||||
|
dsn += fmt.Sprintf(" sslmode=%s", cc.SSLMode)
|
||||||
|
} else {
|
||||||
|
dsn += " sslmode=disable"
|
||||||
|
}
|
||||||
|
|
||||||
|
if cc.Schema != "" {
|
||||||
|
dsn += fmt.Sprintf(" search_path=%s", cc.Schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dsn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *ConnectionConfig) buildSQLiteDSN() string {
|
||||||
|
if cc.FilePath != "" {
|
||||||
|
return cc.FilePath
|
||||||
|
}
|
||||||
|
return ":memory:"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *ConnectionConfig) buildMSSQLDSN() string {
|
||||||
|
// Format: sqlserver://username:password@host:port?database=dbname
|
||||||
|
dsn := fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s",
|
||||||
|
cc.User, cc.Password, cc.Host, cc.Port, cc.Database)
|
||||||
|
|
||||||
|
if cc.Schema != "" {
|
||||||
|
dsn += fmt.Sprintf("&schema=%s", cc.Schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dsn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *ConnectionConfig) buildMongoDSN() string {
|
||||||
|
// Format: mongodb://username:password@host:port/database?authSource=admin
|
||||||
|
var dsn string
|
||||||
|
|
||||||
|
if cc.User != "" && cc.Password != "" {
|
||||||
|
dsn = fmt.Sprintf("mongodb://%s:%s@%s:%d/%s",
|
||||||
|
cc.User, cc.Password, cc.Host, cc.Port, cc.Database)
|
||||||
|
} else {
|
||||||
|
dsn = fmt.Sprintf("mongodb://%s:%d/%s", cc.Host, cc.Port, cc.Database)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := ""
|
||||||
|
if cc.AuthSource != "" {
|
||||||
|
params += fmt.Sprintf("authSource=%s", cc.AuthSource)
|
||||||
|
}
|
||||||
|
if cc.ReplicaSet != "" {
|
||||||
|
if params != "" {
|
||||||
|
params += "&"
|
||||||
|
}
|
||||||
|
params += fmt.Sprintf("replicaSet=%s", cc.ReplicaSet)
|
||||||
|
}
|
||||||
|
if cc.ReadPreference != "" {
|
||||||
|
if params != "" {
|
||||||
|
params += "&"
|
||||||
|
}
|
||||||
|
params += fmt.Sprintf("readPreference=%s", cc.ReadPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params != "" {
|
||||||
|
dsn += "?" + params
|
||||||
|
}
|
||||||
|
|
||||||
|
return dsn
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromConfig converts config.DBManagerConfig to internal ManagerConfig
|
||||||
|
func FromConfig(cfg config.DBManagerConfig) ManagerConfig {
|
||||||
|
mgr := ManagerConfig{
|
||||||
|
DefaultConnection: cfg.DefaultConnection,
|
||||||
|
Connections: make(map[string]ConnectionConfig),
|
||||||
|
MaxOpenConns: cfg.MaxOpenConns,
|
||||||
|
MaxIdleConns: cfg.MaxIdleConns,
|
||||||
|
ConnMaxLifetime: cfg.ConnMaxLifetime,
|
||||||
|
ConnMaxIdleTime: cfg.ConnMaxIdleTime,
|
||||||
|
RetryAttempts: cfg.RetryAttempts,
|
||||||
|
RetryDelay: cfg.RetryDelay,
|
||||||
|
RetryMaxDelay: cfg.RetryMaxDelay,
|
||||||
|
HealthCheckInterval: cfg.HealthCheckInterval,
|
||||||
|
EnableAutoReconnect: cfg.EnableAutoReconnect,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert connections
|
||||||
|
for name := range cfg.Connections {
|
||||||
|
connCfg := cfg.Connections[name]
|
||||||
|
mgr.Connections[name] = ConnectionConfig{
|
||||||
|
Name: connCfg.Name,
|
||||||
|
Type: DatabaseType(connCfg.Type),
|
||||||
|
DSN: connCfg.DSN,
|
||||||
|
Host: connCfg.Host,
|
||||||
|
Port: connCfg.Port,
|
||||||
|
User: connCfg.User,
|
||||||
|
Password: connCfg.Password,
|
||||||
|
Database: connCfg.Database,
|
||||||
|
SSLMode: connCfg.SSLMode,
|
||||||
|
Schema: connCfg.Schema,
|
||||||
|
FilePath: connCfg.FilePath,
|
||||||
|
AuthSource: connCfg.AuthSource,
|
||||||
|
ReplicaSet: connCfg.ReplicaSet,
|
||||||
|
ReadPreference: connCfg.ReadPreference,
|
||||||
|
MaxOpenConns: connCfg.MaxOpenConns,
|
||||||
|
MaxIdleConns: connCfg.MaxIdleConns,
|
||||||
|
ConnMaxLifetime: connCfg.ConnMaxLifetime,
|
||||||
|
ConnMaxIdleTime: connCfg.ConnMaxIdleTime,
|
||||||
|
ConnectTimeout: connCfg.ConnectTimeout,
|
||||||
|
QueryTimeout: connCfg.QueryTimeout,
|
||||||
|
EnableTracing: connCfg.EnableTracing,
|
||||||
|
EnableMetrics: connCfg.EnableMetrics,
|
||||||
|
EnableLogging: connCfg.EnableLogging,
|
||||||
|
DefaultORM: connCfg.DefaultORM,
|
||||||
|
Tags: connCfg.Tags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mgr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter methods to implement providers.ConnectionConfig interface
|
||||||
|
func (cc *ConnectionConfig) GetName() string { return cc.Name }
|
||||||
|
func (cc *ConnectionConfig) GetType() string { return string(cc.Type) }
|
||||||
|
func (cc *ConnectionConfig) GetHost() string { return cc.Host }
|
||||||
|
func (cc *ConnectionConfig) GetPort() int { return cc.Port }
|
||||||
|
func (cc *ConnectionConfig) GetUser() string { return cc.User }
|
||||||
|
func (cc *ConnectionConfig) GetPassword() string { return cc.Password }
|
||||||
|
func (cc *ConnectionConfig) GetDatabase() string { return cc.Database }
|
||||||
|
func (cc *ConnectionConfig) GetFilePath() string { return cc.FilePath }
|
||||||
|
func (cc *ConnectionConfig) GetConnectTimeout() time.Duration { return cc.ConnectTimeout }
|
||||||
|
func (cc *ConnectionConfig) GetEnableLogging() bool { return cc.EnableLogging }
|
||||||
|
func (cc *ConnectionConfig) GetMaxOpenConns() *int { return cc.MaxOpenConns }
|
||||||
|
func (cc *ConnectionConfig) GetMaxIdleConns() *int { return cc.MaxIdleConns }
|
||||||
|
func (cc *ConnectionConfig) GetConnMaxLifetime() *time.Duration { return cc.ConnMaxLifetime }
|
||||||
|
func (cc *ConnectionConfig) GetConnMaxIdleTime() *time.Duration { return cc.ConnMaxIdleTime }
|
||||||
|
func (cc *ConnectionConfig) GetQueryTimeout() time.Duration { return cc.QueryTimeout }
|
||||||
|
func (cc *ConnectionConfig) GetEnableMetrics() bool { return cc.EnableMetrics }
|
||||||
|
func (cc *ConnectionConfig) GetReadPreference() string { return cc.ReadPreference }
|
||||||
607
pkg/dbmanager/connection.go
Normal file
607
pkg/dbmanager/connection.go
Normal file
@@ -0,0 +1,607 @@
|
|||||||
|
package dbmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/schema"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Connection represents a single named database connection
|
||||||
|
type Connection interface {
|
||||||
|
// Metadata
|
||||||
|
Name() string
|
||||||
|
Type() DatabaseType
|
||||||
|
|
||||||
|
// ORM Access (SQL databases only)
|
||||||
|
Bun() (*bun.DB, error)
|
||||||
|
GORM() (*gorm.DB, error)
|
||||||
|
Native() (*sql.DB, error)
|
||||||
|
|
||||||
|
// Common Database interface (for SQL databases)
|
||||||
|
Database() (common.Database, error)
|
||||||
|
|
||||||
|
// MongoDB Access (MongoDB only)
|
||||||
|
MongoDB() (*mongo.Client, error)
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
Connect(ctx context.Context) error
|
||||||
|
Close() error
|
||||||
|
HealthCheck(ctx context.Context) error
|
||||||
|
Reconnect(ctx context.Context) error
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
Stats() *ConnectionStats
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionStats contains statistics about a database connection
|
||||||
|
type ConnectionStats struct {
|
||||||
|
Name string
|
||||||
|
Type DatabaseType
|
||||||
|
Connected bool
|
||||||
|
LastHealthCheck time.Time
|
||||||
|
HealthCheckStatus string
|
||||||
|
|
||||||
|
// SQL connection pool stats
|
||||||
|
OpenConnections int
|
||||||
|
InUse int
|
||||||
|
Idle int
|
||||||
|
WaitCount int64
|
||||||
|
WaitDuration time.Duration
|
||||||
|
MaxIdleClosed int64
|
||||||
|
MaxLifetimeClosed int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqlConnection implements Connection for SQL databases (PostgreSQL, SQLite, MSSQL)
|
||||||
|
type sqlConnection struct {
|
||||||
|
name string
|
||||||
|
dbType DatabaseType
|
||||||
|
config ConnectionConfig
|
||||||
|
provider Provider
|
||||||
|
|
||||||
|
// Lazy-initialized ORM instances (all wrap the same sql.DB)
|
||||||
|
nativeDB *sql.DB
|
||||||
|
bunDB *bun.DB
|
||||||
|
gormDB *gorm.DB
|
||||||
|
|
||||||
|
// Adapters for common.Database interface
|
||||||
|
bunAdapter *database.BunAdapter
|
||||||
|
gormAdapter *database.GormAdapter
|
||||||
|
nativeAdapter common.Database
|
||||||
|
|
||||||
|
// State
|
||||||
|
connected bool
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
lastHealthCheck time.Time
|
||||||
|
healthCheckStatus string
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSQLConnection creates a new SQL connection
|
||||||
|
func newSQLConnection(name string, dbType DatabaseType, config ConnectionConfig, provider Provider) *sqlConnection {
|
||||||
|
return &sqlConnection{
|
||||||
|
name: name,
|
||||||
|
dbType: dbType,
|
||||||
|
config: config,
|
||||||
|
provider: provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the connection name
|
||||||
|
func (c *sqlConnection) Name() string {
|
||||||
|
return c.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the database type
|
||||||
|
func (c *sqlConnection) Type() DatabaseType {
|
||||||
|
return c.dbType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes the database connection
|
||||||
|
func (c *sqlConnection) Connect(ctx context.Context) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.connected {
|
||||||
|
return ErrAlreadyConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.provider.Connect(ctx, &c.config); err != nil {
|
||||||
|
return NewConnectionError(c.name, "connect", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.connected = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the database connection and all ORM instances
|
||||||
|
func (c *sqlConnection) Close() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if !c.connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close Bun if initialized
|
||||||
|
if c.bunDB != nil {
|
||||||
|
if err := c.bunDB.Close(); err != nil {
|
||||||
|
return NewConnectionError(c.name, "close bun", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GORM doesn't have a separate close - it uses the underlying sql.DB
|
||||||
|
|
||||||
|
// Close the provider (which closes the underlying sql.DB)
|
||||||
|
if err := c.provider.Close(); err != nil {
|
||||||
|
return NewConnectionError(c.name, "close", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.connected = false
|
||||||
|
c.nativeDB = nil
|
||||||
|
c.bunDB = nil
|
||||||
|
c.gormDB = nil
|
||||||
|
c.bunAdapter = nil
|
||||||
|
c.gormAdapter = nil
|
||||||
|
c.nativeAdapter = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck verifies the connection is alive
|
||||||
|
func (c *sqlConnection) HealthCheck(ctx context.Context) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.lastHealthCheck = time.Now()
|
||||||
|
|
||||||
|
if !c.connected {
|
||||||
|
c.healthCheckStatus = "disconnected"
|
||||||
|
return ErrConnectionClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.provider.HealthCheck(ctx); err != nil {
|
||||||
|
c.healthCheckStatus = "unhealthy: " + err.Error()
|
||||||
|
return NewConnectionError(c.name, "health check", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.healthCheckStatus = "healthy"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconnect closes and re-establishes the connection
|
||||||
|
func (c *sqlConnection) Reconnect(ctx context.Context) error {
|
||||||
|
if err := c.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Connect(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native returns the native *sql.DB connection
|
||||||
|
func (c *sqlConnection) Native() (*sql.DB, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
if c.nativeDB != nil {
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.nativeDB, nil
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
// Double-check after acquiring write lock
|
||||||
|
if c.nativeDB != nil {
|
||||||
|
return c.nativeDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.connected {
|
||||||
|
return nil, ErrConnectionClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get native connection from provider
|
||||||
|
db, err := c.provider.GetNative()
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewConnectionError(c.name, "get native", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.nativeDB = db
|
||||||
|
return c.nativeDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bun returns a Bun ORM instance wrapping the native connection
|
||||||
|
func (c *sqlConnection) Bun() (*bun.DB, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
if c.bunDB != nil {
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.bunDB, nil
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
// Double-check after acquiring write lock
|
||||||
|
if c.bunDB != nil {
|
||||||
|
return c.bunDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get native connection first
|
||||||
|
native, err := c.provider.GetNative()
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewConnectionError(c.name, "get bun", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Bun DB wrapping the same sql.DB
|
||||||
|
dialect := c.getBunDialect()
|
||||||
|
c.bunDB = bun.NewDB(native, dialect)
|
||||||
|
|
||||||
|
return c.bunDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GORM returns a GORM instance wrapping the native connection
|
||||||
|
func (c *sqlConnection) GORM() (*gorm.DB, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
if c.gormDB != nil {
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.gormDB, nil
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
// Double-check after acquiring write lock
|
||||||
|
if c.gormDB != nil {
|
||||||
|
return c.gormDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get native connection first
|
||||||
|
native, err := c.provider.GetNative()
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewConnectionError(c.name, "get gorm", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create GORM DB wrapping the same sql.DB
|
||||||
|
dialector := c.getGORMDialector(native)
|
||||||
|
db, err := gorm.Open(dialector, &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewConnectionError(c.name, "initialize gorm", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.gormDB = db
|
||||||
|
return c.gormDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database returns the common.Database interface using the configured default ORM
|
||||||
|
func (c *sqlConnection) Database() (common.Database, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defaultORM := c.config.DefaultORM
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
switch ORMType(defaultORM) {
|
||||||
|
case ORMTypeBun:
|
||||||
|
return c.getBunAdapter()
|
||||||
|
case ORMTypeGORM:
|
||||||
|
return c.getGORMAdapter()
|
||||||
|
case ORMTypeNative:
|
||||||
|
return c.getNativeAdapter()
|
||||||
|
default:
|
||||||
|
// Default to Bun
|
||||||
|
return c.getBunAdapter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MongoDB returns an error for SQL connections
|
||||||
|
func (c *sqlConnection) MongoDB() (*mongo.Client, error) {
|
||||||
|
return nil, ErrNotMongoDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns connection statistics
|
||||||
|
func (c *sqlConnection) Stats() *ConnectionStats {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
stats := &ConnectionStats{
|
||||||
|
Name: c.name,
|
||||||
|
Type: c.dbType,
|
||||||
|
Connected: c.connected,
|
||||||
|
LastHealthCheck: c.lastHealthCheck,
|
||||||
|
HealthCheckStatus: c.healthCheckStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get SQL stats if connected
|
||||||
|
if c.connected && c.provider != nil {
|
||||||
|
if providerStats := c.provider.Stats(); providerStats != nil {
|
||||||
|
stats.OpenConnections = providerStats.OpenConnections
|
||||||
|
stats.InUse = providerStats.InUse
|
||||||
|
stats.Idle = providerStats.Idle
|
||||||
|
stats.WaitCount = providerStats.WaitCount
|
||||||
|
stats.WaitDuration = providerStats.WaitDuration
|
||||||
|
stats.MaxIdleClosed = providerStats.MaxIdleClosed
|
||||||
|
stats.MaxLifetimeClosed = providerStats.MaxLifetimeClosed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBunAdapter returns or creates the Bun adapter
|
||||||
|
func (c *sqlConnection) getBunAdapter() (common.Database, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
if c.bunAdapter != nil {
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.bunAdapter, nil
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.bunAdapter != nil {
|
||||||
|
return c.bunAdapter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bunDB, err := c.Bun()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.bunAdapter = database.NewBunAdapter(bunDB)
|
||||||
|
return c.bunAdapter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGORMAdapter returns or creates the GORM adapter
|
||||||
|
func (c *sqlConnection) getGORMAdapter() (common.Database, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
if c.gormAdapter != nil {
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.gormAdapter, nil
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.gormAdapter != nil {
|
||||||
|
return c.gormAdapter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gormDB, err := c.GORM()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.gormAdapter = database.NewGormAdapter(gormDB)
|
||||||
|
return c.gormAdapter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNativeAdapter returns or creates the native adapter
|
||||||
|
func (c *sqlConnection) getNativeAdapter() (common.Database, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
if c.nativeAdapter != nil {
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.nativeAdapter, nil
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.nativeAdapter != nil {
|
||||||
|
return c.nativeAdapter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
native, err := c.Native()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a native adapter based on database type
|
||||||
|
switch c.dbType {
|
||||||
|
case DatabaseTypePostgreSQL:
|
||||||
|
c.nativeAdapter = database.NewPgSQLAdapter(native)
|
||||||
|
case DatabaseTypeSQLite:
|
||||||
|
// For SQLite, we'll use the PgSQL adapter as it works with standard sql.DB
|
||||||
|
c.nativeAdapter = database.NewPgSQLAdapter(native)
|
||||||
|
case DatabaseTypeMSSQL:
|
||||||
|
// For MSSQL, we'll use the PgSQL adapter as it works with standard sql.DB
|
||||||
|
c.nativeAdapter = database.NewPgSQLAdapter(native)
|
||||||
|
default:
|
||||||
|
return nil, ErrUnsupportedDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.nativeAdapter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBunDialect returns the appropriate Bun dialect for the database type
|
||||||
|
func (c *sqlConnection) getBunDialect() schema.Dialect {
|
||||||
|
switch c.dbType {
|
||||||
|
case DatabaseTypePostgreSQL:
|
||||||
|
return database.GetPostgresDialect()
|
||||||
|
case DatabaseTypeSQLite:
|
||||||
|
return database.GetSQLiteDialect()
|
||||||
|
case DatabaseTypeMSSQL:
|
||||||
|
return database.GetMSSQLDialect()
|
||||||
|
default:
|
||||||
|
// Default to PostgreSQL
|
||||||
|
return database.GetPostgresDialect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGORMDialector returns the appropriate GORM dialector for the database type
|
||||||
|
func (c *sqlConnection) getGORMDialector(db *sql.DB) gorm.Dialector {
|
||||||
|
switch c.dbType {
|
||||||
|
case DatabaseTypePostgreSQL:
|
||||||
|
return database.GetPostgresDialector(db)
|
||||||
|
case DatabaseTypeSQLite:
|
||||||
|
return database.GetSQLiteDialector(db)
|
||||||
|
case DatabaseTypeMSSQL:
|
||||||
|
return database.GetMSSQLDialector(db)
|
||||||
|
default:
|
||||||
|
// Default to PostgreSQL
|
||||||
|
return database.GetPostgresDialector(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mongoConnection implements Connection for MongoDB
|
||||||
|
type mongoConnection struct {
|
||||||
|
name string
|
||||||
|
config ConnectionConfig
|
||||||
|
provider Provider
|
||||||
|
|
||||||
|
// MongoDB client
|
||||||
|
client *mongo.Client
|
||||||
|
|
||||||
|
// State
|
||||||
|
connected bool
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
lastHealthCheck time.Time
|
||||||
|
healthCheckStatus string
|
||||||
|
}
|
||||||
|
|
||||||
|
// newMongoConnection creates a new MongoDB connection
|
||||||
|
func newMongoConnection(name string, config ConnectionConfig, provider Provider) *mongoConnection {
|
||||||
|
return &mongoConnection{
|
||||||
|
name: name,
|
||||||
|
config: config,
|
||||||
|
provider: provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the connection name
|
||||||
|
func (c *mongoConnection) Name() string {
|
||||||
|
return c.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the database type (MongoDB)
|
||||||
|
func (c *mongoConnection) Type() DatabaseType {
|
||||||
|
return DatabaseTypeMongoDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes the MongoDB connection
|
||||||
|
func (c *mongoConnection) Connect(ctx context.Context) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.connected {
|
||||||
|
return ErrAlreadyConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.provider.Connect(ctx, &c.config); err != nil {
|
||||||
|
return NewConnectionError(c.name, "connect", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the mongo client
|
||||||
|
client, err := c.provider.GetMongo()
|
||||||
|
if err != nil {
|
||||||
|
return NewConnectionError(c.name, "get mongo client", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.client = client
|
||||||
|
c.connected = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the MongoDB connection
|
||||||
|
func (c *mongoConnection) Close() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if !c.connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.provider.Close(); err != nil {
|
||||||
|
return NewConnectionError(c.name, "close", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.connected = false
|
||||||
|
c.client = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck verifies the MongoDB connection is alive
|
||||||
|
func (c *mongoConnection) HealthCheck(ctx context.Context) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.lastHealthCheck = time.Now()
|
||||||
|
|
||||||
|
if !c.connected {
|
||||||
|
c.healthCheckStatus = "disconnected"
|
||||||
|
return ErrConnectionClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.provider.HealthCheck(ctx); err != nil {
|
||||||
|
c.healthCheckStatus = "unhealthy: " + err.Error()
|
||||||
|
return NewConnectionError(c.name, "health check", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.healthCheckStatus = "healthy"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconnect closes and re-establishes the MongoDB connection
|
||||||
|
func (c *mongoConnection) Reconnect(ctx context.Context) error {
|
||||||
|
if err := c.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Connect(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MongoDB returns the MongoDB client
|
||||||
|
func (c *mongoConnection) MongoDB() (*mongo.Client, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
if !c.connected || c.client == nil {
|
||||||
|
return nil, ErrConnectionClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bun returns an error for MongoDB connections
|
||||||
|
func (c *mongoConnection) Bun() (*bun.DB, error) {
|
||||||
|
return nil, ErrNotSQLDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
// GORM returns an error for MongoDB connections
|
||||||
|
func (c *mongoConnection) GORM() (*gorm.DB, error) {
|
||||||
|
return nil, ErrNotSQLDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native returns an error for MongoDB connections
|
||||||
|
func (c *mongoConnection) Native() (*sql.DB, error) {
|
||||||
|
return nil, ErrNotSQLDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database returns an error for MongoDB connections
|
||||||
|
func (c *mongoConnection) Database() (common.Database, error) {
|
||||||
|
return nil, ErrNotSQLDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns connection statistics for MongoDB
|
||||||
|
func (c *mongoConnection) Stats() *ConnectionStats {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
return &ConnectionStats{
|
||||||
|
Name: c.name,
|
||||||
|
Type: DatabaseTypeMongoDB,
|
||||||
|
Connected: c.connected,
|
||||||
|
LastHealthCheck: c.lastHealthCheck,
|
||||||
|
HealthCheckStatus: c.healthCheckStatus,
|
||||||
|
}
|
||||||
|
}
|
||||||
82
pkg/dbmanager/errors.go
Normal file
82
pkg/dbmanager/errors.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package dbmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common errors
|
||||||
|
var (
|
||||||
|
// ErrConnectionNotFound is returned when a connection with the given name doesn't exist
|
||||||
|
ErrConnectionNotFound = errors.New("connection not found")
|
||||||
|
|
||||||
|
// ErrInvalidConfiguration is returned when the configuration is invalid
|
||||||
|
ErrInvalidConfiguration = errors.New("invalid configuration")
|
||||||
|
|
||||||
|
// ErrConnectionClosed is returned when attempting to use a closed connection
|
||||||
|
ErrConnectionClosed = errors.New("connection is closed")
|
||||||
|
|
||||||
|
// ErrNotSQLDatabase is returned when attempting SQL operations on a non-SQL database
|
||||||
|
ErrNotSQLDatabase = errors.New("not a SQL database")
|
||||||
|
|
||||||
|
// ErrNotMongoDB is returned when attempting MongoDB operations on a non-MongoDB connection
|
||||||
|
ErrNotMongoDB = errors.New("not a MongoDB connection")
|
||||||
|
|
||||||
|
// ErrUnsupportedDatabase is returned when the database type is not supported
|
||||||
|
ErrUnsupportedDatabase = errors.New("unsupported database type")
|
||||||
|
|
||||||
|
// ErrNoDefaultConnection is returned when no default connection is configured
|
||||||
|
ErrNoDefaultConnection = errors.New("no default connection configured")
|
||||||
|
|
||||||
|
// ErrAlreadyConnected is returned when attempting to connect an already connected connection
|
||||||
|
ErrAlreadyConnected = errors.New("already connected")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnectionError wraps errors that occur during connection operations
|
||||||
|
type ConnectionError struct {
|
||||||
|
Name string
|
||||||
|
Operation string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnectionError) Error() string {
|
||||||
|
return fmt.Sprintf("connection '%s' %s: %v", e.Name, e.Operation, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnectionError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnectionError creates a new ConnectionError
|
||||||
|
func NewConnectionError(name, operation string, err error) *ConnectionError {
|
||||||
|
return &ConnectionError{
|
||||||
|
Name: name,
|
||||||
|
Operation: operation,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigurationError wraps configuration-related errors
|
||||||
|
type ConfigurationError struct {
|
||||||
|
Field string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConfigurationError) Error() string {
|
||||||
|
if e.Field != "" {
|
||||||
|
return fmt.Sprintf("configuration error in field '%s': %v", e.Field, e.Err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("configuration error: %v", e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConfigurationError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigurationError creates a new ConfigurationError
|
||||||
|
func NewConfigurationError(field string, err error) *ConfigurationError {
|
||||||
|
return &ConfigurationError{
|
||||||
|
Field: field,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
51
pkg/dbmanager/factory.go
Normal file
51
pkg/dbmanager/factory.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package dbmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/dbmanager/providers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createConnection creates a database connection based on the configuration
|
||||||
|
func createConnection(cfg ConnectionConfig) (Connection, error) {
|
||||||
|
// Validate configuration
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid connection configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create provider based on database type
|
||||||
|
provider, err := createProvider(cfg.Type)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create connection wrapper based on database type
|
||||||
|
switch cfg.Type {
|
||||||
|
case DatabaseTypePostgreSQL, DatabaseTypeSQLite, DatabaseTypeMSSQL:
|
||||||
|
return newSQLConnection(cfg.Name, cfg.Type, cfg, provider), nil
|
||||||
|
case DatabaseTypeMongoDB:
|
||||||
|
return newMongoConnection(cfg.Name, cfg, provider), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrUnsupportedDatabase, cfg.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createProvider creates a database provider based on the database type
|
||||||
|
func createProvider(dbType DatabaseType) (Provider, error) {
|
||||||
|
switch dbType {
|
||||||
|
case DatabaseTypePostgreSQL:
|
||||||
|
return providers.NewPostgresProvider(), nil
|
||||||
|
case DatabaseTypeSQLite:
|
||||||
|
return providers.NewSQLiteProvider(), nil
|
||||||
|
case DatabaseTypeMSSQL:
|
||||||
|
return providers.NewMSSQLProvider(), nil
|
||||||
|
case DatabaseTypeMongoDB:
|
||||||
|
return providers.NewMongoProvider(), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrUnsupportedDatabase, dbType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is an alias to the providers.Provider interface
|
||||||
|
// This allows dbmanager package consumers to use Provider without importing providers
|
||||||
|
type Provider = providers.Provider
|
||||||
326
pkg/dbmanager/manager.go
Normal file
326
pkg/dbmanager/manager.go
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
package dbmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager manages multiple named database connections
|
||||||
|
type Manager interface {
|
||||||
|
// Connection retrieval
|
||||||
|
Get(name string) (Connection, error)
|
||||||
|
GetDefault() (Connection, error)
|
||||||
|
GetAll() map[string]Connection
|
||||||
|
|
||||||
|
// Default database management
|
||||||
|
GetDefaultDatabase() (common.Database, error)
|
||||||
|
SetDefaultDatabase(name string) error
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
Connect(ctx context.Context) error
|
||||||
|
Close() error
|
||||||
|
HealthCheck(ctx context.Context) error
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
Stats() *ManagerStats
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagerStats contains statistics about the connection manager
|
||||||
|
type ManagerStats struct {
|
||||||
|
TotalConnections int
|
||||||
|
HealthyCount int
|
||||||
|
UnhealthyCount int
|
||||||
|
ConnectionStats map[string]*ConnectionStats
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectionManager implements Manager
|
||||||
|
type connectionManager struct {
|
||||||
|
connections map[string]Connection
|
||||||
|
config ManagerConfig
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// Background health check
|
||||||
|
healthTicker *time.Ticker
|
||||||
|
stopChan chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new database connection manager
|
||||||
|
func NewManager(cfg ManagerConfig) (Manager, error) {
|
||||||
|
// Apply defaults and validate configuration
|
||||||
|
cfg.ApplyDefaults()
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := &connectionManager{
|
||||||
|
connections: make(map[string]Connection),
|
||||||
|
config: cfg,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return mgr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a named connection
|
||||||
|
func (m *connectionManager) Get(name string) (Connection, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
conn, ok := m.connections[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrConnectionNotFound, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefault retrieves the default connection
|
||||||
|
func (m *connectionManager) GetDefault() (Connection, error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
defaultName := m.config.DefaultConnection
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if defaultName == "" {
|
||||||
|
return nil, ErrNoDefaultConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Get(defaultName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll returns all connections
|
||||||
|
func (m *connectionManager) GetAll() map[string]Connection {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
// Create a copy to avoid concurrent access issues
|
||||||
|
result := make(map[string]Connection, len(m.connections))
|
||||||
|
for name, conn := range m.connections {
|
||||||
|
result[name] = conn
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultDatabase returns the common.Database interface from the default connection
|
||||||
|
func (m *connectionManager) GetDefaultDatabase() (common.Database, error) {
|
||||||
|
conn, err := m.GetDefault()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := conn.Database()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get database from default connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultDatabase sets the default database connection by name
|
||||||
|
func (m *connectionManager) SetDefaultDatabase(name string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
// Verify the connection exists
|
||||||
|
if _, ok := m.connections[name]; !ok {
|
||||||
|
return fmt.Errorf("%w: %s", ErrConnectionNotFound, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.config.DefaultConnection = name
|
||||||
|
logger.Info("Default database connection changed: name=%s", name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes all configured database connections
|
||||||
|
func (m *connectionManager) Connect(ctx context.Context) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
// Create connections from configuration
|
||||||
|
for name := range m.config.Connections {
|
||||||
|
// Get a copy of the connection config
|
||||||
|
connCfg := m.config.Connections[name]
|
||||||
|
// Apply global defaults to connection config
|
||||||
|
connCfg.ApplyDefaults(&m.config)
|
||||||
|
connCfg.Name = name
|
||||||
|
|
||||||
|
// Create connection using factory
|
||||||
|
conn, err := createConnection(connCfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create connection '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
if err := conn.Connect(ctx); err != nil {
|
||||||
|
return fmt.Errorf("failed to connect '%s': %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.connections[name] = conn
|
||||||
|
logger.Info("Database connection established: name=%s, type=%s", name, connCfg.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start background health checks if enabled
|
||||||
|
if m.config.EnableAutoReconnect && m.config.HealthCheckInterval > 0 {
|
||||||
|
m.startHealthChecker()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Database manager initialized: connections=%d", len(m.connections))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes all database connections
|
||||||
|
func (m *connectionManager) Close() error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
// Stop health checker
|
||||||
|
m.stopHealthChecker()
|
||||||
|
|
||||||
|
// Close all connections
|
||||||
|
var errors []error
|
||||||
|
for name, conn := range m.connections {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
errors = append(errors, fmt.Errorf("failed to close connection '%s': %w", name, err))
|
||||||
|
logger.Error("Failed to close connection", "name", name, "error", err)
|
||||||
|
} else {
|
||||||
|
logger.Info("Connection closed: name=%s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.connections = make(map[string]Connection)
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return fmt.Errorf("errors closing connections: %v", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Database manager closed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck performs health checks on all connections
|
||||||
|
func (m *connectionManager) HealthCheck(ctx context.Context) error {
|
||||||
|
m.mu.RLock()
|
||||||
|
connections := make(map[string]Connection, len(m.connections))
|
||||||
|
for name, conn := range m.connections {
|
||||||
|
connections[name] = conn
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
var errors []error
|
||||||
|
for name, conn := range connections {
|
||||||
|
if err := conn.HealthCheck(ctx); err != nil {
|
||||||
|
errors = append(errors, fmt.Errorf("connection '%s': %w", name, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
return fmt.Errorf("health check failed for %d connections: %v", len(errors), errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns statistics for all connections
|
||||||
|
func (m *connectionManager) Stats() *ManagerStats {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
stats := &ManagerStats{
|
||||||
|
TotalConnections: len(m.connections),
|
||||||
|
ConnectionStats: make(map[string]*ConnectionStats),
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, conn := range m.connections {
|
||||||
|
connStats := conn.Stats()
|
||||||
|
stats.ConnectionStats[name] = connStats
|
||||||
|
|
||||||
|
if connStats.Connected && connStats.HealthCheckStatus == "healthy" {
|
||||||
|
stats.HealthyCount++
|
||||||
|
} else {
|
||||||
|
stats.UnhealthyCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// startHealthChecker starts background health checking
|
||||||
|
func (m *connectionManager) startHealthChecker() {
|
||||||
|
if m.healthTicker != nil {
|
||||||
|
return // Already running
|
||||||
|
}
|
||||||
|
|
||||||
|
m.healthTicker = time.NewTicker(m.config.HealthCheckInterval)
|
||||||
|
|
||||||
|
m.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer m.wg.Done()
|
||||||
|
logger.Info("Health checker started: interval=%v", m.config.HealthCheckInterval)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.healthTicker.C:
|
||||||
|
m.performHealthCheck()
|
||||||
|
case <-m.stopChan:
|
||||||
|
logger.Info("Health checker stopped")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopHealthChecker stops background health checking
|
||||||
|
func (m *connectionManager) stopHealthChecker() {
|
||||||
|
if m.healthTicker != nil {
|
||||||
|
m.healthTicker.Stop()
|
||||||
|
close(m.stopChan)
|
||||||
|
m.wg.Wait()
|
||||||
|
m.healthTicker = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// performHealthCheck performs a health check on all connections
|
||||||
|
func (m *connectionManager) performHealthCheck() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
connections := make([]struct {
|
||||||
|
name string
|
||||||
|
conn Connection
|
||||||
|
}, 0, len(m.connections))
|
||||||
|
for name, conn := range m.connections {
|
||||||
|
connections = append(connections, struct {
|
||||||
|
name string
|
||||||
|
conn Connection
|
||||||
|
}{name, conn})
|
||||||
|
}
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, item := range connections {
|
||||||
|
if err := item.conn.HealthCheck(ctx); err != nil {
|
||||||
|
logger.Warn("Health check failed",
|
||||||
|
"connection", item.name,
|
||||||
|
"error", err)
|
||||||
|
|
||||||
|
// Attempt reconnection if enabled
|
||||||
|
if m.config.EnableAutoReconnect {
|
||||||
|
logger.Info("Attempting reconnection: connection=%s", item.name)
|
||||||
|
if err := item.conn.Reconnect(ctx); err != nil {
|
||||||
|
logger.Error("Reconnection failed",
|
||||||
|
"connection", item.name,
|
||||||
|
"error", err)
|
||||||
|
} else {
|
||||||
|
logger.Info("Reconnection successful: connection=%s", item.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
pkg/dbmanager/metrics.go
Normal file
136
pkg/dbmanager/metrics.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package dbmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// connectionsTotal tracks the total number of configured database connections
|
||||||
|
connectionsTotal = promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "dbmanager_connections_total",
|
||||||
|
Help: "Total number of configured database connections",
|
||||||
|
},
|
||||||
|
[]string{"type"},
|
||||||
|
)
|
||||||
|
|
||||||
|
// connectionStatus tracks connection health status (1=healthy, 0=unhealthy)
|
||||||
|
connectionStatus = promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "dbmanager_connection_status",
|
||||||
|
Help: "Connection status (1=healthy, 0=unhealthy)",
|
||||||
|
},
|
||||||
|
[]string{"name", "type"},
|
||||||
|
)
|
||||||
|
|
||||||
|
// connectionPoolSize tracks connection pool sizes
|
||||||
|
connectionPoolSize = promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "dbmanager_connection_pool_size",
|
||||||
|
Help: "Current connection pool size",
|
||||||
|
},
|
||||||
|
[]string{"name", "type", "state"}, // state: open, idle, in_use
|
||||||
|
)
|
||||||
|
|
||||||
|
// connectionWaitCount tracks how many times connections had to wait for availability
|
||||||
|
connectionWaitCount = promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "dbmanager_connection_wait_count",
|
||||||
|
Help: "Number of times connections had to wait for availability",
|
||||||
|
},
|
||||||
|
[]string{"name", "type"},
|
||||||
|
)
|
||||||
|
|
||||||
|
// connectionWaitDuration tracks total time connections spent waiting
|
||||||
|
connectionWaitDuration = promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "dbmanager_connection_wait_duration_seconds",
|
||||||
|
Help: "Total time connections spent waiting for availability",
|
||||||
|
},
|
||||||
|
[]string{"name", "type"},
|
||||||
|
)
|
||||||
|
|
||||||
|
// reconnectAttempts tracks reconnection attempts and their outcomes
|
||||||
|
reconnectAttempts = promauto.NewCounterVec(
|
||||||
|
prometheus.CounterOpts{
|
||||||
|
Name: "dbmanager_reconnect_attempts_total",
|
||||||
|
Help: "Total number of reconnection attempts",
|
||||||
|
},
|
||||||
|
[]string{"name", "type", "result"}, // result: success, failure
|
||||||
|
)
|
||||||
|
|
||||||
|
// connectionLifetimeClosed tracks connections closed due to max lifetime
|
||||||
|
connectionLifetimeClosed = promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "dbmanager_connection_lifetime_closed_total",
|
||||||
|
Help: "Total connections closed due to exceeding max lifetime",
|
||||||
|
},
|
||||||
|
[]string{"name", "type"},
|
||||||
|
)
|
||||||
|
|
||||||
|
// connectionIdleClosed tracks connections closed due to max idle time
|
||||||
|
connectionIdleClosed = promauto.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "dbmanager_connection_idle_closed_total",
|
||||||
|
Help: "Total connections closed due to exceeding max idle time",
|
||||||
|
},
|
||||||
|
[]string{"name", "type"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// PublishMetrics publishes current metrics for all connections
|
||||||
|
func (m *connectionManager) PublishMetrics() {
|
||||||
|
stats := m.Stats()
|
||||||
|
|
||||||
|
// Count connections by type
|
||||||
|
typeCount := make(map[DatabaseType]int)
|
||||||
|
for _, connStats := range stats.ConnectionStats {
|
||||||
|
typeCount[connStats.Type]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total connections gauge
|
||||||
|
for dbType, count := range typeCount {
|
||||||
|
connectionsTotal.WithLabelValues(string(dbType)).Set(float64(count))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update per-connection metrics
|
||||||
|
for name, connStats := range stats.ConnectionStats {
|
||||||
|
labels := prometheus.Labels{
|
||||||
|
"name": name,
|
||||||
|
"type": string(connStats.Type),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection status
|
||||||
|
status := float64(0)
|
||||||
|
if connStats.Connected && connStats.HealthCheckStatus == "healthy" {
|
||||||
|
status = 1
|
||||||
|
}
|
||||||
|
connectionStatus.With(labels).Set(status)
|
||||||
|
|
||||||
|
// Pool size metrics (SQL databases only)
|
||||||
|
if connStats.Type != DatabaseTypeMongoDB {
|
||||||
|
connectionPoolSize.WithLabelValues(name, string(connStats.Type), "open").Set(float64(connStats.OpenConnections))
|
||||||
|
connectionPoolSize.WithLabelValues(name, string(connStats.Type), "idle").Set(float64(connStats.Idle))
|
||||||
|
connectionPoolSize.WithLabelValues(name, string(connStats.Type), "in_use").Set(float64(connStats.InUse))
|
||||||
|
|
||||||
|
// Wait stats
|
||||||
|
connectionWaitCount.With(labels).Set(float64(connStats.WaitCount))
|
||||||
|
connectionWaitDuration.With(labels).Set(connStats.WaitDuration.Seconds())
|
||||||
|
|
||||||
|
// Lifetime/idle closed
|
||||||
|
connectionLifetimeClosed.With(labels).Set(float64(connStats.MaxLifetimeClosed))
|
||||||
|
connectionIdleClosed.With(labels).Set(float64(connStats.MaxIdleClosed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordReconnectAttempt records a reconnection attempt
|
||||||
|
func RecordReconnectAttempt(name string, dbType DatabaseType, success bool) {
|
||||||
|
result := "failure"
|
||||||
|
if success {
|
||||||
|
result = "success"
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnectAttempts.WithLabelValues(name, string(dbType), result).Inc()
|
||||||
|
}
|
||||||
319
pkg/dbmanager/providers/POSTGRES_NOTIFY_LISTEN.md
Normal file
319
pkg/dbmanager/providers/POSTGRES_NOTIFY_LISTEN.md
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
# PostgreSQL NOTIFY/LISTEN Support
|
||||||
|
|
||||||
|
The `dbmanager` package provides built-in support for PostgreSQL's NOTIFY/LISTEN functionality through the `PostgresListener` type.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
PostgreSQL NOTIFY/LISTEN is a simple pub/sub mechanism that allows database clients to:
|
||||||
|
- **LISTEN** on named channels to receive notifications
|
||||||
|
- **NOTIFY** channels to send messages to all listeners
|
||||||
|
- Receive asynchronous notifications without polling
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ Subscribe to multiple channels simultaneously
|
||||||
|
- ✅ Callback-based notification handling
|
||||||
|
- ✅ Automatic reconnection on connection loss
|
||||||
|
- ✅ Automatic resubscription after reconnection
|
||||||
|
- ✅ Thread-safe operations
|
||||||
|
- ✅ Panic recovery in notification handlers
|
||||||
|
- ✅ Dedicated connection for listening (doesn't interfere with queries)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/dbmanager/providers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create PostgreSQL provider
|
||||||
|
cfg := &providers.Config{
|
||||||
|
Name: "primary",
|
||||||
|
Type: "postgres",
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
User: "postgres",
|
||||||
|
Password: "password",
|
||||||
|
Database: "myapp",
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := providers.NewPostgresProvider()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := provider.Connect(ctx, cfg); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer provider.Close()
|
||||||
|
|
||||||
|
// Get listener
|
||||||
|
listener, err := provider.GetListener(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to a channel
|
||||||
|
err = listener.Listen("events", func(channel, payload string) {
|
||||||
|
fmt.Printf("Received on %s: %s\n", channel, payload)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a notification
|
||||||
|
err = listener.Notify(ctx, "events", "Hello, World!")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the program running
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Channels
|
||||||
|
|
||||||
|
```go
|
||||||
|
listener, _ := provider.GetListener(ctx)
|
||||||
|
|
||||||
|
// Listen to different channels with different handlers
|
||||||
|
listener.Listen("user_events", func(channel, payload string) {
|
||||||
|
fmt.Printf("User event: %s\n", payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
listener.Listen("order_events", func(channel, payload string) {
|
||||||
|
fmt.Printf("Order event: %s\n", payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
listener.Listen("payment_events", func(channel, payload string) {
|
||||||
|
fmt.Printf("Payment event: %s\n", payload)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unsubscribing
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Stop listening to a specific channel
|
||||||
|
err := listener.Unlisten("user_events")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to unlisten: %v\n", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking Active Channels
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get list of channels currently being listened to
|
||||||
|
channels := listener.Channels()
|
||||||
|
fmt.Printf("Listening to: %v\n", channels)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking Connection Status
|
||||||
|
|
||||||
|
```go
|
||||||
|
if listener.IsConnected() {
|
||||||
|
fmt.Println("Listener is connected")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Listener is disconnected")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with DBManager
|
||||||
|
|
||||||
|
When using the DBManager, you can access the listener through the PostgreSQL provider:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Initialize DBManager
|
||||||
|
mgr, err := dbmanager.NewManager(dbmanager.FromConfig(cfg.DBManager))
|
||||||
|
mgr.Connect(ctx)
|
||||||
|
defer mgr.Close()
|
||||||
|
|
||||||
|
// Get PostgreSQL connection
|
||||||
|
conn, err := mgr.Get("primary")
|
||||||
|
|
||||||
|
// Note: You'll need to cast to the underlying provider type
|
||||||
|
// This requires exposing the provider through the Connection interface
|
||||||
|
// or providing a helper method
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### Cache Invalidation
|
||||||
|
|
||||||
|
```go
|
||||||
|
listener.Listen("cache_invalidation", func(channel, payload string) {
|
||||||
|
// Parse the payload to determine what to invalidate
|
||||||
|
cache.Invalidate(payload)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Real-time Updates
|
||||||
|
|
||||||
|
```go
|
||||||
|
listener.Listen("data_updates", func(channel, payload string) {
|
||||||
|
// Broadcast update to WebSocket clients
|
||||||
|
websocketBroadcast(payload)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Reload
|
||||||
|
|
||||||
|
```go
|
||||||
|
listener.Listen("config_reload", func(channel, payload string) {
|
||||||
|
// Reload application configuration
|
||||||
|
config.Reload()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Distributed Locking
|
||||||
|
|
||||||
|
```go
|
||||||
|
listener.Listen("lock_released", func(channel, payload string) {
|
||||||
|
// Attempt to acquire the lock
|
||||||
|
tryAcquireLock(payload)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automatic Reconnection
|
||||||
|
|
||||||
|
The listener automatically handles connection failures:
|
||||||
|
|
||||||
|
1. When a connection error is detected, the listener initiates reconnection
|
||||||
|
2. Once reconnected, it automatically resubscribes to all previous channels
|
||||||
|
3. Notification handlers remain active throughout the reconnection process
|
||||||
|
|
||||||
|
No manual intervention is required for reconnection.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Handler Panics
|
||||||
|
|
||||||
|
If a notification handler panics, the panic is recovered and logged. The listener continues to function normally:
|
||||||
|
|
||||||
|
```go
|
||||||
|
listener.Listen("events", func(channel, payload string) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Printf("Handler panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Your event processing logic
|
||||||
|
processEvent(payload)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Errors
|
||||||
|
|
||||||
|
Connection errors trigger automatic reconnection. Check logs for reconnection events when `EnableLogging` is true.
|
||||||
|
|
||||||
|
## Thread Safety
|
||||||
|
|
||||||
|
All `PostgresListener` methods are thread-safe and can be called concurrently from multiple goroutines.
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
1. **Dedicated Connection**: The listener uses a dedicated PostgreSQL connection separate from the query connection pool
|
||||||
|
2. **Asynchronous Handlers**: Notification handlers run in separate goroutines to avoid blocking
|
||||||
|
3. **Lightweight**: NOTIFY/LISTEN has minimal overhead compared to polling
|
||||||
|
|
||||||
|
## Comparison with Polling
|
||||||
|
|
||||||
|
| Feature | NOTIFY/LISTEN | Polling |
|
||||||
|
|---------|---------------|---------|
|
||||||
|
| Latency | Low (near real-time) | High (depends on poll interval) |
|
||||||
|
| Database Load | Minimal | High (constant queries) |
|
||||||
|
| Scalability | Excellent | Poor |
|
||||||
|
| Complexity | Simple | Moderate |
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
1. **PostgreSQL Only**: This feature is specific to PostgreSQL and not available for other databases
|
||||||
|
2. **No Message Persistence**: Notifications are not stored; if no listener is connected, the message is lost
|
||||||
|
3. **Payload Limit**: Notification payload is limited to 8000 bytes in PostgreSQL
|
||||||
|
4. **No Guaranteed Delivery**: If a listener disconnects, in-flight notifications may be lost
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Keep Handlers Fast**: Notification handlers should be quick; for heavy processing, send work to a queue
|
||||||
|
2. **Use JSON Payloads**: Encode structured data as JSON for easy parsing
|
||||||
|
3. **Handle Errors Gracefully**: Always recover from panics in handlers
|
||||||
|
4. **Close Properly**: Always close the provider to ensure the listener is properly shut down
|
||||||
|
5. **Monitor Connection Status**: Use `IsConnected()` for health checks
|
||||||
|
|
||||||
|
## Example: Real-World Application
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Subscribe to various application events
|
||||||
|
listener, _ := provider.GetListener(ctx)
|
||||||
|
|
||||||
|
// User registration events
|
||||||
|
listener.Listen("user_registered", func(channel, payload string) {
|
||||||
|
var event UserRegisteredEvent
|
||||||
|
json.Unmarshal([]byte(payload), &event)
|
||||||
|
|
||||||
|
// Send welcome email
|
||||||
|
sendWelcomeEmail(event.UserID)
|
||||||
|
|
||||||
|
// Invalidate user count cache
|
||||||
|
cache.Delete("user_count")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Order placement events
|
||||||
|
listener.Listen("order_placed", func(channel, payload string) {
|
||||||
|
var event OrderPlacedEvent
|
||||||
|
json.Unmarshal([]byte(payload), &event)
|
||||||
|
|
||||||
|
// Notify warehouse system
|
||||||
|
warehouse.ProcessOrder(event.OrderID)
|
||||||
|
|
||||||
|
// Update inventory cache
|
||||||
|
cache.Invalidate("inventory:" + event.ProductID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Configuration changes
|
||||||
|
listener.Listen("config_updated", func(channel, payload string) {
|
||||||
|
// Reload configuration from database
|
||||||
|
appConfig.Reload()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Triggering Notifications from SQL
|
||||||
|
|
||||||
|
You can trigger notifications directly from PostgreSQL triggers or functions:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Example trigger to notify on new user
|
||||||
|
CREATE OR REPLACE FUNCTION notify_user_registered()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM pg_notify('user_registered',
|
||||||
|
json_build_object(
|
||||||
|
'user_id', NEW.id,
|
||||||
|
'email', NEW.email,
|
||||||
|
'timestamp', NOW()
|
||||||
|
)::text
|
||||||
|
);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER user_registered_trigger
|
||||||
|
AFTER INSERT ON users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION notify_user_registered();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [PostgreSQL NOTIFY Documentation](https://www.postgresql.org/docs/current/sql-notify.html)
|
||||||
|
- [PostgreSQL LISTEN Documentation](https://www.postgresql.org/docs/current/sql-listen.html)
|
||||||
|
- [pgx Driver Documentation](https://github.com/jackc/pgx)
|
||||||
214
pkg/dbmanager/providers/mongodb.go
Normal file
214
pkg/dbmanager/providers/mongodb.go
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MongoProvider implements Provider for MongoDB databases
|
||||||
|
type MongoProvider struct {
|
||||||
|
client *mongo.Client
|
||||||
|
config ConnectionConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMongoProvider creates a new MongoDB provider
|
||||||
|
func NewMongoProvider() *MongoProvider {
|
||||||
|
return &MongoProvider{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes a MongoDB connection
|
||||||
|
func (p *MongoProvider) Connect(ctx context.Context, cfg ConnectionConfig) error {
|
||||||
|
// Build DSN
|
||||||
|
dsn, err := cfg.BuildDSN()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build DSN: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create client options
|
||||||
|
clientOpts := options.Client().ApplyURI(dsn)
|
||||||
|
|
||||||
|
// Set connection pool size
|
||||||
|
if cfg.GetMaxOpenConns() != nil {
|
||||||
|
maxPoolSize := uint64(*cfg.GetMaxOpenConns())
|
||||||
|
clientOpts.SetMaxPoolSize(maxPoolSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.GetMaxIdleConns() != nil {
|
||||||
|
minPoolSize := uint64(*cfg.GetMaxIdleConns())
|
||||||
|
clientOpts.SetMinPoolSize(minPoolSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timeouts
|
||||||
|
clientOpts.SetConnectTimeout(cfg.GetConnectTimeout())
|
||||||
|
if cfg.GetQueryTimeout() > 0 {
|
||||||
|
clientOpts.SetTimeout(cfg.GetQueryTimeout())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set read preference if specified
|
||||||
|
if cfg.GetReadPreference() != "" {
|
||||||
|
rp, err := parseReadPreference(cfg.GetReadPreference())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid read preference: %w", err)
|
||||||
|
}
|
||||||
|
clientOpts.SetReadPreference(rp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect with retry logic
|
||||||
|
var client *mongo.Client
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
retryAttempts := 3
|
||||||
|
retryDelay := 1 * time.Second
|
||||||
|
|
||||||
|
for attempt := 0; attempt < retryAttempts; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
delay := calculateBackoff(attempt, retryDelay, 10*time.Second)
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Info("Retrying MongoDB connection: attempt=%d/%d, delay=%v", attempt+1, retryAttempts, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(delay):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MongoDB client
|
||||||
|
client, err = mongo.Connect(ctx, clientOpts)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Warn("Failed to connect to MongoDB", "error", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping the database to verify connection
|
||||||
|
pingCtx, cancel := context.WithTimeout(ctx, cfg.GetConnectTimeout())
|
||||||
|
err = client.Ping(pingCtx, readpref.Primary())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
_ = client.Disconnect(ctx)
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Warn("Failed to ping MongoDB", "error", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection successful
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect after %d attempts: %w", retryAttempts, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.client = client
|
||||||
|
p.config = cfg
|
||||||
|
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Info("MongoDB connection established: name=%s, host=%s, database=%s", cfg.GetName(), cfg.GetHost(), cfg.GetDatabase())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the MongoDB connection
|
||||||
|
func (p *MongoProvider) Close() error {
|
||||||
|
if p.client == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := p.client.Disconnect(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close MongoDB connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.config.GetEnableLogging() {
|
||||||
|
logger.Info("MongoDB connection closed: name=%s", p.config.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
p.client = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck verifies the MongoDB connection is alive
|
||||||
|
func (p *MongoProvider) HealthCheck(ctx context.Context) error {
|
||||||
|
if p.client == nil {
|
||||||
|
return fmt.Errorf("MongoDB client is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a short timeout for health checks
|
||||||
|
healthCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := p.client.Ping(healthCtx, readpref.Primary()); err != nil {
|
||||||
|
return fmt.Errorf("health check failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNative returns an error for MongoDB (not a SQL database)
|
||||||
|
func (p *MongoProvider) GetNative() (*sql.DB, error) {
|
||||||
|
return nil, ErrNotSQLDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMongo returns the MongoDB client
|
||||||
|
func (p *MongoProvider) GetMongo() (*mongo.Client, error) {
|
||||||
|
if p.client == nil {
|
||||||
|
return nil, fmt.Errorf("MongoDB client is not initialized")
|
||||||
|
}
|
||||||
|
return p.client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns connection statistics for MongoDB
|
||||||
|
func (p *MongoProvider) Stats() *ConnectionStats {
|
||||||
|
if p.client == nil {
|
||||||
|
return &ConnectionStats{
|
||||||
|
Name: p.config.GetName(),
|
||||||
|
Type: "mongodb",
|
||||||
|
Connected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MongoDB doesn't expose detailed connection pool stats like sql.DB
|
||||||
|
// We return basic stats
|
||||||
|
return &ConnectionStats{
|
||||||
|
Name: p.config.GetName(),
|
||||||
|
Type: "mongodb",
|
||||||
|
Connected: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseReadPreference parses a read preference string into a readpref.ReadPref
|
||||||
|
func parseReadPreference(rp string) (*readpref.ReadPref, error) {
|
||||||
|
switch rp {
|
||||||
|
case "primary":
|
||||||
|
return readpref.Primary(), nil
|
||||||
|
case "primaryPreferred":
|
||||||
|
return readpref.PrimaryPreferred(), nil
|
||||||
|
case "secondary":
|
||||||
|
return readpref.Secondary(), nil
|
||||||
|
case "secondaryPreferred":
|
||||||
|
return readpref.SecondaryPreferred(), nil
|
||||||
|
case "nearest":
|
||||||
|
return readpref.Nearest(), nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown read preference: %s", rp)
|
||||||
|
}
|
||||||
|
}
|
||||||
184
pkg/dbmanager/providers/mssql.go
Normal file
184
pkg/dbmanager/providers/mssql.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/microsoft/go-mssqldb" // MSSQL driver
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MSSQLProvider implements Provider for Microsoft SQL Server databases
|
||||||
|
type MSSQLProvider struct {
|
||||||
|
db *sql.DB
|
||||||
|
config ConnectionConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMSSQLProvider creates a new MSSQL provider
|
||||||
|
func NewMSSQLProvider() *MSSQLProvider {
|
||||||
|
return &MSSQLProvider{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes a MSSQL connection
|
||||||
|
func (p *MSSQLProvider) Connect(ctx context.Context, cfg ConnectionConfig) error {
|
||||||
|
// Build DSN
|
||||||
|
dsn, err := cfg.BuildDSN()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build DSN: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect with retry logic
|
||||||
|
var db *sql.DB
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
retryAttempts := 3 // Default retry attempts
|
||||||
|
retryDelay := 1 * time.Second
|
||||||
|
|
||||||
|
for attempt := 0; attempt < retryAttempts; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
delay := calculateBackoff(attempt, retryDelay, 10*time.Second)
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Info("Retrying MSSQL connection: attempt=%d/%d, delay=%v", attempt+1, retryAttempts, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(delay):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open database connection
|
||||||
|
db, err = sql.Open("sqlserver", dsn)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Warn("Failed to open MSSQL connection", "error", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the connection with context timeout
|
||||||
|
connectCtx, cancel := context.WithTimeout(ctx, cfg.GetConnectTimeout())
|
||||||
|
err = db.PingContext(connectCtx)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
db.Close()
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Warn("Failed to ping MSSQL database", "error", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection successful
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect after %d attempts: %w", retryAttempts, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure connection pool
|
||||||
|
if cfg.GetMaxOpenConns() != nil {
|
||||||
|
db.SetMaxOpenConns(*cfg.GetMaxOpenConns())
|
||||||
|
}
|
||||||
|
if cfg.GetMaxIdleConns() != nil {
|
||||||
|
db.SetMaxIdleConns(*cfg.GetMaxIdleConns())
|
||||||
|
}
|
||||||
|
if cfg.GetConnMaxLifetime() != nil {
|
||||||
|
db.SetConnMaxLifetime(*cfg.GetConnMaxLifetime())
|
||||||
|
}
|
||||||
|
if cfg.GetConnMaxIdleTime() != nil {
|
||||||
|
db.SetConnMaxIdleTime(*cfg.GetConnMaxIdleTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
p.db = db
|
||||||
|
p.config = cfg
|
||||||
|
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Info("MSSQL connection established: name=%s, host=%s, database=%s", cfg.GetName(), cfg.GetHost(), cfg.GetDatabase())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the MSSQL connection
|
||||||
|
func (p *MSSQLProvider) Close() error {
|
||||||
|
if p.db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.db.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close MSSQL connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.config.GetEnableLogging() {
|
||||||
|
logger.Info("MSSQL connection closed: name=%s", p.config.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
p.db = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck verifies the MSSQL connection is alive
|
||||||
|
func (p *MSSQLProvider) HealthCheck(ctx context.Context) error {
|
||||||
|
if p.db == nil {
|
||||||
|
return fmt.Errorf("database connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a short timeout for health checks
|
||||||
|
healthCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := p.db.PingContext(healthCtx); err != nil {
|
||||||
|
return fmt.Errorf("health check failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNative returns the native *sql.DB connection
|
||||||
|
func (p *MSSQLProvider) GetNative() (*sql.DB, error) {
|
||||||
|
if p.db == nil {
|
||||||
|
return nil, fmt.Errorf("database connection is not initialized")
|
||||||
|
}
|
||||||
|
return p.db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMongo returns an error for MSSQL (not a MongoDB connection)
|
||||||
|
func (p *MSSQLProvider) GetMongo() (*mongo.Client, error) {
|
||||||
|
return nil, ErrNotMongoDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns connection pool statistics
|
||||||
|
func (p *MSSQLProvider) Stats() *ConnectionStats {
|
||||||
|
if p.db == nil {
|
||||||
|
return &ConnectionStats{
|
||||||
|
Name: p.config.GetName(),
|
||||||
|
Type: "mssql",
|
||||||
|
Connected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := p.db.Stats()
|
||||||
|
|
||||||
|
return &ConnectionStats{
|
||||||
|
Name: p.config.GetName(),
|
||||||
|
Type: "mssql",
|
||||||
|
Connected: true,
|
||||||
|
OpenConnections: stats.OpenConnections,
|
||||||
|
InUse: stats.InUse,
|
||||||
|
Idle: stats.Idle,
|
||||||
|
WaitCount: stats.WaitCount,
|
||||||
|
WaitDuration: stats.WaitDuration,
|
||||||
|
MaxIdleClosed: stats.MaxIdleClosed,
|
||||||
|
MaxLifetimeClosed: stats.MaxLifetimeClosed,
|
||||||
|
}
|
||||||
|
}
|
||||||
231
pkg/dbmanager/providers/postgres.go
Normal file
231
pkg/dbmanager/providers/postgres.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/jackc/pgx/v5/stdlib" // PostgreSQL driver
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostgresProvider implements Provider for PostgreSQL databases
|
||||||
|
type PostgresProvider struct {
|
||||||
|
db *sql.DB
|
||||||
|
config ConnectionConfig
|
||||||
|
listener *PostgresListener
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPostgresProvider creates a new PostgreSQL provider
|
||||||
|
func NewPostgresProvider() *PostgresProvider {
|
||||||
|
return &PostgresProvider{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes a PostgreSQL connection
|
||||||
|
func (p *PostgresProvider) Connect(ctx context.Context, cfg ConnectionConfig) error {
|
||||||
|
// Build DSN
|
||||||
|
dsn, err := cfg.BuildDSN()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build DSN: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect with retry logic
|
||||||
|
var db *sql.DB
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
retryAttempts := 3 // Default retry attempts
|
||||||
|
retryDelay := 1 * time.Second
|
||||||
|
|
||||||
|
for attempt := 0; attempt < retryAttempts; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
delay := calculateBackoff(attempt, retryDelay, 10*time.Second)
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Info("Retrying PostgreSQL connection: attempt=%d/%d, delay=%v", attempt+1, retryAttempts, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(delay):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open database connection
|
||||||
|
db, err = sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Warn("Failed to open PostgreSQL connection", "error", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the connection with context timeout
|
||||||
|
connectCtx, cancel := context.WithTimeout(ctx, cfg.GetConnectTimeout())
|
||||||
|
err = db.PingContext(connectCtx)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
db.Close()
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Warn("Failed to ping PostgreSQL database", "error", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection successful
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect after %d attempts: %w", retryAttempts, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure connection pool
|
||||||
|
if cfg.GetMaxOpenConns() != nil {
|
||||||
|
db.SetMaxOpenConns(*cfg.GetMaxOpenConns())
|
||||||
|
}
|
||||||
|
if cfg.GetMaxIdleConns() != nil {
|
||||||
|
db.SetMaxIdleConns(*cfg.GetMaxIdleConns())
|
||||||
|
}
|
||||||
|
if cfg.GetConnMaxLifetime() != nil {
|
||||||
|
db.SetConnMaxLifetime(*cfg.GetConnMaxLifetime())
|
||||||
|
}
|
||||||
|
if cfg.GetConnMaxIdleTime() != nil {
|
||||||
|
db.SetConnMaxIdleTime(*cfg.GetConnMaxIdleTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
p.db = db
|
||||||
|
p.config = cfg
|
||||||
|
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Info("PostgreSQL connection established: name=%s, host=%s, database=%s", cfg.GetName(), cfg.GetHost(), cfg.GetDatabase())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the PostgreSQL connection
|
||||||
|
func (p *PostgresProvider) Close() error {
|
||||||
|
// Close listener if it exists
|
||||||
|
p.mu.Lock()
|
||||||
|
if p.listener != nil {
|
||||||
|
if err := p.listener.Close(); err != nil {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return fmt.Errorf("failed to close listener: %w", err)
|
||||||
|
}
|
||||||
|
p.listener = nil
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
if p.db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.db.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close PostgreSQL connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.config.GetEnableLogging() {
|
||||||
|
logger.Info("PostgreSQL connection closed: name=%s", p.config.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
p.db = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck verifies the PostgreSQL connection is alive
|
||||||
|
func (p *PostgresProvider) HealthCheck(ctx context.Context) error {
|
||||||
|
if p.db == nil {
|
||||||
|
return fmt.Errorf("database connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a short timeout for health checks
|
||||||
|
healthCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := p.db.PingContext(healthCtx); err != nil {
|
||||||
|
return fmt.Errorf("health check failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNative returns the native *sql.DB connection
|
||||||
|
func (p *PostgresProvider) GetNative() (*sql.DB, error) {
|
||||||
|
if p.db == nil {
|
||||||
|
return nil, fmt.Errorf("database connection is not initialized")
|
||||||
|
}
|
||||||
|
return p.db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMongo returns an error for PostgreSQL (not a MongoDB connection)
|
||||||
|
func (p *PostgresProvider) GetMongo() (*mongo.Client, error) {
|
||||||
|
return nil, ErrNotMongoDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns connection pool statistics
|
||||||
|
func (p *PostgresProvider) Stats() *ConnectionStats {
|
||||||
|
if p.db == nil {
|
||||||
|
return &ConnectionStats{
|
||||||
|
Name: p.config.GetName(),
|
||||||
|
Type: "postgres",
|
||||||
|
Connected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := p.db.Stats()
|
||||||
|
|
||||||
|
return &ConnectionStats{
|
||||||
|
Name: p.config.GetName(),
|
||||||
|
Type: "postgres",
|
||||||
|
Connected: true,
|
||||||
|
OpenConnections: stats.OpenConnections,
|
||||||
|
InUse: stats.InUse,
|
||||||
|
Idle: stats.Idle,
|
||||||
|
WaitCount: stats.WaitCount,
|
||||||
|
WaitDuration: stats.WaitDuration,
|
||||||
|
MaxIdleClosed: stats.MaxIdleClosed,
|
||||||
|
MaxLifetimeClosed: stats.MaxLifetimeClosed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetListener returns a PostgreSQL listener for NOTIFY/LISTEN functionality
|
||||||
|
// The listener is lazily initialized on first call and reused for subsequent calls
|
||||||
|
func (p *PostgresProvider) GetListener(ctx context.Context) (*PostgresListener, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
// Return existing listener if already created
|
||||||
|
if p.listener != nil {
|
||||||
|
return p.listener, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new listener
|
||||||
|
listener := NewPostgresListener(p.config)
|
||||||
|
|
||||||
|
// Connect the listener
|
||||||
|
if err := listener.Connect(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.listener = listener
|
||||||
|
return p.listener, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateBackoff calculates exponential backoff delay
|
||||||
|
func calculateBackoff(attempt int, initial, maxDelay time.Duration) time.Duration {
|
||||||
|
delay := initial * time.Duration(math.Pow(2, float64(attempt)))
|
||||||
|
if delay > maxDelay {
|
||||||
|
delay = maxDelay
|
||||||
|
}
|
||||||
|
return delay
|
||||||
|
}
|
||||||
401
pkg/dbmanager/providers/postgres_listener.go
Normal file
401
pkg/dbmanager/providers/postgres_listener.go
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotificationHandler is called when a notification is received
|
||||||
|
type NotificationHandler func(channel string, payload string)
|
||||||
|
|
||||||
|
// PostgresListener manages PostgreSQL LISTEN/NOTIFY functionality
|
||||||
|
type PostgresListener struct {
|
||||||
|
config ConnectionConfig
|
||||||
|
conn *pgx.Conn
|
||||||
|
|
||||||
|
// Channel subscriptions
|
||||||
|
channels map[string]NotificationHandler
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// Lifecycle management
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
closed bool
|
||||||
|
closeMu sync.Mutex
|
||||||
|
reconnectC chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPostgresListener creates a new PostgreSQL listener
|
||||||
|
func NewPostgresListener(cfg ConnectionConfig) *PostgresListener {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
return &PostgresListener{
|
||||||
|
config: cfg,
|
||||||
|
channels: make(map[string]NotificationHandler),
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
reconnectC: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes a dedicated connection for listening
|
||||||
|
func (l *PostgresListener) Connect(ctx context.Context) error {
|
||||||
|
dsn, err := l.config.BuildDSN()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build DSN: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse connection config
|
||||||
|
connConfig, err := pgx.ParseConfig(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse connection config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect with retry logic
|
||||||
|
var conn *pgx.Conn
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
retryAttempts := 3
|
||||||
|
retryDelay := 1 * time.Second
|
||||||
|
|
||||||
|
for attempt := 0; attempt < retryAttempts; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
delay := calculateBackoff(attempt, retryDelay, 10*time.Second)
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Info("Retrying PostgreSQL listener connection: attempt=%d/%d, delay=%v", attempt+1, retryAttempts, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(delay):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err = pgx.ConnectConfig(ctx, connConfig)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Warn("Failed to connect PostgreSQL listener", "error", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the connection
|
||||||
|
if err = conn.Ping(ctx); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
conn.Close(ctx)
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Warn("Failed to ping PostgreSQL listener", "error", err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection successful
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect listener after %d attempts: %w", retryAttempts, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
l.conn = conn
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
// Start notification handler
|
||||||
|
go l.handleNotifications()
|
||||||
|
|
||||||
|
// Start reconnection handler
|
||||||
|
go l.handleReconnection()
|
||||||
|
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Info("PostgreSQL listener connected: name=%s", l.config.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen subscribes to a PostgreSQL notification channel
|
||||||
|
func (l *PostgresListener) Listen(channel string, handler NotificationHandler) error {
|
||||||
|
l.closeMu.Lock()
|
||||||
|
if l.closed {
|
||||||
|
l.closeMu.Unlock()
|
||||||
|
return fmt.Errorf("listener is closed")
|
||||||
|
}
|
||||||
|
l.closeMu.Unlock()
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
if l.conn == nil {
|
||||||
|
return fmt.Errorf("listener connection is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute LISTEN command
|
||||||
|
_, err := l.conn.Exec(l.ctx, fmt.Sprintf("LISTEN %s", pgx.Identifier{channel}.Sanitize()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to listen on channel %s: %w", channel, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the handler
|
||||||
|
l.channels[channel] = handler
|
||||||
|
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Info("Listening on channel: name=%s, channel=%s", l.config.GetName(), channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlisten unsubscribes from a PostgreSQL notification channel
|
||||||
|
func (l *PostgresListener) Unlisten(channel string) error {
|
||||||
|
l.closeMu.Lock()
|
||||||
|
if l.closed {
|
||||||
|
l.closeMu.Unlock()
|
||||||
|
return fmt.Errorf("listener is closed")
|
||||||
|
}
|
||||||
|
l.closeMu.Unlock()
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
if l.conn == nil {
|
||||||
|
return fmt.Errorf("listener connection is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute UNLISTEN command
|
||||||
|
_, err := l.conn.Exec(l.ctx, fmt.Sprintf("UNLISTEN %s", pgx.Identifier{channel}.Sanitize()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to unlisten from channel %s: %w", channel, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the handler
|
||||||
|
delete(l.channels, channel)
|
||||||
|
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Info("Unlistened from channel: name=%s, channel=%s", l.config.GetName(), channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify sends a notification to a PostgreSQL channel
|
||||||
|
func (l *PostgresListener) Notify(ctx context.Context, channel string, payload string) error {
|
||||||
|
l.closeMu.Lock()
|
||||||
|
if l.closed {
|
||||||
|
l.closeMu.Unlock()
|
||||||
|
return fmt.Errorf("listener is closed")
|
||||||
|
}
|
||||||
|
l.closeMu.Unlock()
|
||||||
|
|
||||||
|
l.mu.RLock()
|
||||||
|
conn := l.conn
|
||||||
|
l.mu.RUnlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
return fmt.Errorf("listener connection is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute NOTIFY command
|
||||||
|
_, err := conn.Exec(ctx, "SELECT pg_notify($1, $2)", channel, payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to notify channel %s: %w", channel, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the listener and all subscriptions
|
||||||
|
func (l *PostgresListener) Close() error {
|
||||||
|
l.closeMu.Lock()
|
||||||
|
if l.closed {
|
||||||
|
l.closeMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
l.closed = true
|
||||||
|
l.closeMu.Unlock()
|
||||||
|
|
||||||
|
// Cancel context to stop background goroutines
|
||||||
|
l.cancel()
|
||||||
|
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
if l.conn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlisten from all channels
|
||||||
|
for channel := range l.channels {
|
||||||
|
_, _ = l.conn.Exec(context.Background(), fmt.Sprintf("UNLISTEN %s", pgx.Identifier{channel}.Sanitize()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close connection
|
||||||
|
err := l.conn.Close(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close listener connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.conn = nil
|
||||||
|
l.channels = make(map[string]NotificationHandler)
|
||||||
|
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Info("PostgreSQL listener closed: name=%s", l.config.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNotifications processes incoming notifications
|
||||||
|
func (l *PostgresListener) handleNotifications() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-l.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
l.mu.RLock()
|
||||||
|
conn := l.conn
|
||||||
|
l.mu.RUnlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
// Connection not available, wait for reconnection
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for notification with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(l.ctx, 5*time.Second)
|
||||||
|
notification, err := conn.WaitForNotification(ctx)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Check if context was cancelled
|
||||||
|
if l.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a connection error
|
||||||
|
if pgconn.Timeout(err) {
|
||||||
|
// Timeout is normal, continue waiting
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection error, trigger reconnection
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Warn("Notification error, triggering reconnection", "error", err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case l.reconnectC <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process notification
|
||||||
|
l.mu.RLock()
|
||||||
|
handler, exists := l.channels[notification.Channel]
|
||||||
|
l.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists && handler != nil {
|
||||||
|
// Call handler in a goroutine to avoid blocking
|
||||||
|
go func(ch, payload string) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Error("Notification handler panic: channel=%s, error=%v", ch, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
handler(ch, payload)
|
||||||
|
}(notification.Channel, notification.Payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleReconnection manages automatic reconnection
|
||||||
|
func (l *PostgresListener) handleReconnection() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-l.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-l.reconnectC:
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Info("Attempting to reconnect listener: name=%s", l.config.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close existing connection
|
||||||
|
l.mu.Lock()
|
||||||
|
if l.conn != nil {
|
||||||
|
l.conn.Close(context.Background())
|
||||||
|
l.conn = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current subscriptions
|
||||||
|
channels := make(map[string]NotificationHandler)
|
||||||
|
for ch, handler := range l.channels {
|
||||||
|
channels[ch] = handler
|
||||||
|
}
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
// Attempt reconnection
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
err := l.Connect(ctx)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Error("Failed to reconnect listener: name=%s, error=%v", l.config.GetName(), err)
|
||||||
|
}
|
||||||
|
// Retry after delay
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
select {
|
||||||
|
case l.reconnectC <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resubscribe to all channels
|
||||||
|
for channel, handler := range channels {
|
||||||
|
if err := l.Listen(channel, handler); err != nil {
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Error("Failed to resubscribe to channel: name=%s, channel=%s, error=%v", l.config.GetName(), channel, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.config.GetEnableLogging() {
|
||||||
|
logger.Info("Listener reconnected successfully: name=%s", l.config.GetName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConnected returns true if the listener is connected
|
||||||
|
func (l *PostgresListener) IsConnected() bool {
|
||||||
|
l.mu.RLock()
|
||||||
|
defer l.mu.RUnlock()
|
||||||
|
return l.conn != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channels returns the list of channels currently being listened to
|
||||||
|
func (l *PostgresListener) Channels() []string {
|
||||||
|
l.mu.RLock()
|
||||||
|
defer l.mu.RUnlock()
|
||||||
|
|
||||||
|
channels := make([]string, 0, len(l.channels))
|
||||||
|
for ch := range l.channels {
|
||||||
|
channels = append(channels, ch)
|
||||||
|
}
|
||||||
|
return channels
|
||||||
|
}
|
||||||
228
pkg/dbmanager/providers/postgres_listener_example_test.go
Normal file
228
pkg/dbmanager/providers/postgres_listener_example_test.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
package providers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/dbmanager"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/dbmanager/providers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExamplePostgresListener_basic demonstrates basic LISTEN/NOTIFY usage
|
||||||
|
func ExamplePostgresListener_basic() {
|
||||||
|
// Create a connection config
|
||||||
|
cfg := &dbmanager.ConnectionConfig{
|
||||||
|
Name: "example",
|
||||||
|
Type: dbmanager.DatabaseTypePostgreSQL,
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
User: "postgres",
|
||||||
|
Password: "password",
|
||||||
|
Database: "testdb",
|
||||||
|
ConnectTimeout: 10 * time.Second,
|
||||||
|
EnableLogging: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and connect PostgreSQL provider
|
||||||
|
provider := providers.NewPostgresProvider()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := provider.Connect(ctx, cfg); err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to connect: %v", err))
|
||||||
|
}
|
||||||
|
defer provider.Close()
|
||||||
|
|
||||||
|
// Get listener
|
||||||
|
listener, err := provider.GetListener(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to get listener: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to a channel with a handler
|
||||||
|
err = listener.Listen("user_events", func(channel, payload string) {
|
||||||
|
fmt.Printf("Received notification on %s: %s\n", channel, payload)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to listen: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a notification
|
||||||
|
err = listener.Notify(ctx, "user_events", `{"event":"user_created","user_id":123}`)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to notify: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for notification to be processed
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Unsubscribe from the channel
|
||||||
|
if err := listener.Unlisten("user_events"); err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to unlisten: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExamplePostgresListener_multipleChannels demonstrates listening to multiple channels
|
||||||
|
func ExamplePostgresListener_multipleChannels() {
|
||||||
|
cfg := &dbmanager.ConnectionConfig{
|
||||||
|
Name: "example",
|
||||||
|
Type: dbmanager.DatabaseTypePostgreSQL,
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
User: "postgres",
|
||||||
|
Password: "password",
|
||||||
|
Database: "testdb",
|
||||||
|
ConnectTimeout: 10 * time.Second,
|
||||||
|
EnableLogging: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := providers.NewPostgresProvider()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := provider.Connect(ctx, cfg); err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to connect: %v", err))
|
||||||
|
}
|
||||||
|
defer provider.Close()
|
||||||
|
|
||||||
|
listener, err := provider.GetListener(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to get listener: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to multiple channels
|
||||||
|
channels := []string{"orders", "payments", "notifications"}
|
||||||
|
for _, ch := range channels {
|
||||||
|
channel := ch // Capture for closure
|
||||||
|
err := listener.Listen(channel, func(ch, payload string) {
|
||||||
|
fmt.Printf("[%s] %s\n", ch, payload)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to listen on %s: %v", channel, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notifications to different channels
|
||||||
|
listener.Notify(ctx, "orders", "New order #12345")
|
||||||
|
listener.Notify(ctx, "payments", "Payment received $99.99")
|
||||||
|
listener.Notify(ctx, "notifications", "Welcome email sent")
|
||||||
|
|
||||||
|
// Wait for notifications
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
// Check active channels
|
||||||
|
activeChannels := listener.Channels()
|
||||||
|
fmt.Printf("Listening to %d channels: %v\n", len(activeChannels), activeChannels)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExamplePostgresListener_withDBManager demonstrates usage with DBManager
|
||||||
|
func ExamplePostgresListener_withDBManager() {
|
||||||
|
// This example shows how to use the listener with the full DBManager
|
||||||
|
|
||||||
|
// Assume we have a DBManager instance and get a connection
|
||||||
|
// conn, _ := dbMgr.Get("primary")
|
||||||
|
|
||||||
|
// Get the underlying provider (this would need to be exposed via the Connection interface)
|
||||||
|
// For now, this is a conceptual example
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Create provider directly for demonstration
|
||||||
|
cfg := &dbmanager.ConnectionConfig{
|
||||||
|
Name: "primary",
|
||||||
|
Type: dbmanager.DatabaseTypePostgreSQL,
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
User: "postgres",
|
||||||
|
Password: "password",
|
||||||
|
Database: "myapp",
|
||||||
|
ConnectTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := providers.NewPostgresProvider()
|
||||||
|
if err := provider.Connect(ctx, cfg); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer provider.Close()
|
||||||
|
|
||||||
|
// Get listener
|
||||||
|
listener, err := provider.GetListener(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to application events
|
||||||
|
listener.Listen("cache_invalidation", func(channel, payload string) {
|
||||||
|
fmt.Printf("Cache invalidation request: %s\n", payload)
|
||||||
|
// Handle cache invalidation logic here
|
||||||
|
})
|
||||||
|
|
||||||
|
listener.Listen("config_reload", func(channel, payload string) {
|
||||||
|
fmt.Printf("Configuration reload request: %s\n", payload)
|
||||||
|
// Handle configuration reload logic here
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simulate receiving notifications
|
||||||
|
listener.Notify(ctx, "cache_invalidation", "user:123")
|
||||||
|
listener.Notify(ctx, "config_reload", "database")
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExamplePostgresListener_errorHandling demonstrates error handling and reconnection
|
||||||
|
func ExamplePostgresListener_errorHandling() {
|
||||||
|
cfg := &dbmanager.ConnectionConfig{
|
||||||
|
Name: "example",
|
||||||
|
Type: dbmanager.DatabaseTypePostgreSQL,
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
User: "postgres",
|
||||||
|
Password: "password",
|
||||||
|
Database: "testdb",
|
||||||
|
ConnectTimeout: 10 * time.Second,
|
||||||
|
EnableLogging: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := providers.NewPostgresProvider()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
if err := provider.Connect(ctx, cfg); err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to connect: %v", err))
|
||||||
|
}
|
||||||
|
defer provider.Close()
|
||||||
|
|
||||||
|
listener, err := provider.GetListener(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Failed to get listener: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The listener automatically reconnects if the connection is lost
|
||||||
|
// Subscribe with error handling in the callback
|
||||||
|
err = listener.Listen("critical_events", func(channel, payload string) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
fmt.Printf("Handler panic recovered: %v\n", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Process the event
|
||||||
|
fmt.Printf("Processing critical event: %s\n", payload)
|
||||||
|
|
||||||
|
// If processing fails, the panic will be caught by the defer above
|
||||||
|
// The listener will continue to function normally
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to listen: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if listener is connected
|
||||||
|
if listener.IsConnected() {
|
||||||
|
fmt.Println("Listener is connected and ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a notification
|
||||||
|
listener.Notify(ctx, "critical_events", "system_alert")
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
83
pkg/dbmanager/providers/provider.go
Normal file
83
pkg/dbmanager/providers/provider.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common errors
|
||||||
|
var (
|
||||||
|
// ErrNotSQLDatabase is returned when attempting SQL operations on a non-SQL database
|
||||||
|
ErrNotSQLDatabase = errors.New("not a SQL database")
|
||||||
|
|
||||||
|
// ErrNotMongoDB is returned when attempting MongoDB operations on a non-MongoDB connection
|
||||||
|
ErrNotMongoDB = errors.New("not a MongoDB connection")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnectionStats contains statistics about a database connection
|
||||||
|
type ConnectionStats struct {
|
||||||
|
Name string
|
||||||
|
Type string // Database type as string to avoid circular dependency
|
||||||
|
Connected bool
|
||||||
|
LastHealthCheck time.Time
|
||||||
|
HealthCheckStatus string
|
||||||
|
|
||||||
|
// SQL connection pool stats
|
||||||
|
OpenConnections int
|
||||||
|
InUse int
|
||||||
|
Idle int
|
||||||
|
WaitCount int64
|
||||||
|
WaitDuration time.Duration
|
||||||
|
MaxIdleClosed int64
|
||||||
|
MaxLifetimeClosed int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionConfig is a minimal interface for configuration
|
||||||
|
// The actual implementation is in dbmanager package
|
||||||
|
type ConnectionConfig interface {
|
||||||
|
BuildDSN() (string, error)
|
||||||
|
GetName() string
|
||||||
|
GetType() string
|
||||||
|
GetHost() string
|
||||||
|
GetPort() int
|
||||||
|
GetUser() string
|
||||||
|
GetPassword() string
|
||||||
|
GetDatabase() string
|
||||||
|
GetFilePath() string
|
||||||
|
GetConnectTimeout() time.Duration
|
||||||
|
GetQueryTimeout() time.Duration
|
||||||
|
GetEnableLogging() bool
|
||||||
|
GetEnableMetrics() bool
|
||||||
|
GetMaxOpenConns() *int
|
||||||
|
GetMaxIdleConns() *int
|
||||||
|
GetConnMaxLifetime() *time.Duration
|
||||||
|
GetConnMaxIdleTime() *time.Duration
|
||||||
|
GetReadPreference() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider creates and manages the underlying database connection
|
||||||
|
type Provider interface {
|
||||||
|
// Connect establishes the database connection
|
||||||
|
Connect(ctx context.Context, cfg ConnectionConfig) error
|
||||||
|
|
||||||
|
// Close closes the connection
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
// HealthCheck verifies the connection is alive
|
||||||
|
HealthCheck(ctx context.Context) error
|
||||||
|
|
||||||
|
// GetNative returns the native *sql.DB (SQL databases only)
|
||||||
|
// Returns an error for non-SQL databases
|
||||||
|
GetNative() (*sql.DB, error)
|
||||||
|
|
||||||
|
// GetMongo returns the MongoDB client (MongoDB only)
|
||||||
|
// Returns an error for non-MongoDB databases
|
||||||
|
GetMongo() (*mongo.Client, error)
|
||||||
|
|
||||||
|
// Stats returns connection statistics
|
||||||
|
Stats() *ConnectionStats
|
||||||
|
}
|
||||||
177
pkg/dbmanager/providers/sqlite.go
Normal file
177
pkg/dbmanager/providers/sqlite.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/glebarez/sqlite" // Pure Go SQLite driver
|
||||||
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SQLiteProvider implements Provider for SQLite databases
|
||||||
|
type SQLiteProvider struct {
|
||||||
|
db *sql.DB
|
||||||
|
config ConnectionConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSQLiteProvider creates a new SQLite provider
|
||||||
|
func NewSQLiteProvider() *SQLiteProvider {
|
||||||
|
return &SQLiteProvider{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect establishes a SQLite connection
|
||||||
|
func (p *SQLiteProvider) Connect(ctx context.Context, cfg ConnectionConfig) error {
|
||||||
|
// Build DSN
|
||||||
|
dsn, err := cfg.BuildDSN()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build DSN: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open database connection
|
||||||
|
db, err := sql.Open("sqlite", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open SQLite connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the connection with context timeout
|
||||||
|
connectCtx, cancel := context.WithTimeout(ctx, cfg.GetConnectTimeout())
|
||||||
|
err = db.PingContext(connectCtx)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
db.Close()
|
||||||
|
return fmt.Errorf("failed to ping SQLite database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure connection pool
|
||||||
|
// Note: SQLite works best with MaxOpenConns=1 for write operations
|
||||||
|
// but can handle multiple readers
|
||||||
|
if cfg.GetMaxOpenConns() != nil {
|
||||||
|
db.SetMaxOpenConns(*cfg.GetMaxOpenConns())
|
||||||
|
} else {
|
||||||
|
// Default to 1 for SQLite to avoid "database is locked" errors
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.GetMaxIdleConns() != nil {
|
||||||
|
db.SetMaxIdleConns(*cfg.GetMaxIdleConns())
|
||||||
|
}
|
||||||
|
if cfg.GetConnMaxLifetime() != nil {
|
||||||
|
db.SetConnMaxLifetime(*cfg.GetConnMaxLifetime())
|
||||||
|
}
|
||||||
|
if cfg.GetConnMaxIdleTime() != nil {
|
||||||
|
db.SetConnMaxIdleTime(*cfg.GetConnMaxIdleTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable WAL mode for better concurrent access
|
||||||
|
_, err = db.ExecContext(ctx, "PRAGMA journal_mode=WAL")
|
||||||
|
if err != nil {
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Warn("Failed to enable WAL mode for SQLite", "error", err)
|
||||||
|
}
|
||||||
|
// Don't fail connection if WAL mode cannot be enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set busy timeout to handle locked database
|
||||||
|
_, err = db.ExecContext(ctx, "PRAGMA busy_timeout=5000")
|
||||||
|
if err != nil {
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Warn("Failed to set busy timeout for SQLite", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.db = db
|
||||||
|
p.config = cfg
|
||||||
|
|
||||||
|
if cfg.GetEnableLogging() {
|
||||||
|
logger.Info("SQLite connection established: name=%s, filepath=%s", cfg.GetName(), cfg.GetFilePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the SQLite connection
|
||||||
|
func (p *SQLiteProvider) Close() error {
|
||||||
|
if p.db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.db.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close SQLite connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.config.GetEnableLogging() {
|
||||||
|
logger.Info("SQLite connection closed: name=%s", p.config.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
p.db = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HealthCheck verifies the SQLite connection is alive
|
||||||
|
func (p *SQLiteProvider) HealthCheck(ctx context.Context) error {
|
||||||
|
if p.db == nil {
|
||||||
|
return fmt.Errorf("database connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a short timeout for health checks
|
||||||
|
healthCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Execute a simple query to verify the database is accessible
|
||||||
|
var result int
|
||||||
|
err := p.db.QueryRowContext(healthCtx, "SELECT 1").Scan(&result)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("health check failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != 1 {
|
||||||
|
return fmt.Errorf("health check returned unexpected result: %d", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNative returns the native *sql.DB connection
|
||||||
|
func (p *SQLiteProvider) GetNative() (*sql.DB, error) {
|
||||||
|
if p.db == nil {
|
||||||
|
return nil, fmt.Errorf("database connection is not initialized")
|
||||||
|
}
|
||||||
|
return p.db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMongo returns an error for SQLite (not a MongoDB connection)
|
||||||
|
func (p *SQLiteProvider) GetMongo() (*mongo.Client, error) {
|
||||||
|
return nil, ErrNotMongoDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns connection pool statistics
|
||||||
|
func (p *SQLiteProvider) Stats() *ConnectionStats {
|
||||||
|
if p.db == nil {
|
||||||
|
return &ConnectionStats{
|
||||||
|
Name: p.config.GetName(),
|
||||||
|
Type: "sqlite",
|
||||||
|
Connected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := p.db.Stats()
|
||||||
|
|
||||||
|
return &ConnectionStats{
|
||||||
|
Name: p.config.GetName(),
|
||||||
|
Type: "sqlite",
|
||||||
|
Connected: true,
|
||||||
|
OpenConnections: stats.OpenConnections,
|
||||||
|
InUse: stats.InUse,
|
||||||
|
Idle: stats.Idle,
|
||||||
|
WaitCount: stats.WaitCount,
|
||||||
|
WaitDuration: stats.WaitDuration,
|
||||||
|
MaxIdleClosed: stats.MaxIdleClosed,
|
||||||
|
MaxLifetimeClosed: stats.MaxLifetimeClosed,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user