Files
query-orchestration/internal/serviceconfig/common.go
T
Jay Brown 8e66e55119 Merged in feature/serviceconfig (pull request #33)
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
2025-01-27 17:59:03 +00:00

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