slate-test-1770505673/pkg/config/config.go
jordan 3bc5efe56f
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-07 23:07:54 +00:00

236 lines
6.5 KiB
Go

// Package config provides configuration loading using Viper.
//
// This package standardizes configuration loading across all services with:
// - Environment variable support
// - .env file support for development
// - Sensible defaults
// - Type-safe configuration structs
//
// Usage:
//
// // Initialize configuration (call once at startup)
// config.MustInit(config.Options{
// AppName: "my-service",
// DefaultPort: 8080,
// })
//
// // Read configuration
// appCfg := config.ReadAppConfig()
// serverCfg := config.ReadServerConfig()
//
// // Or read specific values
// dbURL := viper.GetString("DATABASE_URL")
package config
import (
"fmt"
"time"
"github.com/spf13/viper"
)
// ServerConfig holds HTTP server configuration.
type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
ReadTimeout time.Duration `json:"read_timeout"`
WriteTimeout time.Duration `json:"write_timeout"`
IdleTimeout time.Duration `json:"idle_timeout"`
}
// Addr returns the server address in host:port format.
func (c ServerConfig) Addr() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
// DatabaseConfig holds database connection configuration.
type DatabaseConfig struct {
URL string `json:"url"`
MaxOpenConns int `json:"max_open_conns"`
MaxIdleConns int `json:"max_idle_conns"`
ConnMaxLifetime time.Duration `json:"conn_max_lifetime"`
}
// AppConfig holds application-level configuration common to all services.
type AppConfig struct {
Name string `json:"name"`
Environment string `json:"environment"`
Debug bool `json:"debug"`
}
// IsDevelopment returns true if the environment is development.
func (c AppConfig) IsDevelopment() bool {
return c.Environment == "development"
}
// IsProduction returns true if the environment is production.
func (c AppConfig) IsProduction() bool {
return c.Environment == "production"
}
// LoggingConfig holds logging configuration.
type LoggingConfig struct {
Level string `json:"level"`
Format string `json:"format"`
}
// Options configures the behavior of Init.
type Options struct {
// AppName is the default name for the application.
AppName string
// DefaultPort is the default server port if not specified.
DefaultPort int
// EnvFile is the path to the .env file for development.
// Defaults to ".env" if not specified.
EnvFile string
// SetDefaults is an optional function to set additional viper defaults
// before loading configuration.
SetDefaults func()
// SkipEnvFile skips loading from .env file.
// Useful for production where all config comes from environment.
SkipEnvFile bool
}
// Init initializes viper with common defaults and loads configuration.
// This should be called once at service startup before using viper.Get* functions.
//
// Load order (later sources override earlier):
// 1. Default values
// 2. .env file (development only)
// 3. Environment variables
func Init(opts Options) error {
viper.SetConfigType("env")
viper.SetEnvPrefix("")
viper.AutomaticEnv()
// Set common defaults
setCommonDefaults(opts)
// Set service-specific defaults
if opts.SetDefaults != nil {
opts.SetDefaults()
}
// In development, optionally load from .env file
if !opts.SkipEnvFile {
env := viper.GetString("APP_ENVIRONMENT")
if env == "development" || env == "" {
envFile := opts.EnvFile
if envFile == "" {
envFile = ".env"
}
viper.SetConfigFile(envFile)
_ = viper.ReadInConfig() // Ignore error - fallback to env vars
}
}
return nil
}
// MustInit is like Init but panics if initialization fails.
// This is useful in main() where you want to fail fast on configuration errors.
func MustInit(opts Options) {
if err := Init(opts); err != nil {
panic(fmt.Sprintf("failed to initialize config: %v", err))
}
}
// setCommonDefaults sets default values for common configuration fields.
func setCommonDefaults(opts Options) {
// App defaults
appName := opts.AppName
if appName == "" {
appName = "service"
}
viper.SetDefault("APP_NAME", appName)
viper.SetDefault("APP_ENVIRONMENT", "development")
viper.SetDefault("APP_DEBUG", false)
// Server defaults
viper.SetDefault("SERVER_HOST", "0.0.0.0")
port := opts.DefaultPort
if port == 0 {
port = 8080
}
viper.SetDefault("SERVER_PORT", port)
viper.SetDefault("SERVER_READ_TIMEOUT", "30s")
viper.SetDefault("SERVER_WRITE_TIMEOUT", "0s") // Disabled for SSE support
viper.SetDefault("SERVER_IDLE_TIMEOUT", "120s")
// Database defaults
viper.SetDefault("DATABASE_MAX_OPEN_CONNS", 25)
viper.SetDefault("DATABASE_MAX_IDLE_CONNS", 5)
viper.SetDefault("DATABASE_CONN_MAX_LIFETIME", "5m")
// Logging defaults
viper.SetDefault("LOG_LEVEL", "info")
viper.SetDefault("LOG_FORMAT", "auto") // auto = JSON in prod, text in dev
}
// ReadAppConfig reads AppConfig from viper.
func ReadAppConfig() AppConfig {
return AppConfig{
Name: viper.GetString("APP_NAME"),
Environment: viper.GetString("APP_ENVIRONMENT"),
Debug: viper.GetBool("APP_DEBUG"),
}
}
// ReadServerConfig reads ServerConfig from viper.
func ReadServerConfig() ServerConfig {
return ServerConfig{
Host: viper.GetString("SERVER_HOST"),
Port: viper.GetInt("SERVER_PORT"),
ReadTimeout: viper.GetDuration("SERVER_READ_TIMEOUT"),
WriteTimeout: viper.GetDuration("SERVER_WRITE_TIMEOUT"),
IdleTimeout: viper.GetDuration("SERVER_IDLE_TIMEOUT"),
}
}
// ReadDatabaseConfig reads DatabaseConfig from viper.
func ReadDatabaseConfig() DatabaseConfig {
return DatabaseConfig{
URL: viper.GetString("DATABASE_URL"),
MaxOpenConns: viper.GetInt("DATABASE_MAX_OPEN_CONNS"),
MaxIdleConns: viper.GetInt("DATABASE_MAX_IDLE_CONNS"),
ConnMaxLifetime: viper.GetDuration("DATABASE_CONN_MAX_LIFETIME"),
}
}
// ReadLoggingConfig reads LoggingConfig from viper.
func ReadLoggingConfig() LoggingConfig {
return LoggingConfig{
Level: viper.GetString("LOG_LEVEL"),
Format: viper.GetString("LOG_FORMAT"),
}
}
// GetString returns a string configuration value.
func GetString(key string) string {
return viper.GetString(key)
}
// GetInt returns an integer configuration value.
func GetInt(key string) int {
return viper.GetInt(key)
}
// GetBool returns a boolean configuration value.
func GetBool(key string) bool {
return viper.GetBool(key)
}
// GetDuration returns a duration configuration value.
func GetDuration(key string) time.Duration {
return viper.GetDuration(key)
}
// IsSet returns true if the key is set in configuration.
func IsSet(key string) bool {
return viper.IsSet(key)
}