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:
n.nakhostin
2025-09-09 18:19:42 +03:30
parent 44d0f57082
commit 57c29dc58f
6 changed files with 348 additions and 284 deletions
+177 -41
View File
@@ -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
}