github.com/rudderlabs/rudder-go-kit@v0.30.0/config/config.go (about) 1 // Package config uses the exact same precedence order as Viper, where 2 // each item takes precedence over the item below it: 3 // 4 // - explicit call to Set (case insensitive) 5 // - flag (case insensitive) 6 // - env (case sensitive - see notes below) 7 // - config (case insensitive) 8 // - key/value store (case insensitive) 9 // - default (case insensitive) 10 // 11 // Environment variable resolution is performed based on the following rules: 12 // - If the key contains only uppercase characters, numbers and underscores, the environment variable is looked up in its entirety, e.g. SOME_VARIABLE -> SOME_VARIABLE 13 // - In all other cases, the environment variable is transformed before being looked up as following: 14 // 1. camelCase is converted to snake_case, e.g. someVariable -> some_variable 15 // 2. dots (.) are replaced with underscores (_), e.g. some.variable -> some_variable 16 // 3. the resulting string is uppercased and prefixed with ${PREFIX}_ (default RSERVER_), e.g. some_variable -> RSERVER_SOME_VARIABLE 17 // 18 // Order of keys: 19 // 20 // When registering a variable with multiple keys, the order of the keys is important as it determines the 21 // hierarchical order of the keys. 22 // The first key is the most important one, and the last key is the least important one. 23 // Example: 24 // config.RegisterDurationConfigVariable(90, &cmdTimeout, true, time.Second, 25 // "JobsDB.Router.CommandRequestTimeout", 26 // "JobsDB.CommandRequestTimeout", 27 // ) 28 // 29 // In the above example "JobsDB.Router.CommandRequestTimeout" is checked first. If it doesn't exist then 30 // JobsDB.CommandRequestTimeout is checked. 31 // 32 // WARNING: for this reason, registering with the same keys but in a different order is going to return two 33 // different variables. 34 package config 35 36 import ( 37 "fmt" 38 "regexp" 39 "strconv" 40 "strings" 41 "sync" 42 "time" 43 44 "github.com/spf13/viper" 45 ) 46 47 const DefaultEnvPrefix = "RSERVER" 48 49 // regular expression matching lowercase letter followed by an uppercase letter 50 var camelCaseMatch = regexp.MustCompile("([a-z0-9])([A-Z])") 51 52 // regular expression matching uppercase letters contained in environment variable names 53 var upperCaseMatch = regexp.MustCompile("^[A-Z0-9_]+$") 54 55 // Default is the singleton config instance 56 var Default *Config 57 58 func init() { 59 Default = New() 60 } 61 62 // Reset resets the default, singleton config instance. 63 // Shall only be used by tests, until we move to a proper DI framework 64 func Reset() { 65 Default = New() 66 } 67 68 type Opt func(*Config) 69 70 // WithEnvPrefix sets the environment variable prefix (default: RSERVER) 71 func WithEnvPrefix(prefix string) Opt { 72 return func(c *Config) { 73 c.envPrefix = prefix 74 } 75 } 76 77 // New creates a new config instance 78 func New(opts ...Opt) *Config { 79 c := &Config{ 80 envPrefix: DefaultEnvPrefix, 81 reloadableVars: make(map[string]any), 82 reloadableVarsMisuses: make(map[string]string), 83 } 84 for _, opt := range opts { 85 opt(c) 86 } 87 c.load() 88 return c 89 } 90 91 // Config is the entry point for accessing configuration 92 type Config struct { 93 vLock sync.RWMutex // protects reading and writing to the config (viper is not thread-safe) 94 v *viper.Viper 95 hotReloadableConfigLock sync.RWMutex // protects map holding hot reloadable config keys 96 hotReloadableConfig map[string][]*configValue 97 envsLock sync.RWMutex // protects the envs map below 98 envs map[string]string 99 envPrefix string // prefix for environment variables 100 reloadableVars map[string]any 101 reloadableVarsMisuses map[string]string 102 reloadableVarsLock sync.RWMutex // used to protect both the reloadableVars and reloadableVarsMisuses maps 103 configPath string 104 configPathErr error 105 godotEnvErr error 106 } 107 108 // GetBool gets bool value from config 109 func GetBool(key string, defaultValue bool) (value bool) { 110 return Default.GetBool(key, defaultValue) 111 } 112 113 // GetBool gets bool value from config 114 func (c *Config) GetBool(key string, defaultValue bool) (value bool) { 115 c.vLock.RLock() 116 defer c.vLock.RUnlock() 117 if !c.isSetInternal(key) { 118 return defaultValue 119 } 120 return c.v.GetBool(key) 121 } 122 123 // GetInt gets int value from config 124 func GetInt(key string, defaultValue int) (value int) { 125 return Default.GetInt(key, defaultValue) 126 } 127 128 // GetInt gets int value from config 129 func (c *Config) GetInt(key string, defaultValue int) (value int) { 130 c.vLock.RLock() 131 defer c.vLock.RUnlock() 132 if !c.isSetInternal(key) { 133 return defaultValue 134 } 135 return c.v.GetInt(key) 136 } 137 138 // GetStringMap gets string map value from config 139 func GetStringMap(key string, defaultValue map[string]interface{}) (value map[string]interface{}) { 140 return Default.GetStringMap(key, defaultValue) 141 } 142 143 // GetStringMap gets string map value from config 144 func (c *Config) GetStringMap(key string, defaultValue map[string]interface{}) (value map[string]interface{}) { 145 c.vLock.RLock() 146 defer c.vLock.RUnlock() 147 if !c.isSetInternal(key) { 148 return defaultValue 149 } 150 return c.v.GetStringMap(key) 151 } 152 153 // MustGetInt gets int value from config or panics if the config doesn't exist 154 func MustGetInt(key string) (value int) { 155 return Default.MustGetInt(key) 156 } 157 158 // MustGetInt gets int value from config or panics if the config doesn't exist 159 func (c *Config) MustGetInt(key string) (value int) { 160 c.vLock.RLock() 161 defer c.vLock.RUnlock() 162 if !c.isSetInternal(key) { 163 panic(fmt.Errorf("config key %s not found", key)) 164 } 165 return c.v.GetInt(key) 166 } 167 168 // GetInt64 gets int64 value from config 169 func GetInt64(key string, defaultValue int64) (value int64) { 170 return Default.GetInt64(key, defaultValue) 171 } 172 173 // GetInt64 gets int64 value from config 174 func (c *Config) GetInt64(key string, defaultValue int64) (value int64) { 175 c.vLock.RLock() 176 defer c.vLock.RUnlock() 177 if !c.isSetInternal(key) { 178 return defaultValue 179 } 180 return c.v.GetInt64(key) 181 } 182 183 // GetFloat64 gets float64 value from config 184 func GetFloat64(key string, defaultValue float64) (value float64) { 185 return Default.GetFloat64(key, defaultValue) 186 } 187 188 // GetFloat64 gets float64 value from config 189 func (c *Config) GetFloat64(key string, defaultValue float64) (value float64) { 190 c.vLock.RLock() 191 defer c.vLock.RUnlock() 192 if !c.isSetInternal(key) { 193 return defaultValue 194 } 195 return c.v.GetFloat64(key) 196 } 197 198 // GetString gets string value from config 199 func GetString(key, defaultValue string) (value string) { 200 return Default.GetString(key, defaultValue) 201 } 202 203 // GetString gets string value from config 204 func (c *Config) GetString(key, defaultValue string) (value string) { 205 c.vLock.RLock() 206 defer c.vLock.RUnlock() 207 if !c.isSetInternal(key) { 208 return defaultValue 209 } 210 return c.v.GetString(key) 211 } 212 213 // MustGetString gets string value from config or panics if the config doesn't exist 214 func MustGetString(key string) (value string) { 215 return Default.MustGetString(key) 216 } 217 218 // MustGetString gets string value from config or panics if the config doesn't exist 219 func (c *Config) MustGetString(key string) (value string) { 220 c.vLock.RLock() 221 defer c.vLock.RUnlock() 222 if !c.isSetInternal(key) { 223 panic(fmt.Errorf("config key %s not found", key)) 224 } 225 return c.v.GetString(key) 226 } 227 228 // GetStringSlice gets string slice value from config 229 func GetStringSlice(key string, defaultValue []string) (value []string) { 230 return Default.GetStringSlice(key, defaultValue) 231 } 232 233 // GetStringSlice gets string slice value from config 234 func (c *Config) GetStringSlice(key string, defaultValue []string) (value []string) { 235 c.vLock.RLock() 236 defer c.vLock.RUnlock() 237 if !c.isSetInternal(key) { 238 return defaultValue 239 } 240 return c.v.GetStringSlice(key) 241 } 242 243 // GetDuration gets duration value from config 244 func GetDuration(key string, defaultValueInTimescaleUnits int64, timeScale time.Duration) (value time.Duration) { 245 return Default.GetDuration(key, defaultValueInTimescaleUnits, timeScale) 246 } 247 248 // GetDuration gets duration value from config 249 func (c *Config) GetDuration(key string, defaultValueInTimescaleUnits int64, timeScale time.Duration) (value time.Duration) { 250 c.vLock.RLock() 251 defer c.vLock.RUnlock() 252 if !c.isSetInternal(key) { 253 return time.Duration(defaultValueInTimescaleUnits) * timeScale 254 } else { 255 v := c.v.GetString(key) 256 parseDuration, err := time.ParseDuration(v) 257 if err == nil { 258 return parseDuration 259 } else { 260 _, err = strconv.ParseFloat(v, 64) 261 if err == nil { 262 return c.v.GetDuration(key) * timeScale 263 } else { 264 return time.Duration(defaultValueInTimescaleUnits) * timeScale 265 } 266 } 267 } 268 } 269 270 // IsSet checks if config is set for a key 271 func IsSet(key string) bool { 272 return Default.IsSet(key) 273 } 274 275 // IsSet checks if config is set for a key 276 func (c *Config) IsSet(key string) bool { 277 c.vLock.RLock() 278 defer c.vLock.RUnlock() 279 return c.isSetInternal(key) 280 } 281 282 // isSetInternal checks if config is set for a key. Caller needs to hold a read lock on vLock. 283 func (c *Config) isSetInternal(key string) bool { 284 c.bindEnv(key) 285 return c.v.IsSet(key) 286 } 287 288 // Override Config by application or command line 289 290 // Set override existing config 291 func Set(key string, value interface{}) { 292 Default.Set(key, value) 293 } 294 295 // Set override existing config 296 func (c *Config) Set(key string, value interface{}) { 297 c.vLock.Lock() 298 c.v.Set(key, value) 299 c.vLock.Unlock() 300 c.onConfigChange() 301 } 302 303 func getReloadableMapKeys[T configTypes](v T, orderedKeys ...string) (string, string) { 304 k := fmt.Sprintf("%T:%s", v, strings.Join(orderedKeys, ",")) 305 return k, fmt.Sprintf("%s:%v", k, v) 306 } 307 308 func getOrCreatePointer[T configTypes]( 309 m map[string]any, dvs map[string]string, // this function MUST receive maps that are already initialized 310 lock *sync.RWMutex, defaultValue T, orderedKeys ...string, 311 ) (ptr *Reloadable[T], exists bool) { 312 key, dvKey := getReloadableMapKeys(defaultValue, orderedKeys...) 313 314 lock.Lock() 315 defer lock.Unlock() 316 317 defer func() { 318 if _, ok := dvs[key]; !ok { 319 dvs[key] = dvKey 320 } 321 if dvs[key] != dvKey { 322 panic(fmt.Errorf( 323 "Detected misuse of config variable registered with different default values %+v - %+v\n", 324 dvs[key], dvKey, 325 )) 326 } 327 }() 328 329 if p, ok := m[key]; ok { 330 return p.(*Reloadable[T]), true 331 } 332 333 p := &Reloadable[T]{} 334 m[key] = p 335 return p, false 336 } 337 338 // bindEnv handles rudder server's unique snake case replacement by registering 339 // the environment variables to viper, that would otherwise be ignored. 340 // Viper uppercases keys before sending them to its EnvKeyReplacer, thus 341 // the replacer cannot detect camelCase keys. 342 func (c *Config) bindEnv(key string) { 343 envVar := key 344 if !upperCaseMatch.MatchString(key) { 345 envVar = ConfigKeyToEnv(c.envPrefix, key) 346 } 347 // bind once 348 c.envsLock.RLock() 349 if _, ok := c.envs[key]; !ok { 350 c.envsLock.RUnlock() 351 c.envsLock.Lock() // don't really care about race here, setting the same value 352 c.envs[strings.ToUpper(key)] = envVar 353 c.envsLock.Unlock() 354 } else { 355 c.envsLock.RUnlock() 356 } 357 } 358 359 type envReplacer struct { 360 c *Config 361 } 362 363 func (r *envReplacer) Replace(s string) string { 364 r.c.envsLock.RLock() 365 defer r.c.envsLock.RUnlock() 366 if v, ok := r.c.envs[s]; ok { 367 return v 368 } 369 return s // bound environment variables 370 } 371 372 // Fallback environment variables supported (historically) by rudder-server 373 func bindLegacyEnv(v *viper.Viper) { 374 _ = v.BindEnv("DB.host", "JOBS_DB_HOST") 375 _ = v.BindEnv("DB.user", "JOBS_DB_USER") 376 _ = v.BindEnv("DB.name", "JOBS_DB_DB_NAME") 377 _ = v.BindEnv("DB.port", "JOBS_DB_PORT") 378 _ = v.BindEnv("DB.password", "JOBS_DB_PASSWORD") 379 _ = v.BindEnv("DB.sslMode", "JOBS_DB_SSL_MODE") 380 _ = v.BindEnv("SharedDB.dsn", "SHARED_DB_DSN") 381 }