Skip to content

Commit 8fd47a5

Browse files
Watch method to track config file changes (#15)
* Watch to track config file changes * Check missed error in test
1 parent d717df0 commit 8fd47a5

File tree

3 files changed

+81
-6
lines changed

3 files changed

+81
-6
lines changed

config.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ package config
22

33
import (
44
"encoding/base64"
5-
"fmt"
5+
"log"
66
"os"
77
"reflect"
88
"strings"
9+
"sync"
910

1011
"github.com/creasty/defaults"
12+
"github.com/fsnotify/fsnotify"
1113
"github.com/go-playground/validator/v10"
1214
"github.com/iamolegga/enviper"
1315
"github.com/pkg/errors"
16+
jww "github.com/spf13/jwalterweatherman"
1417
"github.com/spf13/pflag"
1518
"github.com/spf13/viper"
1619
)
@@ -41,6 +44,7 @@ type ConfReader struct {
4144
configDirs []string
4245
envVarPrefix string
4346
Verbose bool
47+
configStruct any
4448
}
4549

4650
// NewConfReader creates new instance of ConfReader
@@ -70,8 +74,8 @@ func (c *ConfReader) Read(configStruct interface{}) error {
7074
return errors.Wrap(err, "failed to set default values")
7175
}
7276

73-
//jww.SetLogThreshold(jww.LevelTrace)
74-
//jww.SetStdoutThreshold(jww.LevelTrace)
77+
jww.SetLogThreshold(jww.LevelTrace)
78+
jww.SetStdoutThreshold(jww.LevelTrace)
7579

7680
c.viper.SetConfigFile(c.configName)
7781

@@ -113,6 +117,8 @@ func (c *ConfReader) Read(configStruct interface{}) error {
113117
}
114118
return err
115119
}
120+
121+
c.configStruct = configStruct
116122
return nil
117123
}
118124

@@ -225,7 +231,7 @@ type flagInfo struct {
225231

226232
func (c *ConfReader) dumpStruct(t reflect.Type, path string, res map[string]*flagInfo) map[string]*flagInfo {
227233
if c.Verbose {
228-
fmt.Printf("%s: %s", path, t.Name())
234+
log.Printf("%s: %s", path, t.Name())
229235
}
230236
switch t.Kind() {
231237
case reflect.Ptr:
@@ -282,8 +288,32 @@ func (c *ConfReader) WithSearchDirs(s ...string) *ConfReader {
282288
return c
283289
}
284290

285-
// WithPrefix sets the prefix for environment variables
291+
// WithPrefix sets the prefix for environment variables. It adds '_' to the end of the prefix.
292+
// For example, if prefix is "MYAPP", then environment variable for field "Name" will be "MYAPP_NAME".
286293
func (c *ConfReader) WithPrefix(prefix string) *ConfReader {
287294
c.envVarPrefix = prefix
288295
return c
289296
}
297+
298+
// 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.
299+
// Returns a mutex that can be used to synchronize access to the config.
300+
// If you care about thread safety, call RLock() on the mutex while accessing the config and the RUnlock().
301+
// This will ensure that the config is not reloaded while you are accessing it.
302+
func (c *ConfReader) Watch() *sync.RWMutex {
303+
if c.configStruct == nil {
304+
log.Fatalln("ConfReader: config struct is not set. Call Read before Watch")
305+
}
306+
rwmutex := &sync.RWMutex{}
307+
308+
c.viper.WatchConfig()
309+
c.viper.OnConfigChange(func(e fsnotify.Event) {
310+
rwmutex.Lock()
311+
defer rwmutex.Unlock()
312+
err := c.Read(c.configStruct)
313+
if err != nil {
314+
log.Printf("failed to reload config: %s\n", err)
315+
}
316+
317+
})
318+
return rwmutex
319+
}

config_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,51 @@ func Test_ReadFromJsonFile(t *testing.T) {
121121
}
122122
}
123123

124+
func Test_WatchWithFile(t *testing.T) {
125+
resetFlags()
126+
nc := &FullConfig{}
127+
128+
err := os.Mkdir("testdata/tmp", 0755)
129+
if err != nil {
130+
t.Fatal(err)
131+
}
132+
err = os.WriteFile("testdata/tmp/changing_file.json", []byte(`{"verbose":"true"}`), 0644)
133+
if err != nil {
134+
t.Fatal(err)
135+
}
136+
defer os.RemoveAll("testdata/tmp")
137+
confReader := NewConfReader("changing_file")
138+
confReader.configDirs = []string{"testdata/tmp"}
139+
err = confReader.Read(nc)
140+
if assert.NoError(t, err) {
141+
assert.Equal(t, true, nc.Verbose)
142+
}
143+
mutex := confReader.Watch()
144+
145+
t.Run("configChanges", func(t *testing.T) {
146+
err = os.WriteFile("testdata/tmp/changing_file.json", []byte(`{"verbose":"false"}`), 0644)
147+
if assert.NoError(t, err) {
148+
time.Sleep(10 * time.Millisecond)
149+
assert.Equal(t, false, nc.Verbose)
150+
}
151+
})
152+
153+
t.Run("configStructLock", func(t *testing.T) {
154+
mutex.RLock()
155+
err = os.WriteFile("testdata/tmp/changing_file.json", []byte(`{"verbose":"true"}`), 0644)
156+
if err != nil {
157+
t.Fatal(err)
158+
}
159+
160+
//time.Sleep(time.Second)
161+
assert.Equal(t, false, nc.Verbose)
162+
163+
mutex.RUnlock()
164+
time.Sleep(10 * time.Millisecond)
165+
assert.Equal(t, true, nc.Verbose)
166+
})
167+
}
168+
124169
type dmParent struct {
125170
GlobalConfig `mapstructure:",squash"`
126171
Conf dmSibling `flag:"notAllowed"`

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/num30/config
22

3-
go 1.18
3+
go 1.19
44

55
require (
66
github.com/go-playground/validator/v10 v10.10.1

0 commit comments

Comments
 (0)