Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Watch method to track config file changes #15

Merged
merged 2 commits into from
Aug 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading