477518e5eb
API instrumentation - metrics and logging * api instrumentation prometheus metrics and logging for all routes * Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/prometheus * add log_level and sync the config initialize * docs * cleanup * add to compose:up:test * docs for prometheus testing * custom metrics * cleanup * merge main * add tests * cleanup docs * cleanup * lint * fix unit tests (attempt) * fix tests * resolvesetlogger * expose 8080 * compose changes * bugfixesformichael * don't build _test * merge in main * job_sync_runner
246 lines
5.9 KiB
Go
246 lines
5.9 KiB
Go
package serviceconfig
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"queryorchestration/internal/serviceconfig/aws"
|
|
"queryorchestration/internal/serviceconfig/database"
|
|
"queryorchestration/internal/serviceconfig/logger"
|
|
"queryorchestration/internal/serviceconfig/observability"
|
|
"queryorchestration/internal/serviceconfig/queue"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
|
|
"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 {
|
|
logger.LogConfig
|
|
observability.ObsConfig
|
|
database.DBConfig
|
|
aws.AWSConfig
|
|
queue.QueueConfig
|
|
|
|
// miscellaneous fields uncategorized
|
|
// PWD will replace the BASE_PATH env var
|
|
Pwd string `env:"PWD,required,notEmpty"`
|
|
// BASE_PATH will override the PWD env var if present
|
|
BasePath string `env:"BASE_PATH"`
|
|
}
|
|
|
|
// 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
|
|
GetBasePath() string
|
|
SetBasePath(string)
|
|
SetDBConfig(*database.DBConfig)
|
|
SetAWSConfig(*aws.AWSConfig)
|
|
}
|
|
|
|
// 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 := reflect.ValueOf(cfg).Interface().(*BaseConfig)
|
|
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) {
|
|
baseConfig = getBaseConfig(b.Interface().(ConfigProvider))
|
|
} else if reflect.PointerTo(b.Type()).Implements(intType) {
|
|
baseConfig = getBaseConfig(b.Addr().Interface().(ConfigProvider))
|
|
}
|
|
}
|
|
|
|
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) GetBasePath() string {
|
|
if b.BasePath != "" {
|
|
return b.BasePath
|
|
}
|
|
return b.Pwd
|
|
}
|
|
|
|
func (b *BaseConfig) SetBasePath(p string) {
|
|
b.BasePath = p
|
|
}
|
|
|
|
func (b *BaseConfig) SetDBConfig(cfg *database.DBConfig) {
|
|
b.DBConfig = *cfg
|
|
}
|
|
|
|
func (b *BaseConfig) SetAWSConfig(cfg *aws.AWSConfig) {
|
|
b.AWSConfig = *cfg
|
|
}
|
|
|
|
func (b *BaseConfig) PrintConfig(prefixSecret string) {
|
|
b.printConfigRecursive(reflect.ValueOf(b), "", prefixSecret, make(map[uintptr]bool))
|
|
}
|
|
|
|
func (b *BaseConfig) printConfigRecursive(val reflect.Value, prefix string, prefixSecret string, visited map[uintptr]bool) {
|
|
// Handle pointer dereference
|
|
if val.Kind() == reflect.Ptr {
|
|
val = val.Elem()
|
|
}
|
|
|
|
// Prevent infinite recursion using the memory address
|
|
// Recommended way to get the uintptr
|
|
addr := uintptr(val.Addr().UnsafePointer())
|
|
if visited[addr] {
|
|
return
|
|
}
|
|
visited[addr] = 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
|
|
|
|
// Check for env tag
|
|
envTag := fieldType.Tag.Get("env")
|
|
if envTag == "" {
|
|
// If this is a struct, we still need to recurse into it
|
|
// as it might contain fields with env tags
|
|
if field.Kind() == reflect.Struct {
|
|
b.printConfigRecursive(field, fullPath+".", prefixSecret, visited)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Handle different kinds of fields
|
|
switch field.Kind() {
|
|
case reflect.Struct:
|
|
// If it's a struct AND has an env tag, print it and recurse
|
|
var valueStr string
|
|
if field.CanInterface() {
|
|
valueStr = fmt.Sprintf("%+v", field.Interface())
|
|
}
|
|
|
|
b.Logger.Info("Struct Config value",
|
|
"key", fullPath,
|
|
"value", valueStr)
|
|
b.printConfigRecursive(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) > 3 {
|
|
valueStr = valueStr[:3] + "..."
|
|
}
|
|
}
|
|
|
|
b.Logger.Info("Config value",
|
|
"key", fullPath,
|
|
"value", valueStr)
|
|
}
|
|
}
|
|
}
|