Add configuration management with Viper

- Implement pkg/config package with Viper integration
- Support environment variables with DLC_ prefix
- Add configurable server host, port, and shutdown timeout
- Update server to use configuration values
- Add config.example.yaml template file
- Update AGENTS.md with configuration documentation
- Maintain backward compatibility with default values

Configuration options:
- DLC_SERVER_HOST (default: 0.0.0.0)
- DLC_SERVER_PORT (default: 8080)
- DLC_SHUTDOWN_TIMEOUT (default: 30s)
This commit is contained in:
Gabriel Radureau
2026-04-03 13:53:57 +02:00
parent e52870480d
commit 8103f64db9
6 changed files with 199 additions and 6 deletions

View File

@@ -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

View File

@@ -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 {

23
config.example.yaml Normal file
View File

@@ -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

12
go.mod
View File

@@ -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
)

25
go.sum
View File

@@ -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=

57
pkg/config/config.go Normal file
View File

@@ -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)
}