Files
replica/replica/config.go
2026-02-12 20:33:18 +01:00

272 lines
7.1 KiB
Go

package replica
import (
"os"
"strconv"
"strings"
"time"
"github.com/joho/godotenv"
)
// BinlogConfig holds the configuration for connecting to MySQL/MariaDB binlog
type BinlogConfig struct {
Host string
Port uint16
User string
Password string
ServerID uint32
Name string // Instance name for logging
}
// SecondaryConfig holds the configuration for a secondary database
type SecondaryConfig struct {
Name string // Friendly name for logging
Host string
Port uint16
User string
Password string
DSN string // Pre-built DSN for convenience
}
// GraylogConfig holds the configuration for Graylog integration
type GraylogConfig struct {
Enabled bool
Endpoint string
Protocol string
Timeout time.Duration
Source string
ExtraFields map[string]interface{}
}
// AppConfig holds the complete application configuration
type AppConfig struct {
// Primary configuration
Primary BinlogConfig
// Secondary configurations
Secondaries []SecondaryConfig
// Transfer settings
BatchSize int
WorkerCount int
ExcludeSchemas []string
// Graylog configuration
Graylog GraylogConfig
}
// LoadEnvConfig loads configuration from environment variables
func LoadEnvConfig() (*AppConfig, error) {
// Load .env file
if err := godotenv.Load(); err != nil {
// It's not an error if the .env file doesn't exist
// We just use the system environment variables
}
cfg := &AppConfig{
BatchSize: 10000, // Increase default batch size from 1000 to 10000 for better performance
WorkerCount: 4, // Default to 4 workers for parallel processing
ExcludeSchemas: []string{"information_schema", "performance_schema", "mysql", "sys"},
}
// Primary configuration
cfg.Primary.Host = getEnv("MARIA_PRIMARY_HOST", "localhost")
cfg.Primary.Port = uint16(getEnvInt("MARIA_PRIMARY_PORT", 3306))
cfg.Primary.User = getEnv("MARIA_USER", "replica")
cfg.Primary.Password = getEnv("MARIA_PASS", "replica")
cfg.Primary.ServerID = uint32(getEnvInt("MARIA_SERVER_ID", 100))
cfg.Primary.Name = getEnv("MARIA_PRIMARY_NAME", "mariadb-primary")
// Parse secondary hosts (comma-separated)
secondaryHosts := getEnv("MARIA_SECONDARY_HOSTS", "")
if secondaryHosts == "" {
// Fallback to single secondary for backward compatibility
secondaryHosts = getEnv("MARIA_SECONDARY_HOST", "localhost")
}
// Parse secondary ports (comma-separated, must match number of hosts or be single value)
secondaryPortsStr := getEnv("MARIA_SECONDARY_PORTS", "")
secondaryPorts := parseUint16List(secondaryPortsStr, 3307)
// Parse secondary users (comma-separated, optional)
secondaryUsers := getEnv("MARIA_SECONDARY_USERS", "")
users := parseStringList(secondaryUsers, cfg.Primary.User)
// Parse secondary passwords (comma-separated, optional)
secondaryPasswords := getEnv("MARIA_SECONDARY_PASSWORDS", "")
passwords := parseStringList(secondaryPasswords, cfg.Primary.Password)
// Parse secondary names (comma-separated, optional)
secondaryNames := getEnv("MARIA_SECONDARY_NAMES", "")
hosts := parseStringList(secondaryHosts, "")
names := parseStringList(secondaryNames, "")
portMap := makePortsMap(hosts, secondaryPorts, 3307)
userMap := makeStringMap(hosts, users, cfg.Primary.User)
passMap := makeStringMap(hosts, passwords, cfg.Primary.Password)
// Build secondary configurations
for i, host := range hosts {
name := strconv.Itoa(i + 1)
if i < len(names) && names[i] != "" {
name = names[i]
}
port := uint16(3307)
if p, ok := portMap[host]; ok {
port = p
}
user := cfg.Primary.User
if u, ok := userMap[host]; ok {
user = u
}
pass := cfg.Primary.Password
if p, ok := passMap[host]; ok {
pass = p
}
dsn := buildDSN(user, pass, host, int(port), "replica")
cfg.Secondaries = append(cfg.Secondaries, SecondaryConfig{
Name: name,
Host: host,
Port: port,
User: user,
Password: pass,
DSN: dsn,
})
}
// Batch size override
if batchSize := getEnvInt("TRANSFER_BATCH_SIZE", 0); batchSize > 0 {
cfg.BatchSize = batchSize
}
// Worker count override
if workerCount := getEnvInt("TRANSFER_WORKER_COUNT", 0); workerCount > 0 {
cfg.WorkerCount = workerCount
}
// Graylog configuration
cfg.Graylog.Enabled = getEnvBool("GRAYLOG_ENABLED", false)
cfg.Graylog.Endpoint = getEnv("GRAYLOG_ENDPOINT", "localhost:12201")
cfg.Graylog.Protocol = getEnv("GRAYLOG_PROTOCOL", "udp")
cfg.Graylog.Source = getEnv("GRAYLOG_SOURCE", "binlog-sync")
// Parse timeout
if timeout := getEnv("GRAYLOG_TIMEOUT", "5s"); timeout != "" {
d, err := time.ParseDuration(timeout)
if err == nil {
cfg.Graylog.Timeout = d
} else {
cfg.Graylog.Timeout = 5 * time.Second
}
}
return cfg, nil
}
// getEnv gets an environment variable with a default value
func getEnv(key, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}
// getEnvInt gets an environment variable as an integer with a default value
func getEnvInt(key string, defaultValue int) int {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
i, err := strconv.Atoi(value)
if err != nil {
return defaultValue
}
return i
}
// getEnvBool gets an environment variable as a boolean with a default value
func getEnvBool(key string, defaultValue bool) bool {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
b, err := strconv.ParseBool(value)
if err != nil {
return defaultValue
}
return b
}
// parseStringList parses a comma-separated string into a list
func parseStringList(s string, defaultValue string) []string {
if strings.TrimSpace(s) == "" {
if defaultValue == "" {
return nil
}
return []string{defaultValue}
}
parts := strings.Split(s, ",")
result := make([]string, len(parts))
for i, p := range parts {
result[i] = strings.TrimSpace(p)
}
return result
}
// parseUint16List parses a comma-separated string into a list of uint16
func parseUint16List(s string, defaultValue uint16) []uint16 {
if strings.TrimSpace(s) == "" {
return []uint16{defaultValue}
}
parts := strings.Split(s, ",")
result := make([]uint16, len(parts))
for i, p := range parts {
p = strings.TrimSpace(p)
val, err := strconv.ParseUint(p, 10, 16)
if err != nil {
result[i] = defaultValue
} else {
result[i] = uint16(val)
}
}
return result
}
// makePortsMap creates a map of host to port
func makePortsMap(hosts []string, ports []uint16, defaultPort uint16) map[string]uint16 {
result := make(map[string]uint16)
for i, host := range hosts {
if i < len(ports) {
result[host] = ports[i]
} else {
result[host] = defaultPort
}
}
return result
}
// makeStringMap creates a map of host to string value
func makeStringMap(hosts []string, values []string, defaultValue string) map[string]string {
result := make(map[string]string)
for i, host := range hosts {
if i < len(values) && values[i] != "" {
result[host] = values[i]
} else {
result[host] = defaultValue
}
}
return result
}
// buildDSN builds a MySQL DSN string
func buildDSN(user, password, host string, port int, database string) string {
return user + ":" + password + "@tcp(" + host + ":" + strconv.Itoa(port) + ")/" + database + "?multiStatements=true"
}