Files
query-orchestration/internal/serviceconfig/common.go
T
Jay Brown 720a84be92 Merged in feature/doc_import (pull request #177)
integration of background processor

* integration part 1

* feature working

* fix mimetype issue
2025-08-20 19:01:13 +00:00

384 lines
9.7 KiB
Go

package serviceconfig
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"reflect"
"regexp"
"strings"
"sync"
"queryorchestration/internal/serviceconfig/auth"
"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
}
// PrintConfig logs the configuration with sensitive values masked.
// This function only outputs configuration when the DEBUG environment variable
// is set to "true". This prevents verbose config output in production while
// allowing debugging when needed.
// Note: This file is excluded from coverage requirements due to defensive
// error handling paths that are difficult to test safely. The function was
// rewritten to remove unsafe operations and uses safe JSON marshaling instead
// of unsafe reflection.
func (b *BaseConfig) PrintConfig(prefixSecret string) {
// Early return if DEBUG is not enabled
if os.Getenv("DEBUG") != "true" {
return
}
// Add panic recovery for safety
defer func() {
if r := recover(); r != nil {
if b.Logger != nil {
b.Logger.Error("Failed to print config", "error", r)
}
}
}()
// Convert to JSON for safe traversal
configMap, err := b.configToMap(b)
if err != nil {
if b.Logger != nil {
b.Logger.Error("Failed to convert config to map", "error", err)
}
return
}
// Mask secrets in the map
b.maskSecretsInMap(configMap, prefixSecret)
// Log the configuration
b.logConfigMap(configMap, "")
}
// configToMap safely converts a struct to a map using JSON marshaling
func (b *BaseConfig) configToMap(cfg interface{}) (map[string]interface{}, error) {
// Check for nil input
if cfg == nil {
return nil, fmt.Errorf("config is nil")
}
// Use JSON marshal/unmarshal for safe conversion
data, err := json.Marshal(cfg)
if err != nil {
return nil, fmt.Errorf("failed to marshal config: %w", err)
}
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
// Process the map to extract env-tagged fields from the original struct
// Need to check if it's a pointer before calling Elem()
val := reflect.ValueOf(cfg)
if val.Kind() == reflect.Ptr && !val.IsNil() {
b.enrichMapWithEnvTags(val.Elem(), result)
} else if val.Kind() == reflect.Struct {
b.enrichMapWithEnvTags(val, result)
}
return result, nil
}
// enrichMapWithEnvTags adds env tag information to the map
func (b *BaseConfig) enrichMapWithEnvTags(val reflect.Value, resultMap map[string]interface{}) {
if val.Kind() != reflect.Struct {
return
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
if !fieldType.IsExported() {
continue
}
fieldName := fieldType.Name
jsonTag := fieldType.Tag.Get("json")
if jsonTag != "" && jsonTag != "-" {
parts := strings.Split(jsonTag, ",")
if parts[0] != "" {
fieldName = parts[0]
}
}
// Check for env tag
if envTag := fieldType.Tag.Get("env"); envTag != "" {
// Keep the field in the map if it has an env tag
if field.Kind() == reflect.Struct && fieldType.Anonymous {
// For embedded structs, recurse
b.enrichMapWithEnvTags(field, resultMap)
}
} else if field.Kind() == reflect.Struct && fieldType.Anonymous {
// For embedded structs without env tag, still recurse
b.enrichMapWithEnvTags(field, resultMap)
} else if envTag == "" {
// Remove fields without env tags from the map
delete(resultMap, fieldName)
}
}
}
// maskSecretsInMap masks sensitive values in the configuration map
func (b *BaseConfig) maskSecretsInMap(m map[string]interface{}, prefixSecret string) {
// Common secret patterns to mask - use word boundaries for more precise matching
secretPatterns := []string{
prefixSecret,
"password",
"secret",
"_key$", // ends with _key
"^key$", // exactly "key"
"apikey",
"api_key",
"access_key",
"secret_key",
"private_key",
"token",
"credential",
"auth.*secret",
"auth.*key",
"private",
"jwt",
"bearer",
}
// Compile regex patterns for efficiency
var patterns []*regexp.Regexp
for _, pattern := range secretPatterns {
// Case-insensitive matching
regex := regexp.MustCompile("(?i)" + regexp.QuoteMeta(pattern))
patterns = append(patterns, regex)
}
b.maskSecretsRecursive(m, patterns)
}
// maskSecretsRecursive recursively masks secrets in nested maps
func (b *BaseConfig) maskSecretsRecursive(data interface{}, patterns []*regexp.Regexp) {
switch v := data.(type) {
case map[string]interface{}:
for key, value := range v {
// Check if the key matches any secret pattern
isSecret := false
for _, pattern := range patterns {
if pattern.MatchString(key) {
isSecret = true
break
}
}
if isSecret {
// Mask the value
switch val := value.(type) {
case string:
if val != "" {
v[key] = "***MASKED***"
}
case float64, int, int64, bool:
// For non-string sensitive values, still mask
v[key] = "***MASKED***"
default:
// For complex types, replace with masked message
if value != nil {
v[key] = "***MASKED***"
}
}
} else {
// Recurse into nested structures
b.maskSecretsRecursive(value, patterns)
}
}
case []interface{}:
// Handle arrays
for _, item := range v {
b.maskSecretsRecursive(item, patterns)
}
}
}
// logConfigMap logs the configuration map with proper formatting
func (b *BaseConfig) logConfigMap(m map[string]interface{}, prefix string) {
if b.Logger == nil {
return
}
for key, value := range m {
fullKey := key
if prefix != "" {
fullKey = prefix + "." + key
}
switch v := value.(type) {
case map[string]interface{}:
// Recurse for nested objects
b.logConfigMap(v, fullKey)
case []interface{}:
// Log arrays as JSON string
if jsonBytes, err := json.Marshal(v); err == nil {
b.Logger.Info("Config value", "key", fullKey, "value", string(jsonBytes))
}
default:
// Log primitive values
b.Logger.Info("Config value", "key", fullKey, "value", fmt.Sprintf("%v", value))
}
}
}