8e66e55119
DRAFT PR: Prototype for serviceconfig * in progress * in progress * extract common methods * Merge remote-tracking branch 'origin/feature/serviceconfig' into feature/serviceconfig * fix types * cleanup * add tests * Merged main into feature/serviceconfig Approved-by: Michael McGuinness
181 lines
5.0 KiB
Go
181 lines
5.0 KiB
Go
package serviceconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/caarlos0/env/v11"
|
|
"github.com/joho/godotenv"
|
|
"log/slog"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
// Common Types
|
|
// ------------
|
|
|
|
// DbConfig contains database connection configuration parameters
|
|
// that are shared across services.
|
|
type DbConfig struct {
|
|
DBUser string `env:"DB_USER,required,notEmpty"` // Database username
|
|
DBSecret string `env:"DB_PASS,required,notEmpty"` // Database password
|
|
DBHost string `env:"DB_HOST,required,notEmpty"` // Database host address
|
|
DBPort int `env:"DB_PORT,required,notEmpty"` // Database port number
|
|
DBName string `env:"DB_NAME,required,notEmpty"` // Database name
|
|
DBNoSSL bool `env:"DB_NOSSL" envDefault:"false"` // SSL mode configuration
|
|
}
|
|
|
|
// LoggerConfig wraps the application logger instance.
|
|
type LoggerConfig struct {
|
|
Logger *slog.Logger // Structured logger instance
|
|
}
|
|
|
|
// AwsConfig contains AWS credentials and configuration.
|
|
type AwsConfig struct {
|
|
AWSClientID string `env:"AWS_CLIENT_ID"` // AWS client identifier
|
|
AWSSecretKey string `env:"AWS_SECRET_KEY"` // AWS secret access key
|
|
}
|
|
|
|
// BaseConfig provides common configuration fields and functionality
|
|
// that can be embedded in service-specific configs.
|
|
type BaseConfig struct {
|
|
LoggerConfig
|
|
DbConfig
|
|
AwsConfig
|
|
}
|
|
|
|
// Interfaces
|
|
// ----------
|
|
|
|
// ConfigProvider defines the basic requirements for all configuration types.
|
|
// Implementations must provide logging capabilities and configuration display.
|
|
type ConfigProvider interface {
|
|
GetLogger() *slog.Logger
|
|
LogConfig(prefixSecret string)
|
|
}
|
|
|
|
// 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 InitializeConfig(cfg ConfigProvider) error {
|
|
// First, initialize the logger by getting the base config through reflection
|
|
val := reflect.ValueOf(cfg).Elem()
|
|
|
|
// Look for the embedded BaseConfig
|
|
var baseConfig *BaseConfig
|
|
for i := 0; i < val.NumField(); i++ {
|
|
if val.Type().Field(i).Anonymous && val.Field(i).Type() == reflect.TypeOf(BaseConfig{}) {
|
|
baseConfig = val.Field(i).Addr().Interface().(*BaseConfig)
|
|
break
|
|
}
|
|
}
|
|
|
|
if baseConfig == nil {
|
|
return fmt.Errorf("no BaseConfig found in the provided config struct")
|
|
}
|
|
|
|
// Initialize the logger in the base config
|
|
baseConfig.Logger = slog.New(slog.NewTextHandler(os.Stdout, nil))
|
|
slog.SetDefault(baseConfig.Logger)
|
|
|
|
// Load .env file
|
|
if err := godotenv.Load(); err != nil {
|
|
slog.Warn("No .env file found or error loading it", "error", err)
|
|
}
|
|
|
|
// Parse environment variables
|
|
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)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Log the configuration
|
|
cfg.LogConfig("secret")
|
|
return nil
|
|
}
|
|
|
|
// GetLogger implements the ConfigProvider interface by returning
|
|
// the configured logger instance.
|
|
func (b *BaseConfig) GetLogger() *slog.Logger {
|
|
return b.Logger
|
|
}
|
|
|
|
// Logging Methods
|
|
// --------------
|
|
|
|
// LogConfig displays the current configuration values, masking sensitive
|
|
// information based on the provided prefixSecret. It implements the
|
|
// ConfigProvider interface.
|
|
func (b *BaseConfig) LogConfig(prefixSecret string) {
|
|
b.logConfigRecursive(reflect.ValueOf(b), "", prefixSecret, make(map[reflect.Value]bool))
|
|
}
|
|
|
|
// logConfigRecursive is a helper method that recursively traverses the configuration
|
|
// structure and logs each field's value. It handles nested structs, pointers,
|
|
// and masks sensitive values containing the specified prefixSecret in their names.
|
|
func (b *BaseConfig) logConfigRecursive(val reflect.Value, prefix string, prefixSecret string, visited map[reflect.Value]bool) {
|
|
// Handle pointer dereference
|
|
if val.Kind() == reflect.Ptr {
|
|
val = val.Elem()
|
|
}
|
|
|
|
// Prevent infinite recursion
|
|
if visited[val] {
|
|
return
|
|
}
|
|
visited[val] = true
|
|
|
|
typ := val.Type()
|
|
|
|
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
|
|
fullPath := prefix + fieldName
|
|
|
|
// Handle embedded fields
|
|
if fieldType.Anonymous {
|
|
b.logConfigRecursive(field, prefix, prefixSecret, visited)
|
|
continue
|
|
}
|
|
|
|
switch field.Kind() {
|
|
case reflect.Struct:
|
|
b.logConfigRecursive(field, fullPath+".", prefixSecret, visited)
|
|
default:
|
|
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) > 5 {
|
|
valueStr = valueStr[:5] + "..."
|
|
}
|
|
}
|
|
|
|
b.Logger.Info("Config value",
|
|
"key", fullPath,
|
|
"value", valueStr)
|
|
}
|
|
}
|
|
}
|