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 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: 1000, 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 } // 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" }