Files
query-orchestration/internal/serviceconfig/common.go
T
2025-04-11 11:13:07 -07:00

257 lines
6.1 KiB
Go

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"
"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.
Pwd string `env:"PWD,required,notEmpty"`
//rbac.AuthConfig
auth.CognitoConfig
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 {
database.ConfigProvider
observability.ConfigProvider
logger.ConfigProvider
aws.ConfigProvider
queue.ConfigProvider
//rbac.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)
}
}
}