package config import ( "fmt" "os" "strings" "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/viper" "dance-lessons-coach/pkg/version" ) // Config represents the application configuration type Config struct { Server ServerConfig `mapstructure:"server"` Shutdown ShutdownConfig `mapstructure:"shutdown"` Logging LoggingConfig `mapstructure:"logging"` Telemetry TelemetryConfig `mapstructure:"telemetry"` API APIConfig `mapstructure:"api"` } // ServerConfig holds server-related configuration type ServerConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` } // ShutdownConfig holds shutdown-related configuration type ShutdownConfig struct { Timeout time.Duration `mapstructure:"timeout"` } // LoggingConfig holds logging-related configuration type LoggingConfig struct { JSON bool `mapstructure:"json"` Level string `mapstructure:"level"` Output string `mapstructure:"output"` } // TelemetryConfig holds OpenTelemetry-related configuration type TelemetryConfig struct { Enabled bool `mapstructure:"enabled"` OTLPEndpoint string `mapstructure:"otlp_endpoint"` ServiceName string `mapstructure:"service_name"` Insecure bool `mapstructure:"insecure"` Sampler SamplerConfig `mapstructure:"sampler"` } // APIConfig holds API version configuration type APIConfig struct { V2Enabled bool `mapstructure:"v2_enabled"` } // VersionInfo holds application version information type VersionInfo struct { Version string `mapstructure:"-"` // Set via ldflags Commit string `mapstructure:"-"` // Set via ldflags Date string `mapstructure:"-"` // Set via ldflags GoVersion string `mapstructure:"-"` // Set at runtime } // VersionCommand handles version display func (c *Config) VersionCommand() string { // This will be enhanced when we integrate with cobra return fmt.Sprintf("DanceLessonsCoach %s (commit: %s, built: %s, go: %s)", version.Version, version.Commit, version.Date, version.GoVersion) } // SamplerConfig holds tracing sampler configuration type SamplerConfig struct { Type string `mapstructure:"type"` Ratio float64 `mapstructure:"ratio"` } // LoadConfig loads configuration from file, environment variables, and defaults // Configuration priority: file > environment variables > defaults // To specify a custom config file path, set DLC_CONFIG_FILE environment variable func LoadConfig() (*Config, error) { v := viper.New() // Set up initial console logging for config loading messages consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr} log.Logger = log.Output(consoleWriter) // Set default values v.SetDefault("server.host", "0.0.0.0") v.SetDefault("server.port", 8080) v.SetDefault("shutdown.timeout", 30*time.Second) v.SetDefault("logging.json", false) v.SetDefault("logging.level", "trace") v.SetDefault("logging.output", "") // Telemetry defaults v.SetDefault("telemetry.enabled", false) v.SetDefault("telemetry.otlp_endpoint", "localhost:4317") v.SetDefault("telemetry.service_name", "DanceLessonsCoach") v.SetDefault("telemetry.insecure", true) v.SetDefault("telemetry.sampler.type", "parentbased_always_on") v.SetDefault("telemetry.sampler.ratio", 1.0) // API defaults v.SetDefault("api.v2_enabled", false) // Check for custom config file path via environment variable if configFile := os.Getenv("DLC_CONFIG_FILE"); configFile != "" { v.SetConfigFile(configFile) log.Info().Str("config_file", configFile).Msg("Using custom config file path") } else { // Default: look for config.yaml in current directory v.SetConfigName("config") v.SetConfigType("yaml") v.AddConfigPath(".") } // Read config file if it exists if err := v.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { // Config file was found but there was an error reading it log.Warn().Err(err).Msg("Error reading config file, using defaults") } // Config file not found, continue with environment variables and defaults } else { log.Info().Str("config_file", v.ConfigFileUsed()).Msg("Config file loaded") } // 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") v.BindEnv("logging.json", "DLC_LOGGING_JSON") v.BindEnv("logging.level", "DLC_LOGGING_LEVEL") v.BindEnv("logging.output", "DLC_LOGGING_OUTPUT") // Telemetry environment variables v.BindEnv("telemetry.enabled", "DLC_TELEMETRY_ENABLED") v.BindEnv("telemetry.otlp_endpoint", "DLC_TELEMETRY_OTLP_ENDPOINT") v.BindEnv("telemetry.service_name", "DLC_TELEMETRY_SERVICE_NAME") v.BindEnv("telemetry.insecure", "DLC_TELEMETRY_INSECURE") v.BindEnv("telemetry.sampler.type", "DLC_TELEMETRY_SAMPLER_TYPE") v.BindEnv("telemetry.sampler.ratio", "DLC_TELEMETRY_SAMPLER_RATIO") // API environment variables v.BindEnv("api.v2_enabled", "DLC_API_V2_ENABLED") // 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) } // Configure log output format (JSON or console) first if config.Logging.JSON { log.Logger = log.Output(os.Stderr) } else { consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr} log.Logger = log.Output(consoleWriter) } // Setup logging based on configuration config.SetupLogging() log.Info(). Str("host", config.Server.Host). Int("port", config.Server.Port). Dur("shutdown_timeout", config.Shutdown.Timeout). Bool("logging_json", config.Logging.JSON). Str("logging_level", config.Logging.Level). Str("logging_output", config.Logging.Output). Bool("telemetry_enabled", config.Telemetry.Enabled). Str("telemetry_service", config.Telemetry.ServiceName). Bool("api_v2_enabled", config.API.V2Enabled). 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) } // GetTelemetryEnabled returns whether telemetry is enabled func (c *Config) GetTelemetryEnabled() bool { return c.Telemetry.Enabled } // GetOTLPEndpoint returns the OTLP endpoint for telemetry func (c *Config) GetOTLPEndpoint() string { return c.Telemetry.OTLPEndpoint } // GetServiceName returns the service name for telemetry func (c *Config) GetServiceName() string { return c.Telemetry.ServiceName } // GetTelemetryInsecure returns whether to use insecure connection func (c *Config) GetTelemetryInsecure() bool { return c.Telemetry.Insecure } // GetSamplerType returns the sampler type func (c *Config) GetSamplerType() string { return c.Telemetry.Sampler.Type } // GetSamplerRatio returns the sampler ratio func (c *Config) GetSamplerRatio() float64 { return c.Telemetry.Sampler.Ratio } // GetV2Enabled returns whether v2 API is enabled func (c *Config) GetV2Enabled() bool { return c.API.V2Enabled } // GetLogLevel returns the logging level func (c *Config) GetLogLevel() string { return c.Logging.Level } // GetLogOutput returns the log output path func (c *Config) GetLogOutput() string { return c.Logging.Output } // SetupLogging configures zerolog based on the configuration func (c *Config) SetupLogging() { // Parse log level level := parseLogLevel(c.GetLogLevel()) zerolog.SetGlobalLevel(level) // Setup log output c.setupLogOutput() zerolog.TimeFieldFormat = zerolog.TimeFormatUnix } // parseLogLevel converts a string log level to zerolog.Level func parseLogLevel(level string) zerolog.Level { switch strings.ToLower(level) { case "trace": return zerolog.TraceLevel case "debug": return zerolog.DebugLevel case "info": return zerolog.InfoLevel case "warn", "warning": return zerolog.WarnLevel case "error": return zerolog.ErrorLevel case "fatal": return zerolog.FatalLevel case "panic": return zerolog.PanicLevel default: log.Warn().Str("level", level).Msg("Unknown log level, defaulting to trace") return zerolog.TraceLevel } } // setupLogOutput configures the log output based on configuration func (c *Config) setupLogOutput() { output := c.GetLogOutput() if output == "" { // Use stderr by default return } // Open the log file file, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { log.Error().Err(err).Str("output", output).Msg("Failed to open log file, using stderr") return } // Set the log output log.Logger = log.Output(file) log.Trace().Str("output", output).Msg("Logging to file") }