272 lines
7.1 KiB
Go
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"
|
|
}
|