Skip to content

Commit

Permalink
Watch method to track config file changes (#15)
Browse files Browse the repository at this point in the history
* Watch to track config file changes

* Check missed error in test
  • Loading branch information
DenisPalnitsky authored Aug 6, 2023
1 parent d717df0 commit 8fd47a5
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 6 deletions.
40 changes: 35 additions & 5 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -41,6 +44,7 @@ type ConfReader struct {
configDirs []string
envVarPrefix string
Verbose bool
configStruct any
}

// NewConfReader creates new instance of ConfReader
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -113,6 +117,8 @@ func (c *ConfReader) Read(configStruct interface{}) error {
}
return err
}

c.configStruct = configStruct
return nil
}

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
}
45 changes: 45 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit 8fd47a5

Please sign in to comment.