Refactor Configuration System to Use Environment Variables
- Removed YAML configuration files for the scraper and web commands, transitioning to an environment-based configuration system. - Updated configuration structs to utilize `env` tags for loading values from `.env` files, enhancing security and flexibility. - Implemented reflection-based parsing for environment variables, ensuring type-safe configuration loading. - Revised documentation to reflect the new configuration approach, including usage examples and best practices for environment variable naming conventions.
This commit is contained in:
+177
-41
@@ -1,79 +1,215 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/subosito/gotenv"
|
||||
)
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
ReadTimeout time.Duration `mapstructure:"read_timeout"`
|
||||
WriteTimeout time.Duration `mapstructure:"write_timeout"`
|
||||
Host string `env:"SERVER_HOST"`
|
||||
Port int `env:"SERVER_PORT"`
|
||||
Timeout time.Duration `env:"SERVER_TIMEOUT"`
|
||||
ReadTimeout time.Duration `env:"SERVER_READ_TIMEOUT"`
|
||||
WriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
MongoDB MongoConfig `mapstructure:"mongodb"`
|
||||
MongoDB MongoConfig
|
||||
}
|
||||
|
||||
type MongoConfig struct {
|
||||
URI string `mapstructure:"uri"`
|
||||
Name string `mapstructure:"name"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
MaxPoolSize int `mapstructure:"max_pool_size"`
|
||||
URI string `env:"MONGODB_URI"`
|
||||
Name string `env:"MONGODB_NAME"`
|
||||
Timeout time.Duration `env:"MONGODB_TIMEOUT"`
|
||||
MaxPoolSize int `env:"MONGODB_MAX_POOL_SIZE"`
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
Redis RedisConfig
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
PoolSize int `mapstructure:"pool_size"`
|
||||
Host string `env:"REDIS_HOST"`
|
||||
Port int `env:"REDIS_PORT"`
|
||||
Password string `env:"REDIS_PASSWORD"`
|
||||
DB int `env:"REDIS_DB"`
|
||||
PoolSize int `env:"REDIS_POOL_SIZE"`
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
File LogFileConfig `mapstructure:"file"`
|
||||
Level string `env:"LOG_LEVEL"`
|
||||
Format string `env:"LOG_FORMAT"`
|
||||
Output string `env:"LOG_OUTPUT"`
|
||||
File LogFileConfig
|
||||
}
|
||||
|
||||
type LogFileConfig struct {
|
||||
Path string `mapstructure:"path"`
|
||||
MaxSize int `mapstructure:"max_size"` // MB
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"` // days
|
||||
Compress bool `mapstructure:"compress"`
|
||||
Path string `env:"LOG_FILE_PATH"`
|
||||
MaxSize int `env:"LOG_FILE_MAX_SIZE"`
|
||||
MaxBackups int `env:"LOG_FILE_MAX_BACKUPS"`
|
||||
MaxAge int `env:"LOG_FILE_MAX_AGE"`
|
||||
Compress bool `env:"LOG_FILE_COMPRESS"`
|
||||
}
|
||||
|
||||
type AssetsConfig struct {
|
||||
FlagsPath string `env:"ASSETS_FLAGS_PATH"`
|
||||
}
|
||||
|
||||
type RateLimitConfig struct {
|
||||
RequestsPerMinute int `mapstructure:"requests_per_minute"`
|
||||
Burst int `mapstructure:"burst"`
|
||||
RequestsPerMinute int `env:"RATE_LIMIT_REQUESTS_PER_MINUTE"`
|
||||
Burst int `env:"RATE_LIMIT_BURST"`
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from file and environment variables
|
||||
// LoadConfig loads configuration from .env file
|
||||
func LoadConfig[T any](path string, config T) (T, error) {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(path)
|
||||
|
||||
// Enable reading from environment variables
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// Read configuration file
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return config, err
|
||||
// Load .env file
|
||||
envPath := fmt.Sprintf("%s/.env", path)
|
||||
if err := gotenv.Load(envPath); err != nil {
|
||||
return config, fmt.Errorf("failed to load .env file from %s: %w", envPath, err)
|
||||
}
|
||||
|
||||
if err := viper.Unmarshal(config); err != nil {
|
||||
return config, err
|
||||
// Parse configuration using reflection
|
||||
if err := parseConfig(config); err != nil {
|
||||
return config, fmt.Errorf("failed to parse configuration: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// parseConfig uses reflection to parse environment variables into the config struct
|
||||
func parseConfig(config interface{}) error {
|
||||
v := reflect.ValueOf(config)
|
||||
if v.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("config must be a pointer")
|
||||
}
|
||||
|
||||
v = v.Elem()
|
||||
if v.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("config must be a struct")
|
||||
}
|
||||
|
||||
return parseStruct(v)
|
||||
}
|
||||
|
||||
// parseStruct recursively parses struct fields
|
||||
func parseStruct(v reflect.Value) error {
|
||||
t := v.Type()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
fieldType := t.Field(i)
|
||||
|
||||
// Skip unexported fields
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get env tag
|
||||
envTag := fieldType.Tag.Get("env")
|
||||
if envTag == "" {
|
||||
// If no env tag, try to parse nested struct
|
||||
if field.Kind() == reflect.Struct {
|
||||
if err := parseStruct(field); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Get environment variable value
|
||||
envValue := os.Getenv(envTag)
|
||||
if envValue == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse and set the field value
|
||||
if err := setFieldValue(field, envValue); err != nil {
|
||||
return fmt.Errorf("failed to set field %s: %w", fieldType.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setFieldValue sets the field value from environment variable string
|
||||
func setFieldValue(field reflect.Value, value string) error {
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
field.SetString(value)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if field.Type() == reflect.TypeOf(time.Duration(0)) {
|
||||
// Handle time.Duration
|
||||
duration, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid duration format: %s", value)
|
||||
}
|
||||
field.Set(reflect.ValueOf(duration))
|
||||
} else {
|
||||
// Handle regular integers
|
||||
intValue, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid integer format: %s", value)
|
||||
}
|
||||
field.SetInt(intValue)
|
||||
}
|
||||
case reflect.Bool:
|
||||
boolValue, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid boolean format: %s", value)
|
||||
}
|
||||
field.SetBool(boolValue)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
floatValue, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid float format: %s", value)
|
||||
}
|
||||
field.SetFloat(floatValue)
|
||||
default:
|
||||
return fmt.Errorf("unsupported field type: %s", field.Kind())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to get environment variable with default value
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Helper function to get environment variable as int with default value
|
||||
func getEnvAsInt(key string, defaultValue int) int {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Helper function to get environment variable as duration with default value
|
||||
func getEnvAsDuration(key string, defaultValue time.Duration) time.Duration {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if duration, err := time.ParseDuration(value); err == nil {
|
||||
return duration
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Helper function to get environment variable as bool with default value
|
||||
func getEnvAsBool(key string, defaultValue bool) bool {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||
return boolValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user