package config import ( "fmt" "os" "strconv" "strings" "time" ) const defaultEnvPrefix = "GO_OMNI_" // LoadFromEnv loads application settings from GO_OMNI_* variables. The // database settings remain owned by database.ConfigFromEnv. func LoadFromEnv(prefix string) (Config, error) { return applyEnv(Default(), prefix) } // ApplyEnv overlays nonempty environment values on a caller's defaults or file // settings, then validates the result. Caller-owned slices are not retained. func ApplyEnv(defaults Config, prefix string) (Config, error) { defaults.Errors = defaults.Errors.Clone() defaults.TrustedProxies = append([]string(nil), defaults.TrustedProxies...) defaults.Log.RedactKeys = append([]string(nil), defaults.Log.RedactKeys...) return applyEnv(defaults, prefix) } func applyEnv(cfg Config, prefix string) (Config, error) { prefix = strings.TrimSpace(prefix) if prefix == "" { prefix = defaultEnvPrefix } cfg.Address = envString(prefix+"HTTP_ADDR", cfg.Address) cfg.Transport = envString(prefix+"TRANSPORT", cfg.Transport) cfg.TLS.CertFile = envString(prefix+"TLS_CERT_FILE", cfg.TLS.CertFile) cfg.TLS.KeyFile = envString(prefix+"TLS_KEY_FILE", cfg.TLS.KeyFile) cfg.RequestIDHeader = envString(prefix+"REQUEST_ID_HEADER", cfg.RequestIDHeader) if value := strings.TrimSpace(os.Getenv(prefix + "TRUSTED_PROXIES")); value != "" { cfg.TrustedProxies = strings.Split(value, ",") for i := range cfg.TrustedProxies { cfg.TrustedProxies[i] = strings.TrimSpace(cfg.TrustedProxies[i]) } } cfg.Log.Level = envString(prefix+"LOG_LEVEL", cfg.Log.Level) cfg.Log.Format = envString(prefix+"LOG_FORMAT", cfg.Log.Format) cfg.Log.Output = envString(prefix+"LOG_OUTPUT", cfg.Log.Output) cfg.Log.File = envString(prefix+"LOG_FILE", cfg.Log.File) cfg.Log.Service = envString(prefix+"SERVICE", cfg.Log.Service) cfg.Log.Environment = envString(prefix+"ENVIRONMENT", cfg.Log.Environment) var err error for _, field := range []struct { name string value *time.Duration }{ {"HTTP_READ_HEADER_TIMEOUT", &cfg.HTTP.ReadHeaderTimeout}, {"HTTP_READ_TIMEOUT", &cfg.HTTP.ReadTimeout}, {"HTTP_WRITE_TIMEOUT", &cfg.HTTP.WriteTimeout}, {"HTTP_IDLE_TIMEOUT", &cfg.HTTP.IdleTimeout}, } { if value := strings.TrimSpace(os.Getenv(prefix + field.name)); value != "" { *field.value, err = time.ParseDuration(value) if err != nil { return Config{}, fmt.Errorf("%s%s must be a duration: %w", prefix, field.name, err) } } } if value := strings.TrimSpace(os.Getenv(prefix + "HTTP_MAX_HEADER_BYTES")); value != "" { cfg.HTTP.MaxHeaderBytes, err = strconv.Atoi(value) if err != nil { return Config{}, fmt.Errorf("%sHTTP_MAX_HEADER_BYTES must be an integer: %w", prefix, err) } } if cfg.EnableLogger, err = envBool(prefix+"ENABLE_LOGGER", cfg.EnableLogger); err != nil { return Config{}, err } if cfg.Log.AddSource, err = envBool(prefix+"LOG_ADD_SOURCE", cfg.Log.AddSource); err != nil { return Config{}, err } if value := strings.TrimSpace(os.Getenv(prefix + "SLOW_REQUEST_THRESHOLD")); value != "" { cfg.SlowRequestThreshold, err = time.ParseDuration(value) if err != nil { return Config{}, fmt.Errorf("%sSLOW_REQUEST_THRESHOLD must be a duration: %w", prefix, err) } } if value := strings.TrimSpace(os.Getenv(prefix + "SHUTDOWN_TIMEOUT")); value != "" { cfg.ShutdownTimeout, err = time.ParseDuration(value) if err != nil { return Config{}, fmt.Errorf("%sSHUTDOWN_TIMEOUT must be a duration: %w", prefix, err) } } if err := cfg.Validate(); err != nil { return Config{}, err } return cfg, nil } func envString(key, fallback string) string { if value := strings.TrimSpace(os.Getenv(key)); value != "" { return value } return fallback } func envBool(key string, fallback bool) (bool, error) { value := strings.TrimSpace(os.Getenv(key)) if value == "" { return fallback, nil } parsed, err := strconv.ParseBool(value) if err != nil { return false, fmt.Errorf("%s must be a boolean: %w", key, err) } return parsed, nil }