feat(core): implement cordis configuration extension and migrate all plugins

This commit is contained in:
ryan
2026-08-29 10:53:53 +08:00
parent b43c429544
commit ed8491addf
74 changed files with 1354 additions and 1145 deletions
+15 -19
View File
@@ -5,7 +5,6 @@
package database
import (
"Wavelet/pkg/config"
"context"
"fmt"
"log"
@@ -34,35 +33,35 @@ var (
chDB *gorm.DB
)
func init() {
if !config.Config.ClickHouse.Enabled {
return
// InitClickHouseWithConfig initializes the ClickHouse connection using the provided configuration.
func InitClickHouseWithConfig(cfg ClickHouseConfig) error {
if !cfg.Enabled {
return nil
}
cfg := config.Config.ClickHouse
if cfg.Database == "" {
log.Fatalf("[ClickHouse] database name is required (expected: wavelet)\n")
return fmt.Errorf("[ClickHouse] database name is required (expected: wavelet)")
}
opts := buildClickHouseOptions()
opts := buildClickHouseOptions(cfg)
var err error
ChConn, err = clickhouse.Open(opts)
if err != nil {
log.Fatalf("[ClickHouse] init connection failed: %v\n", err)
return fmt.Errorf("[ClickHouse] init connection failed: %w", err)
}
if err = ChConn.Ping(context.Background()); err != nil {
log.Fatalf("[ClickHouse] ping failed: %v\n", err)
return fmt.Errorf("[ClickHouse] ping failed: %w", err)
}
chDB, err = gorm.Open(clickhouseDriver.New(clickhouseDriver.Config{
DSN: buildClickHouseDSN(),
DSN: buildClickHouseDSN(cfg),
}), &gorm.Config{
SkipDefaultTransaction: true,
})
if err != nil {
log.Fatalf("[ClickHouse] init gorm connection failed: %v\n", err)
return fmt.Errorf("[ClickHouse] init gorm connection failed: %w", err)
}
if err = chDB.Use(
@@ -74,12 +73,12 @@ func init() {
),
),
); err != nil {
log.Fatalf("[ClickHouse] init trace failed: %v\n", err)
return fmt.Errorf("[ClickHouse] init trace failed: %w", err)
}
sqlDB, err := chDB.DB()
if err != nil {
log.Fatalf("[ClickHouse] load sql db failed: %v\n", err)
return fmt.Errorf("[ClickHouse] load sql db failed: %w", err)
}
sqlDB.SetMaxIdleConns(cfg.MaxIdleConn)
@@ -87,11 +86,10 @@ func init() {
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
log.Println("[ClickHouse] connection established successfully")
return nil
}
func buildClickHouseOptions() *clickhouse.Options {
cfg := config.Config.ClickHouse
func buildClickHouseOptions(cfg ClickHouseConfig) *clickhouse.Options {
return &clickhouse.Options{
Addr: cfg.Hosts,
Auth: clickhouse.Auth{
@@ -114,9 +112,7 @@ func buildClickHouseOptions() *clickhouse.Options {
}
}
func buildClickHouseDSN() string {
cfg := config.Config.ClickHouse
func buildClickHouseDSN(cfg ClickHouseConfig) string {
chURL := &url.URL{
Scheme: "clickhouse",
Host: strings.Join(cfg.Hosts, ","),
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package database
import "time"
// ReplicaConfig declares a single read-replica configuration for PostgreSQL.
type ReplicaConfig struct {
Host string `config:"host"`
Port int `config:"port"`
Username string `config:"username"`
Password string `config:"password" secret:"true"`
}
// Config declares PostgreSQL and SQLite database configuration.
type Config struct {
Enabled bool `config:"enabled" env:"DB_ENABLED" default:"false" autoEnable:"DB_HOST"`
SQLitePath string `config:"sqlite_path" env:"SQLITE_PATH"`
Host string `config:"host" env:"DB_HOST"`
Port int `config:"port" env:"DB_PORT" default:"5432"`
Username string `config:"username" env:"DB_USERNAME"`
Password string `config:"password" env:"DB_PASSWORD" secret:"true"`
Database string `config:"database" env:"DB_NAME" default:"wavelet"`
MaxIdleConn int `config:"max_idle_conn" env:"DB_MAX_IDLE_CONN" default:"10"`
MaxOpenConn int `config:"max_open_conn" env:"DB_MAX_OPEN_CONN" default:"100"`
ConnMaxLifetime int `config:"conn_max_lifetime" env:"DB_CONN_MAX_LIFETIME" default:"3600"`
ConnMaxIdleTime int `config:"conn_max_idle_time" env:"DB_CONN_MAX_IDLE_TIME" default:"600"`
LogLevel string `config:"log_level" env:"DB_LOG_LEVEL" default:"warn"`
SSLMode string `config:"ssl_mode" env:"DB_SSL_MODE" default:"disable"`
TimeZone string `config:"time_zone" env:"DB_TIMEZONE" default:"UTC"`
ApplicationName string `config:"application_name" env:"DB_APPLICATION_NAME" default:"wavelet"`
SearchPath string `config:"search_path" env:"DB_SEARCH_PATH" default:"public"`
PreferSimpleProtocol bool `config:"prefer_simple_protocol" env:"DB_PREFER_SIMPLE_PROTOCOL"`
StatementCacheCapacity int `config:"statement_cache_capacity" env:"DB_STATEMENT_CACHE_CAPACITY"`
DefaultQueryExecMode string `config:"default_query_exec_mode" env:"DB_DEFAULT_QUERY_EXEC_MODE"`
Replicas []ReplicaConfig `config:"replicas"`
SlowThreshold time.Duration `config:"slow_threshold" env:"DB_SLOW_THRESHOLD" default:"200ms"`
}
// ClickHouseConfig declares the configuration for ClickHouse analytical storage.
type ClickHouseConfig struct {
Enabled bool `config:"enabled" env:"CLICKHOUSE_ENABLED" default:"false" autoEnable:"CLICKHOUSE_HOST"`
Hosts []string `config:"hosts" env:"CLICKHOUSE_HOST"`
Username string `config:"username" env:"CLICKHOUSE_USERNAME"`
Password string `config:"password" env:"CLICKHOUSE_PASSWORD" secret:"true"`
Database string `config:"database" env:"CLICKHOUSE_NAME" default:"wavelet"`
MaxIdleConn int `config:"max_idle_conn" env:"CLICKHOUSE_MAX_IDLE_CONN" default:"10"`
MaxOpenConn int `config:"max_open_conn" env:"CLICKHOUSE_MAX_OPEN_CONN" default:"50"`
ConnMaxLifetime int `config:"conn_max_lifetime" env:"CLICKHOUSE_CONN_MAX_LIFETIME" default:"3600"`
DialTimeout int `config:"dial_timeout" env:"CLICKHOUSE_DIAL_TIMEOUT" default:"10"`
BlockBufferSize uint8 `config:"block_buffer_size" env:"CLICKHOUSE_BLOCK_BUFFER_SIZE" default:"10"`
}
type appEnvConfig struct {
Env string `config:"env" env:"APP_ENV" default:"development"`
}
+29 -1
View File
@@ -56,17 +56,45 @@ func (p *Plugin) Name() string {
return "database"
}
// DeclareConfig declares database configuration keys.
func (p *Plugin) DeclareConfig() []core.ConfigBinding {
return []core.ConfigBinding{
{Prefix: "database", Target: &Config{}},
{Prefix: "clickhouse", Target: &ClickHouseConfig{}},
{Prefix: "app", Target: &appEnvConfig{}},
}
}
// Apply mounts the database service into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
var dbCfg Config
if err := ctx.Config().Bind("database", &dbCfg); err != nil {
return err
}
var chCfg ClickHouseConfig
if err := ctx.Config().Bind("clickhouse", &chCfg); err != nil {
return err
}
var appCfg appEnvConfig
_ = ctx.Config().Bind("app", &appCfg)
targetDB := p.db
if targetDB == nil {
var err error
targetDB, err = InitDB()
targetDB, err = InitDBWithConfig(dbCfg, appCfg.Env == "production" || appCfg.Env == "prod")
if err != nil {
return err
}
}
if chCfg.Enabled {
if err := InitClickHouseWithConfig(chCfg); err != nil {
return err
}
}
svc := &dbServiceImpl{
primary: targetDB,
namedDBs: p.namedDBs,
@@ -37,9 +37,11 @@ func TestDatabasePlugin(t *testing.T) {
database.WithDB(gdb),
database.WithNamedDB("analytics", namedDB),
)
assert.Equal(t, "database", p.Name())
ctx := core.NewContext(context.Background())
ctx.Config().SetSource(core.NewMapSource(map[string]any{
"database.enabled": false,
}))
require.NoError(t, ctx.Config().Resolve())
require.NoError(t, p.Apply(ctx))
svc, err := core.Inject[contracts.DBService](ctx)
+39 -40
View File
@@ -4,7 +4,6 @@
package database
import (
"Wavelet/pkg/config"
"context"
"fmt"
"log"
@@ -30,15 +29,19 @@ const sqliteDirMode = 0o750
// InitDB 初始化主数据库实例(支持 PostgreSQL / SQLite)
func InitDB() (*gorm.DB, error) {
if !config.Config.Database.Enabled {
return initSQLite()
}
return initPostgres()
return InitDBWithConfig(Config{}, false)
}
// initSQLite 初始化 SQLite 数据库(PostgreSQL 禁用时的后备方案)
func initSQLite() (*gorm.DB, error) {
sqlitePath := config.Config.Database.SQLitePath
// InitDBWithConfig initializes the main database with the provided config.
func InitDBWithConfig(cfg Config, isProd bool) (*gorm.DB, error) {
if !cfg.Enabled {
return initSQLiteWithConfig(cfg, isProd)
}
return initPostgresWithConfig(cfg, isProd)
}
func initSQLiteWithConfig(cfg Config, isProd bool) (*gorm.DB, error) {
sqlitePath := cfg.SQLitePath
if sqlitePath == "" {
sqlitePath = "./data/wavelet.db"
}
@@ -54,9 +57,9 @@ func initSQLite() (*gorm.DB, error) {
targetDB, err := gorm.Open(sqlite.Open(sqlitePath), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: &gormZapLogger{
logLevel: parseLogLevel(config.Config.Database.LogLevel),
slowThreshold: config.Config.Database.SlowThreshold,
ignoreRecordNotFoundError: config.Config.App.IsProduction(),
logLevel: parseLogLevel(cfg.LogLevel),
slowThreshold: cfg.SlowThreshold,
ignoreRecordNotFoundError: isProd,
},
})
if err != nil {
@@ -81,24 +84,21 @@ func initSQLite() (*gorm.DB, error) {
return targetDB, nil
}
// initPostgres 初始化 PostgreSQL 数据库
func initPostgres() (*gorm.DB, error) {
dbConfig := config.Config.Database
func initPostgresWithConfig(cfg Config, isProd bool) (*gorm.DB, error) {
// 构建主库 DSN 并连接
primaryDSN := buildDSN(dbConfig.Host, dbConfig.Port, dbConfig.Username, dbConfig.Password)
primaryDSN := buildDSN(cfg, cfg.Host, cfg.Port, cfg.Username, cfg.Password)
pgConfig := postgres.Config{
DSN: primaryDSN,
PreferSimpleProtocol: dbConfig.PreferSimpleProtocol,
PreferSimpleProtocol: cfg.PreferSimpleProtocol,
}
targetDB, err := gorm.Open(postgres.New(pgConfig), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
Logger: &gormZapLogger{
logLevel: parseLogLevel(config.Config.Database.LogLevel),
slowThreshold: config.Config.Database.SlowThreshold,
ignoreRecordNotFoundError: config.Config.App.IsProduction(),
logLevel: parseLogLevel(cfg.LogLevel),
slowThreshold: cfg.SlowThreshold,
ignoreRecordNotFoundError: isProd,
},
})
if err != nil {
@@ -110,9 +110,9 @@ func initPostgres() (*gorm.DB, error) {
tracing.NewPlugin(
tracing.WithoutMetrics(),
tracing.WithAttributes(
attribute.String("db.instance", dbConfig.Database),
attribute.String("db.ip", dbConfig.Host),
attribute.String("server.address", net.JoinHostPort(dbConfig.Host, strconv.Itoa(dbConfig.Port))),
attribute.String("db.instance", cfg.Database),
attribute.String("db.ip", cfg.Host),
attribute.String("server.address", net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port))),
attribute.String("db.system", "PostgreSQL"),
),
),
@@ -120,21 +120,21 @@ func initPostgres() (*gorm.DB, error) {
return nil, err
}
if len(dbConfig.Replicas) > 0 {
if len(cfg.Replicas) > 0 {
var replicaDialectors []gorm.Dialector
for _, replica := range dbConfig.Replicas {
for _, replica := range cfg.Replicas {
username := replica.Username
if username == "" {
username = dbConfig.Username
username = cfg.Username
}
password := replica.Password
if password == "" {
password = dbConfig.Password
password = cfg.Password
}
replicaDSN := buildDSN(replica.Host, replica.Port, username, password)
replicaDSN := buildDSN(cfg, replica.Host, replica.Port, username, password)
replicaDialectors = append(replicaDialectors, postgres.New(postgres.Config{
DSN: replicaDSN,
PreferSimpleProtocol: dbConfig.PreferSimpleProtocol,
PreferSimpleProtocol: cfg.PreferSimpleProtocol,
}))
}
@@ -143,15 +143,15 @@ func initPostgres() (*gorm.DB, error) {
Policy: dbresolver.RandomPolicy{},
})
resolver.SetMaxIdleConns(dbConfig.MaxIdleConn).
SetMaxOpenConns(dbConfig.MaxOpenConn).
SetConnMaxLifetime(time.Duration(dbConfig.ConnMaxLifetime) * time.Second).
SetConnMaxIdleTime(time.Duration(dbConfig.ConnMaxIdleTime) * time.Second)
resolver.SetMaxIdleConns(cfg.MaxIdleConn).
SetMaxOpenConns(cfg.MaxOpenConn).
SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second).
SetConnMaxIdleTime(time.Duration(cfg.ConnMaxIdleTime) * time.Second)
if err = targetDB.Use(resolver); err != nil {
return nil, err
}
log.Printf("[PostgreSQL] initialized in Primary-Replica mode (%d replicas)\n", len(dbConfig.Replicas))
log.Printf("[PostgreSQL] initialized in Primary-Replica mode (%d replicas)\n", len(cfg.Replicas))
} else {
log.Println("[PostgreSQL] initialized in Standalone mode")
}
@@ -162,18 +162,17 @@ func initPostgres() (*gorm.DB, error) {
return nil, err
}
sqlDB.SetMaxIdleConns(dbConfig.MaxIdleConn)
sqlDB.SetMaxOpenConns(dbConfig.MaxOpenConn)
sqlDB.SetConnMaxLifetime(time.Duration(dbConfig.ConnMaxLifetime) * time.Second)
sqlDB.SetConnMaxIdleTime(time.Duration(dbConfig.ConnMaxIdleTime) * time.Second)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConn)
sqlDB.SetMaxOpenConns(cfg.MaxOpenConn)
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
sqlDB.SetConnMaxIdleTime(time.Duration(cfg.ConnMaxIdleTime) * time.Second)
db = targetDB
return targetDB, nil
}
// buildDSN 构建 PostgreSQL DSN
func buildDSN(host string, port int, username, password string) string {
cfg := config.Config.Database
func buildDSN(cfg Config, host string, port int, username, password string) string {
pqURL := &url.URL{
Scheme: "postgres",
Host: net.JoinHostPort(host, strconv.Itoa(port)),