package config import ( "fmt" "os" "strconv" "strings" "time" "galaxy/postgres" "galaxy/redisconn" ) // LoadFromEnv builds Config from environment variables and validates the // resulting configuration. func LoadFromEnv() (Config, error) { cfg := DefaultConfig() var err error cfg.ShutdownTimeout, err = durationEnv(shutdownTimeoutEnvVar, cfg.ShutdownTimeout) if err != nil { return Config{}, err } cfg.Logging.Level = stringEnv(logLevelEnvVar, cfg.Logging.Level) cfg.PublicHTTP.Addr = stringEnv(publicHTTPAddrEnvVar, cfg.PublicHTTP.Addr) cfg.PublicHTTP.ReadHeaderTimeout, err = durationEnv(publicHTTPReadHeaderTimeoutEnvVar, cfg.PublicHTTP.ReadHeaderTimeout) if err != nil { return Config{}, err } cfg.PublicHTTP.ReadTimeout, err = durationEnv(publicHTTPReadTimeoutEnvVar, cfg.PublicHTTP.ReadTimeout) if err != nil { return Config{}, err } cfg.PublicHTTP.IdleTimeout, err = durationEnv(publicHTTPIdleTimeoutEnvVar, cfg.PublicHTTP.IdleTimeout) if err != nil { return Config{}, err } cfg.InternalHTTP.Addr = stringEnv(internalHTTPAddrEnvVar, cfg.InternalHTTP.Addr) cfg.InternalHTTP.ReadHeaderTimeout, err = durationEnv(internalHTTPReadHeaderTimeoutEnvVar, cfg.InternalHTTP.ReadHeaderTimeout) if err != nil { return Config{}, err } cfg.InternalHTTP.ReadTimeout, err = durationEnv(internalHTTPReadTimeoutEnvVar, cfg.InternalHTTP.ReadTimeout) if err != nil { return Config{}, err } cfg.InternalHTTP.IdleTimeout, err = durationEnv(internalHTTPIdleTimeoutEnvVar, cfg.InternalHTTP.IdleTimeout) if err != nil { return Config{}, err } redisConn, err := redisconn.LoadFromEnv(envPrefix) if err != nil { return Config{}, err } cfg.Redis.Conn = redisConn pgConn, err := postgres.LoadFromEnv(envPrefix) if err != nil { return Config{}, err } cfg.Postgres.Conn = pgConn cfg.Redis.GMEventsStream = stringEnv(gmEventsStreamEnvVar, cfg.Redis.GMEventsStream) cfg.Redis.GMEventsReadBlockTimeout, err = durationEnv(gmEventsReadBlockTimeoutEnvVar, cfg.Redis.GMEventsReadBlockTimeout) if err != nil { return Config{}, err } cfg.Redis.RuntimeStartJobsStream = stringEnv(runtimeStartJobsStreamEnvVar, cfg.Redis.RuntimeStartJobsStream) cfg.Redis.RuntimeStopJobsStream = stringEnv(runtimeStopJobsStreamEnvVar, cfg.Redis.RuntimeStopJobsStream) cfg.Redis.RuntimeJobResultsStream = stringEnv(runtimeJobResultsStreamEnvVar, cfg.Redis.RuntimeJobResultsStream) cfg.Redis.RuntimeJobResultsReadBlockTimeout, err = durationEnv(runtimeJobResultsReadBlockTimeoutEnv, cfg.Redis.RuntimeJobResultsReadBlockTimeout) if err != nil { return Config{}, err } cfg.Redis.NotificationIntentsStream = stringEnv(notificationIntentsStreamEnvVar, cfg.Redis.NotificationIntentsStream) cfg.Redis.UserLifecycleStream = stringEnv(userLifecycleStreamEnvVar, cfg.Redis.UserLifecycleStream) cfg.Redis.UserLifecycleReadBlockTimeout, err = durationEnv(userLifecycleReadBlockTimeoutEnvVar, cfg.Redis.UserLifecycleReadBlockTimeout) if err != nil { return Config{}, err } cfg.UserService.BaseURL = stringEnv(userServiceBaseURLEnvVar, cfg.UserService.BaseURL) cfg.UserService.Timeout, err = durationEnv(userServiceTimeoutEnvVar, cfg.UserService.Timeout) if err != nil { return Config{}, err } cfg.GM.BaseURL = stringEnv(gmBaseURLEnvVar, cfg.GM.BaseURL) cfg.GM.Timeout, err = durationEnv(gmTimeoutEnvVar, cfg.GM.Timeout) if err != nil { return Config{}, err } cfg.EnrollmentAutomation.Interval, err = durationEnv(enrollmentAutomationIntervalEnvVar, cfg.EnrollmentAutomation.Interval) if err != nil { return Config{}, err } cfg.RaceNameDirectory.Backend = stringEnv(raceNameDirectoryBackendEnvVar, cfg.RaceNameDirectory.Backend) cfg.PendingRegistration.Interval, err = durationEnv(raceNameExpirationIntervalEnvVar, cfg.PendingRegistration.Interval) if err != nil { return Config{}, err } cfg.Telemetry.ServiceName = stringEnv(otelServiceNameEnvVar, cfg.Telemetry.ServiceName) cfg.Telemetry.TracesExporter = normalizeExporterValue(stringEnv(otelTracesExporterEnvVar, cfg.Telemetry.TracesExporter)) cfg.Telemetry.MetricsExporter = normalizeExporterValue(stringEnv(otelMetricsExporterEnvVar, cfg.Telemetry.MetricsExporter)) cfg.Telemetry.TracesProtocol = normalizeProtocolValue( os.Getenv(otelExporterOTLPTracesProtocolEnvVar), os.Getenv(otelExporterOTLPProtocolEnvVar), cfg.Telemetry.TracesProtocol, ) cfg.Telemetry.MetricsProtocol = normalizeProtocolValue( os.Getenv(otelExporterOTLPMetricsProtocolEnvVar), os.Getenv(otelExporterOTLPProtocolEnvVar), cfg.Telemetry.MetricsProtocol, ) cfg.Telemetry.StdoutTracesEnabled, err = boolEnv(otelStdoutTracesEnabledEnvVar, cfg.Telemetry.StdoutTracesEnabled) if err != nil { return Config{}, err } cfg.Telemetry.StdoutMetricsEnabled, err = boolEnv(otelStdoutMetricsEnabledEnvVar, cfg.Telemetry.StdoutMetricsEnabled) if err != nil { return Config{}, err } if err := cfg.Validate(); err != nil { return Config{}, err } return cfg, nil } func stringEnv(name string, fallback string) string { value, ok := os.LookupEnv(name) if !ok { return fallback } return strings.TrimSpace(value) } func durationEnv(name string, fallback time.Duration) (time.Duration, error) { value, ok := os.LookupEnv(name) if !ok { return fallback, nil } parsed, err := time.ParseDuration(strings.TrimSpace(value)) if err != nil { return 0, fmt.Errorf("%s: parse duration: %w", name, err) } return parsed, nil } func intEnv(name string, fallback int) (int, error) { value, ok := os.LookupEnv(name) if !ok { return fallback, nil } parsed, err := strconv.Atoi(strings.TrimSpace(value)) if err != nil { return 0, fmt.Errorf("%s: parse int: %w", name, err) } return parsed, nil } func boolEnv(name string, fallback bool) (bool, error) { value, ok := os.LookupEnv(name) if !ok { return fallback, nil } parsed, err := strconv.ParseBool(strings.TrimSpace(value)) if err != nil { return false, fmt.Errorf("%s: parse bool: %w", name, err) } return parsed, nil } func normalizeExporterValue(value string) string { trimmed := strings.TrimSpace(value) switch trimmed { case "", "none": return "none" default: return trimmed } } func normalizeProtocolValue(primary string, fallback string, defaultValue string) string { primary = strings.TrimSpace(primary) if primary != "" { return primary } fallback = strings.TrimSpace(fallback) if fallback != "" { return fallback } return strings.TrimSpace(defaultValue) }