diff --git a/AGENTS.md b/AGENTS.md index 5265d5e..9e3bc65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,14 @@ This file documents the AI agents, tools, and development workflow for the Dance - Server management guide - API endpoint documentation +### Phase 5: Configuration Management (Completed ✅) +- Viper integration for configuration +- Environment variable support with DLC_ prefix +- Customizable server host/port +- Configurable shutdown timeout +- Configuration validation and logging +- Example configuration file + ## 🛠️ Tools & Technologies | Component | Technology | Version | @@ -45,6 +53,7 @@ This file documents the AI agents, tools, and development workflow for the Dance | Language | Go | 1.26.1 | | Router | Chi | v5.2.5 | | Logging | Zerolog | v1.35.0 | +| Configuration | Viper | v1.21.0 | | Testing | Standard Library | - | | Dependency Management | Go Modules | - | @@ -58,6 +67,8 @@ DanceLessonsCoach/ │ └── server/ # Web server │ └── main.go ├── pkg/ +│ ├── config/ # Configuration management +│ │ └── config.go # Viper-based config │ ├── greet/ # Core domain logic │ │ ├── api_v1.go # API handlers │ │ ├── greet.go # Service implementation @@ -66,6 +77,7 @@ DanceLessonsCoach/ │ └── server.go ├── go.mod # Dependencies ├── go.sum # Dependency checksums +├── config.example.yaml # Configuration template ├── README.md # User documentation ├── AGENTS.md # This file └── .gitignore # Ignore patterns @@ -98,6 +110,64 @@ Server running on :8080 - 30-second shutdown timeout - Proper resource cleanup +### Configuration Management + +The server supports flexible configuration through environment variables with the `DLC_` prefix: + +**Available Configuration Options:** + +| Option | Environment Variable | Default Value | Description | +|--------|---------------------|---------------|-------------| +| Host | `DLC_SERVER_HOST` | `0.0.0.0` | Server bind address | +| Port | `DLC_SERVER_PORT` | `8080` | Server listening port | +| Shutdown Timeout | `DLC_SHUTDOWN_TIMEOUT` | `30s` | Graceful shutdown timeout | + +**Usage Examples:** + +```bash +# Custom port +export DLC_SERVER_PORT=9090 +go run cmd/server/main.go & + +# Custom host and port +export DLC_SERVER_HOST="127.0.0.1" +export DLC_SERVER_PORT=8081 +go run cmd/server/main.go & + +# Custom shutdown timeout +export DLC_SHUTDOWN_TIMEOUT=45s +go run cmd/server/main.go & +``` + +**Configuration File Support:** + +A `config.example.yaml` file is provided as a template: + +```yaml +server: + host: "0.0.0.0" + port: 8080 + +shutdown: + timeout: 30s +``` + +**Configuration Loading:** +- Environment variables take precedence over defaults +- All configuration is validated on startup +- Invalid configurations cause server startup failure +- Configuration values are logged at startup + +**Verification:** +```bash +# Test with custom configuration +DLC_SERVER_PORT=9090 DLC_SERVER_HOST="127.0.0.1" go run cmd/server/main.go + +# Verify it's running on the custom port +curl http://127.0.0.1:9090/api/health +# Expected: {"status":"healthy"} +``` + ### Checking Server Status ```bash diff --git a/cmd/server/main.go b/cmd/server/main.go index 92a7dc0..4664c8d 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -7,13 +7,19 @@ import ( "os" "os/signal" "syscall" - "time" + "DanceLessonsCoach/pkg/config" "DanceLessonsCoach/pkg/server" "github.com/rs/zerolog/log" ) func main() { + // Load configuration + cfg, err := config.LoadConfig() + if err != nil { + log.Fatal().Err(err).Msg("Failed to load configuration") + } + // Create root context with cancellation ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -27,11 +33,11 @@ func main() { serverCtx, serverStop := context.WithCancel(ctx) go func() { - fmt.Println("Server running on :8080") - log.Info().Msg("Starting HTTP server on :8080") + fmt.Printf("Server running on %s\n", cfg.GetServerAddress()) + log.Info().Str("address", cfg.GetServerAddress()).Msg("Starting HTTP server") srv := &http.Server{ - Addr: ":8080", + Addr: cfg.GetServerAddress(), Handler: server.Router(), } @@ -40,8 +46,8 @@ func main() { <-sigChan log.Info().Msg("Shutdown signal received") - // Create shutdown context with timeout - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) + // Create shutdown context with timeout from config + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), cfg.Shutdown.Timeout) defer shutdownCancel() if err := srv.Shutdown(shutdownCtx); err != nil { diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..a33dbe9 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,23 @@ +# DanceLessonsCoach Configuration Example +# This file shows the available configuration options +# You can use this as a template for your own configuration + +# Server configuration +server: + # Host address to bind to (default: "0.0.0.0") + host: "0.0.0.0" + + # Port to listen on (default: 8080) + port: 8080 + +# Shutdown configuration +shutdown: + # Timeout duration for graceful shutdown (default: 30s) + # Format: number + unit (s, m, h) + timeout: 30s + +# Environment Variables +# You can also configure via environment variables with DLC_ prefix: +# DLC_SERVER_HOST=0.0.0.0 +# DLC_SERVER_PORT=8080 +# DLC_SHUTDOWN_TIMEOUT=30s \ No newline at end of file diff --git a/go.mod b/go.mod index caa7c62..6f05129 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,21 @@ module DanceLessonsCoach go 1.26.1 require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rs/zerolog v1.35.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index bd12b19..3f02cd8 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,36 @@ +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI= github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..38d2e0f --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,57 @@ +package config + +import ( + "fmt" + "time" + + "github.com/rs/zerolog/log" + "github.com/spf13/viper" +) + +// Config represents the application configuration +type Config struct { + Server struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + } + Shutdown struct { + Timeout time.Duration `mapstructure:"timeout"` + } +} + +// LoadConfig loads configuration from environment variables and defaults +func LoadConfig() (*Config, error) { + v := viper.New() + + // Set default values + v.SetDefault("server.host", "0.0.0.0") + v.SetDefault("server.port", 8080) + v.SetDefault("shutdown.timeout", 30*time.Second) + + // Bind environment variables + v.AutomaticEnv() + v.SetEnvPrefix("DLC") // DanceLessonsCoach prefix + v.BindEnv("server.host", "DLC_SERVER_HOST") + v.BindEnv("server.port", "DLC_SERVER_PORT") + v.BindEnv("shutdown.timeout", "DLC_SHUTDOWN_TIMEOUT") + + // Unmarshal into Config struct + var config Config + if err := v.Unmarshal(&config); err != nil { + log.Error().Err(err).Msg("Failed to unmarshal config") + return nil, fmt.Errorf("config unmarshal error: %w", err) + } + + log.Info(). + Str("host", config.Server.Host). + Int("port", config.Server.Port). + Dur("shutdown_timeout", config.Shutdown.Timeout). + Msg("Configuration loaded") + + return &config, nil +} + +// GetServerAddress returns the formatted server address (host:port) +func (c *Config) GetServerAddress() string { + return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port) +} \ No newline at end of file