diff --git a/config.go b/config.go index b1484e1..28355a9 100644 --- a/config.go +++ b/config.go @@ -2,15 +2,18 @@ package config import ( "encoding/base64" - "fmt" + "log" "os" "reflect" "strings" + "sync" "github.com/creasty/defaults" + "github.com/fsnotify/fsnotify" "github.com/go-playground/validator/v10" "github.com/iamolegga/enviper" "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -41,6 +44,7 @@ type ConfReader struct { configDirs []string envVarPrefix string Verbose bool + configStruct any } // NewConfReader creates new instance of ConfReader @@ -70,8 +74,8 @@ func (c *ConfReader) Read(configStruct interface{}) error { return errors.Wrap(err, "failed to set default values") } - //jww.SetLogThreshold(jww.LevelTrace) - //jww.SetStdoutThreshold(jww.LevelTrace) + jww.SetLogThreshold(jww.LevelTrace) + jww.SetStdoutThreshold(jww.LevelTrace) c.viper.SetConfigFile(c.configName) @@ -113,6 +117,8 @@ func (c *ConfReader) Read(configStruct interface{}) error { } return err } + + c.configStruct = configStruct return nil } @@ -225,7 +231,7 @@ type flagInfo struct { func (c *ConfReader) dumpStruct(t reflect.Type, path string, res map[string]*flagInfo) map[string]*flagInfo { if c.Verbose { - fmt.Printf("%s: %s", path, t.Name()) + log.Printf("%s: %s", path, t.Name()) } switch t.Kind() { case reflect.Ptr: @@ -282,8 +288,32 @@ func (c *ConfReader) WithSearchDirs(s ...string) *ConfReader { return c } -// WithPrefix sets the prefix for environment variables +// WithPrefix sets the prefix for environment variables. It adds '_' to the end of the prefix. +// For example, if prefix is "MYAPP", then environment variable for field "Name" will be "MYAPP_NAME". func (c *ConfReader) WithPrefix(prefix string) *ConfReader { c.envVarPrefix = prefix return c } + +// Watch watches for config changes and reloads config. This method should be called after Read() to make sure that ConfReader konws which struct to reload. +// Returns a mutex that can be used to synchronize access to the config. +// If you care about thread safety, call RLock() on the mutex while accessing the config and the RUnlock(). +// This will ensure that the config is not reloaded while you are accessing it. +func (c *ConfReader) Watch() *sync.RWMutex { + if c.configStruct == nil { + log.Fatalln("ConfReader: config struct is not set. Call Read before Watch") + } + rwmutex := &sync.RWMutex{} + + c.viper.WatchConfig() + c.viper.OnConfigChange(func(e fsnotify.Event) { + rwmutex.Lock() + defer rwmutex.Unlock() + err := c.Read(c.configStruct) + if err != nil { + log.Printf("failed to reload config: %s\n", err) + } + + }) + return rwmutex +} diff --git a/config_test.go b/config_test.go index 709a471..0c6a836 100644 --- a/config_test.go +++ b/config_test.go @@ -121,6 +121,51 @@ func Test_ReadFromJsonFile(t *testing.T) { } } +func Test_WatchWithFile(t *testing.T) { + resetFlags() + nc := &FullConfig{} + + err := os.Mkdir("testdata/tmp", 0755) + if err != nil { + t.Fatal(err) + } + err = os.WriteFile("testdata/tmp/changing_file.json", []byte(`{"verbose":"true"}`), 0644) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll("testdata/tmp") + confReader := NewConfReader("changing_file") + confReader.configDirs = []string{"testdata/tmp"} + err = confReader.Read(nc) + if assert.NoError(t, err) { + assert.Equal(t, true, nc.Verbose) + } + mutex := confReader.Watch() + + t.Run("configChanges", func(t *testing.T) { + err = os.WriteFile("testdata/tmp/changing_file.json", []byte(`{"verbose":"false"}`), 0644) + if assert.NoError(t, err) { + time.Sleep(10 * time.Millisecond) + assert.Equal(t, false, nc.Verbose) + } + }) + + t.Run("configStructLock", func(t *testing.T) { + mutex.RLock() + err = os.WriteFile("testdata/tmp/changing_file.json", []byte(`{"verbose":"true"}`), 0644) + if err != nil { + t.Fatal(err) + } + + //time.Sleep(time.Second) + assert.Equal(t, false, nc.Verbose) + + mutex.RUnlock() + time.Sleep(10 * time.Millisecond) + assert.Equal(t, true, nc.Verbose) + }) +} + type dmParent struct { GlobalConfig `mapstructure:",squash"` Conf dmSibling `flag:"notAllowed"` diff --git a/go.mod b/go.mod index 32c03dd..562267d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/num30/config -go 1.18 +go 1.19 require ( github.com/go-playground/validator/v10 v10.10.1