Files
query-orchestration/internal/serviceconfig/common.go
T
Jay Brown 477518e5eb Merged in feature/prometheus (pull request #52)
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
2025-02-13 15:44:08 +00:00

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