package serviceconfig import ( "errors" "fmt" "log/slog" "queryorchestration/internal/serviceconfig/auth" "reflect" "strings" "sync" "queryorchestration/internal/serviceconfig/aws" "queryorchestration/internal/serviceconfig/database" "queryorchestration/internal/serviceconfig/logger" "queryorchestration/internal/serviceconfig/observability" "queryorchestration/internal/serviceconfig/queue" "queryorchestration/internal/serviceconfig/threadpool" "github.com/caarlos0/env/v11" "github.com/joho/godotenv" ) // Common Types // ------------ // Logging // BaseConfig provides common configuration fields and functionality // that can be embedded in service-specific configs. type BaseConfig struct { // Always place non struct fields at the top of the struct // this is a requirement for the PrintAuthConfig function. // These are the fields that are common to all services. Port int `env:"PORT" envDefault:"8080"` BaseURL string `env:"BASE_URL" envDefault:"http://localhost"` auth.CognitoConfig threadpool.ThreadPoolConfig logger.LogConfig observability.ObsConfig database.DBConfig aws.AWSConfig queue.QueueConfig } // Interfaces // ---------- // ConfigProvider defines the basic requirements for all configuration types. // Implementations must provide logging capabilities and configuration display. type ConfigProvider interface { threadpool.ConfigProvider database.ConfigProvider observability.ConfigProvider logger.ConfigProvider aws.ConfigProvider queue.ConfigProvider auth.ConfigProvider } // Configuration Methods // ------------------- // InitializeConfig sets up a configuration instance by loading environment // variables and initializing the logger. It expects a ConfigProvider implementation. // Returns an error if initialization fails. func getBaseConfig(cfg ConfigProvider) *BaseConfig { if cfg == nil { return nil } t := reflect.ValueOf(cfg).Type() if t == reflect.TypeOf(&BaseConfig{}) { v, ok := reflect.ValueOf(cfg).Interface().(*BaseConfig) if !ok { return nil } return v } else if t.Kind() == reflect.Ptr && t.Elem().Kind() != reflect.Struct { return nil } val := reflect.ValueOf(cfg).Elem() var baseConfig *BaseConfig for i := 0; i < val.NumField() && baseConfig == nil; i++ { b := val.Field(i) intType := reflect.TypeOf((*ConfigProvider)(nil)).Elem() if b.Type().Implements(intType) { provider, ok := b.Interface().(ConfigProvider) if !ok { return nil } baseConfig = getBaseConfig(provider) } else if reflect.PointerTo(b.Type()).Implements(intType) { provider, ok := b.Addr().Interface().(ConfigProvider) if !ok { return nil } baseConfig = getBaseConfig(provider) } } return baseConfig } var ( initOnce = sync.Once{} initErr error ) func ResetConfigInitOnceTestOnly() { initOnce = sync.Once{} initErr = nil } // InitializeConfig will initialize all of the configuration for the serivice // and is guaranteed to run only once however many times it gets called. func InitializeConfig(cfg ConfigProvider) error { var initErr error initOnce.Do(func() { initErr = initializeConfigPrivate(cfg) }) return initErr } // initializeConfigPrivate is the internal implementation of InitializeConfig so that // testing can call it multiple times. func initializeConfigPrivate(cfg ConfigProvider) error { baseConfig := getBaseConfig(cfg) if baseConfig == nil { initErr = errors.New("no BaseConfig found in the provided config struct") return initErr } // Initialize with info level cfg.SetDefaultLogger() if err := godotenv.Load(); err != nil { slog.Warn("No .env file found or error loading it", "error", err) } if err := env.Parse(cfg); err != nil { if envErr, ok := err.(env.AggregateError); ok { slog.Error("Missing required environment variables:") for _, e := range envErr.Errors { slog.Error(e.Error()) } } else { slog.Error("Error parsing environment variables", "error", err) } initErr = err return initErr } // Reinitialize with environment level cfg.SetDefaultLogger() return initErr } func (b *BaseConfig) PrintConfig(prefixSecret string) { visited := make(map[uintptr]bool) b.printConfigRecursive(reflect.ValueOf(b).Elem(), "", prefixSecret, visited) } func (b *BaseConfig) printConfigRecursive(val reflect.Value, prefix string, prefixSecret string, visited map[uintptr]bool) { // Skip non-structs and nil pointers if val.Kind() == reflect.Ptr { if val.IsNil() { return } val = val.Elem() } if val.Kind() != reflect.Struct { return } // Check for cycles, but only for addressable values if val.CanAddr() { addr := uintptr(val.Addr().UnsafePointer()) if visited[addr] { return } visited[addr] = true } typ := val.Type() // Process all fields including embedded ones for i := 0; i < val.NumField(); i++ { field := val.Field(i) fieldType := typ.Field(i) // Skip unexported fields if !fieldType.IsExported() { continue } fieldName := fieldType.Name // Handle anonymous (embedded) fields specially if fieldType.Anonymous { // For embedded fields, recurse without adding to the path // This is key - we want to process the embedded struct's fields directly b.printConfigRecursive(field, prefix, prefixSecret, visited) continue } fullPath := prefix + fieldName envTag := fieldType.Tag.Get("env") // Process fields with env tags if envTag != "" { if field.Kind() == reflect.Struct { // For structs with env tags, log the struct and recurse var valueStr string if field.CanInterface() { valueStr = fmt.Sprintf("%+v", field.Interface()) } b.Logger.Info("Struct Config value", "key", fullPath, "value", valueStr) } else { // For non-struct fields with env tags var valueStr string if field.Kind() == reflect.String { valueStr = field.String() } else { valueStr = fmt.Sprintf("%v", field.Interface()) } // Mask sensitive values if strings.Contains(strings.ToLower(fieldName), strings.ToLower(prefixSecret)) { if len(valueStr) > 3 { valueStr = valueStr[:3] + "..." } } b.Logger.Info("Config value", "key", fullPath, "value", valueStr) } } // Recurse into structs regardless of env tag if field.Kind() == reflect.Struct { b.printConfigRecursive(field, fullPath+".", prefixSecret, visited) } } }