Refactor configuration management by migrating to a modular config system
- Removed the old `infra.Config` structure and replaced it with a new modular configuration system in `pkg/config`. - Introduced a `Config` struct in `cmd/web/config.go` to hold command-specific configurations. - Updated configuration loading functions to utilize the new `LoadConfig` method with generics for type safety. - Adjusted various initialization functions in `cmd/web/bootstrap.go` and `cmd/web/main.go` to reflect the new configuration structure. - Cleaned up the `config.yaml` by removing obsolete fields and ensuring it aligns with the new configuration schema.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
# Modular Configuration System
|
||||
|
||||
This package provides a modular configuration system for the Tender Management backend. It allows each command (web, scraper, worker, etc.) to have its own specific configuration while sharing common base configurations.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Base Configuration (`BaseConfig`)
|
||||
|
||||
The `BaseConfig` struct contains common configuration fields that are used across all commands:
|
||||
|
||||
- **Server**: HTTP server configuration (host, port, timeouts)
|
||||
- **Database**: MongoDB connection configuration
|
||||
- **Cache**: Redis connection configuration
|
||||
- **Logging**: Logging system configuration
|
||||
|
||||
### Command-Specific Configuration
|
||||
|
||||
Each command can extend the `BaseConfig` by embedding it and adding its own specific configuration fields.
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Create Command-Specific Config
|
||||
|
||||
Create a config file for your command (e.g., `cmd/yourcommand/config.go`):
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
"tm/pkg/config"
|
||||
)
|
||||
|
||||
// Config holds configuration for your command
|
||||
type Config struct {
|
||||
config.BaseConfig
|
||||
YourSpecific YourSpecificConfig `mapstructure:"your_specific"`
|
||||
}
|
||||
|
||||
type YourSpecificConfig struct {
|
||||
SomeField string `mapstructure:"some_field"`
|
||||
SomeTimeout time.Duration `mapstructure:"some_timeout"`
|
||||
SomeNumber int `mapstructure:"some_number"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Load Configuration
|
||||
|
||||
In your command's `main.go` or bootstrap file:
|
||||
|
||||
```go
|
||||
func initConfig() Config {
|
||||
conf, err := config.LoadConfig(".", &Config{})
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to load config: %v", err))
|
||||
}
|
||||
|
||||
return *conf
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create YAML Configuration File
|
||||
|
||||
Create a `config.yaml` file in your command directory:
|
||||
|
||||
```yaml
|
||||
# Base configuration (common to all commands)
|
||||
server:
|
||||
host: "localhost"
|
||||
port: 8080
|
||||
timeout: 30s
|
||||
read_timeout: 10s
|
||||
write_timeout: 10s
|
||||
|
||||
database:
|
||||
mongodb:
|
||||
uri: "mongodb://localhost:27017"
|
||||
name: "your_database"
|
||||
timeout: 10s
|
||||
max_pool_size: 100
|
||||
|
||||
cache:
|
||||
redis:
|
||||
host: "localhost"
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
pool_size: 10
|
||||
|
||||
logging:
|
||||
level: "info"
|
||||
format: "json"
|
||||
output: "stdout"
|
||||
file:
|
||||
path: "/var/log/tm/your-command.log"
|
||||
max_size: 100
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
# Your command-specific configuration
|
||||
your_specific:
|
||||
some_field: "value"
|
||||
some_timeout: 30s
|
||||
some_number: 42
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Web Command Config
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
config.BaseConfig
|
||||
Queue QueueConfig `mapstructure:"queue"`
|
||||
UserAuthorization AuthConfig `mapstructure:"user_authorization"`
|
||||
CustomerAuthorization AuthConfig `mapstructure:"customer_authorization"`
|
||||
AI AIConfig `mapstructure:"ai"`
|
||||
RateLimit RateLimitConfig `mapstructure:"rate_limiting"`
|
||||
}
|
||||
```
|
||||
|
||||
### Scraper Command Config
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
config.BaseConfig
|
||||
TED TEDConfig `mapstructure:"ted"`
|
||||
}
|
||||
|
||||
type TEDConfig struct {
|
||||
BaseURL string `mapstructure:"base_url"`
|
||||
MaxRetries int `mapstructure:"max_retries"`
|
||||
MaxConcurrency int `mapstructure:"max_concurrency"`
|
||||
DownloadDir string `mapstructure:"download_dir"`
|
||||
CleanupAfter time.Duration `mapstructure:"cleanup_after"`
|
||||
}
|
||||
```
|
||||
|
||||
### Worker Command Config
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
config.BaseConfig
|
||||
Queue QueueConfig `mapstructure:"queue"`
|
||||
AI AIConfig `mapstructure:"ai"`
|
||||
Worker WorkerConfig `mapstructure:"worker"`
|
||||
Scraping ScrapingConfig `mapstructure:"scraping"`
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Type Safety
|
||||
|
||||
The configuration loader uses Go generics to provide type-safe configuration loading. The function signature ensures that you get back exactly the type you expect.
|
||||
|
||||
### Environment Variable Support
|
||||
|
||||
The configuration system automatically supports environment variable overrides via Viper's `AutomaticEnv()` feature.
|
||||
|
||||
### YAML Support
|
||||
|
||||
Configuration files use YAML format for easy readability and maintenance.
|
||||
|
||||
### Validation
|
||||
|
||||
Each command can implement its own validation logic after loading the configuration.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep BaseConfig minimal**: Only add fields to `BaseConfig` that are truly common across all commands.
|
||||
|
||||
2. **Use descriptive names**: Choose clear, descriptive names for your configuration structs and fields.
|
||||
|
||||
3. **Document your config**: Add comments to your configuration structs explaining what each field does.
|
||||
|
||||
4. **Use appropriate types**: Use `time.Duration` for durations, proper numeric types for numbers, etc.
|
||||
|
||||
5. **Provide defaults**: Consider providing sensible defaults in your YAML files.
|
||||
|
||||
6. **Environment variables**: Use environment variables for sensitive data like API keys and passwords.
|
||||
|
||||
## Migration from Old System
|
||||
|
||||
If you're migrating from the old `infra.Config` system:
|
||||
|
||||
1. Remove imports of `tm/infra`
|
||||
2. Add imports of `tm/pkg/config`
|
||||
3. Change your config struct to embed `config.BaseConfig`
|
||||
4. Update your `LoadConfig` call to use the new generic function
|
||||
5. Update function signatures to use the new config types
|
||||
|
||||
Example migration:
|
||||
|
||||
```go
|
||||
// Old way
|
||||
func initConfig() infra.Config {
|
||||
config, err := infra.LoadConfig(".")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to load config: %v", err))
|
||||
}
|
||||
return *config
|
||||
}
|
||||
|
||||
// New way
|
||||
func initConfig() Config {
|
||||
conf, err := config.LoadConfig(".", &Config{})
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to load config: %v", err))
|
||||
}
|
||||
return *conf
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,79 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
MongoDB MongoConfig `mapstructure:"mongodb"`
|
||||
}
|
||||
|
||||
type MongoConfig struct {
|
||||
URI string `mapstructure:"uri"`
|
||||
Name string `mapstructure:"name"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
MaxPoolSize int `mapstructure:"max_pool_size"`
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
PoolSize int `mapstructure:"pool_size"`
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
Output string `mapstructure:"output"`
|
||||
File LogFileConfig `mapstructure:"file"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type RateLimitConfig struct {
|
||||
RequestsPerMinute int `mapstructure:"requests_per_minute"`
|
||||
Burst int `mapstructure:"burst"`
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from file and environment variables
|
||||
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
|
||||
}
|
||||
|
||||
if err := viper.Unmarshal(config); err != nil {
|
||||
return config, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
Reference in New Issue
Block a user