diff --git a/README.md b/README.md index baa13a3..155d60b 100644 --- a/README.md +++ b/README.md @@ -2,80 +2,92 @@ [![release](https://github.com/steviebps/realm/actions/workflows/go.yml/badge.svg)](https://github.com/steviebps/realm/actions/workflows/go.yml) -```go install github.com/steviebps/realm``` - - -## starter configs - -### a basic chamber file -```wget -O $HOME/.realm/masterChamber.json https://raw.githubusercontent.com/steviebps/realm/master/configs/masterChamber.json``` - +```bash +go install github.com/steviebps/realm +``` ## example commands -### build -```realm build -o /path/to/your/directory``` - -with forced directory creation +### server -```realm build -o /path/to/your/directory --force``` - -### print - -#### Pretty prints your global chamber to stdout: -```realm print -p``` - -#### Print your global chamber to file: -```realm print -o /path/to/your/file.json``` +#### start a local realm server +```bash +realm server --config ./configs/realm.json +``` ## example code snippets ```go +package main + import ( + "context" + "encoding/json" "fmt" "log" "net/http" + "github.com/steviebps/realm/client" realm "github.com/steviebps/realm/pkg" ) +type CustomStruct struct { + Foo string `json:"foo,omitempty"` +} func main() { - // because realm configurations contain overrides based on the version of your application, specify it here - realm.SetVersion("v1.0.0") + var err error - // tell realm where to look for realm configuration - if err := realm.AddConfigPath("./"); err != nil { + // create a realm client for retrieving your chamber from your local or remote host + client, err := client.NewClient(&client.ClientConfig{Address: "http://localhost"}) + if err != nil { log.Fatal(err) } - // tell realm what file name it should look for in the specified paths - if err := realm.SetConfigName("chambers.json"); err != nil { + // initialize your realm + rlm, err := realm.NewRealm(realm.RealmOptions{Client: client, ApplicationVersion: "v1.0.0", Path: "root"}) + if err != nil { log.Fatal(err) } - // look for and read in the realm configuration - // passing "true" will tell realm to watch the file for changes - if err := realm.ReadInConfig(true); err != nil { + // start fetching your chamber from the local or remote host + err = rlm.Start() + if err != nil { log.Fatal(err) } - // return a float64 value from the config and specify a default value if it does not exist - port, _ := realm.Float64Value("port", 3000) - + // create a realm context + bootCtx := rlm.NewContext(context.Background()) + // retrieve your first config value + port, _ := rlm.Float64(bootCtx, "port", 3000) + mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // retrieve a string value from your realm config and specify a default value if it does not exist - message, _ := realm.StringValue("message", "DEFAULT") + // retrieve the message value with a new context + // note: use the same context value throughout the request for consistency + message, _ := rlm.String(rlm.NewContext(r.Context()), "message", "DEFAULT") w.Write([]byte(message)) }) - + + mux.HandleFunc("/custom", func(w http.ResponseWriter, r *http.Request) { + var custom *CustomStruct + // retrieve a custom value and unmarshal it + if err := rlm.CustomValue(rlm.NewContext(r.Context()), "custom", &custom); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(custom) + }) + log.Println("Listening on :", port) - err := http.ListenAndServe(fmt.Sprintf(":%d", int(port)), mux) + err = http.ListenAndServe(fmt.Sprintf(":%d", int(port)), mux) if err != nil { log.Fatal(err) } } ``` + diff --git a/cmd/print.go b/cmd/print.go deleted file mode 100644 index f44521d..0000000 --- a/cmd/print.go +++ /dev/null @@ -1,46 +0,0 @@ -package cmd - -import ( - "io" - "os" - - "github.com/spf13/cobra" - realm "github.com/steviebps/realm/pkg" - "github.com/steviebps/realm/utils" -) - -// printCmd represents the print command -var printCmd = &cobra.Command{ - Use: "print", - Short: "Print all chambers", - Long: "Print all chambers as they exist without inheritence", - Run: func(cmd *cobra.Command, args []string) { - realmCore := cmd.Context().Value("core").(*realm.Realm) - pretty, _ := cmd.Flags().GetBool("pretty") - output, _ := cmd.Flags().GetString("output") - - var w io.Writer = cmd.OutOrStdout() - var err error - - if output != "" { - w, err = os.OpenFile(output, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - realmCore.Logger().Info(err.Error()) - os.Exit(1) - } - } - - if err = utils.WriteInterfaceWith(w, globalChamber, pretty); err != nil { - realmCore.Logger().Info(err.Error()) - os.Exit(1) - } - - os.Exit(0) - }, -} - -func init() { - rootCmd.AddCommand(printCmd) - printCmd.Flags().BoolP("pretty", "p", false, "prints in pretty format") - printCmd.Flags().StringP("output", "o", "", "sets the output file of the printed content") -} diff --git a/cmd/root.go b/cmd/root.go index 1fddde7..6e9f5b4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,12 +6,8 @@ import ( "github.com/hashicorp/go-hclog" "github.com/spf13/cobra" - - realm "github.com/steviebps/realm/pkg" ) -var globalChamber = realm.Chamber{Toggles: map[string]*realm.OverrideableToggle{}} - // Version the version of realm var Version = "development" diff --git a/cmd/server.go b/cmd/server.go index baa3dd7..59a48df 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -85,7 +85,7 @@ var serverCmd = &cobra.Command{ logger.Error(fmt.Sprintf("storage type %q does not exist", storageType)) os.Exit(1) } - stg, err := strgCreator(serverConfig.StorageOptions, logger) + stg, err := strgCreator(serverConfig.StorageOptions) if err != nil { logger.Error(err.Error()) os.Exit(1) diff --git a/configs/masterChamber.json b/configs/chamber.json similarity index 100% rename from configs/masterChamber.json rename to configs/chamber.json diff --git a/configs/realm.json b/configs/realm.json index c2df87c..ed75f20 100644 --- a/configs/realm.json +++ b/configs/realm.json @@ -11,7 +11,10 @@ "options": { "path": "./.realm", "cache": "bigcache", - "source": "file" + "source": "file", + "life_window": "", + "shards": "", + "clean_window": "" } } -} \ No newline at end of file +} diff --git a/examples/go/main.go b/examples/go/main.go index d346395..3fcf70e 100644 --- a/examples/go/main.go +++ b/examples/go/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "log" @@ -30,18 +31,18 @@ func main() { log.Fatal(err) } - port, _ := rlm.Float64("port", 3000) + bootCtx := rlm.NewContext(context.Background()) + port, _ := rlm.Float64(bootCtx, "port", 3000) mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - message, _ := rlm.String("message", "DEFAULT") + message, _ := rlm.String(rlm.NewContext(r.Context()), "message", "DEFAULT") w.Write([]byte(message)) }) mux.HandleFunc("/custom", func(w http.ResponseWriter, r *http.Request) { var custom *CustomStruct - - if err := rlm.CustomValue("custom", &custom); err != nil { + if err := rlm.CustomValue(rlm.NewContext(r.Context()), "custom", &custom); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/go.mod b/go.mod index d5caab6..f29bf5d 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,12 @@ require ( require ( github.com/fatih/color v1.15.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect ) require ( github.com/hashicorp/go-hclog v1.5.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/sys v0.7.0 // indirect + golang.org/x/sys v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index 36efb84..3e8e7eb 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -39,8 +39,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/chamber.go b/pkg/chamber.go index e60f62f..63fc09d 100644 --- a/pkg/chamber.go +++ b/pkg/chamber.go @@ -1,7 +1,6 @@ package realm import ( - "encoding/json" "sync" ) @@ -56,16 +55,6 @@ func NewChamberEntry(c *Chamber, version string) *ChamberEntry { } } -// GetToggleValue returns the toggle with the specified toggleName at the specified version. -// Will return nil if the toggle does not exist -func (c *ChamberEntry) GetToggleValue(toggleName string) interface{} { - t := c.Get(toggleName) - if t == nil { - return nil - } - return t.GetValueAt(c.version) -} - // Get returns the toggle with the specified toggleName. // Will return nil if the toggle does not exist func (c *ChamberEntry) Get(toggleName string) *OverrideableToggle { @@ -79,40 +68,56 @@ func (c *ChamberEntry) Get(toggleName string) *OverrideableToggle { // StringValue retrieves a string by the key of the toggle // and returns the default value if it does not exist and a bool on whether or not the toggle exists -func (c *ChamberEntry) StringValue(toggleKey string, defaultValue string) (string, bool) { - cStr, ok := c.GetToggleValue(toggleKey).(string) +func (c *ChamberEntry) StringValue(toggleKey string, defaultValue string) (string, error) { + t := c.Get(toggleKey) + if t == nil { + return defaultValue, &ErrToggleNotFound{toggleKey} + } + v, ok := t.StringValue(c.version, defaultValue) if !ok { - return defaultValue, ok + return defaultValue, &ErrCouldNotConvertToggle{toggleKey, t.Type} } - return cStr, ok + return v, nil } // BoolValue retrieves a bool by the key of the toggle // and returns the default value if it does not exist and a bool on whether or not the toggle exists -func (c *ChamberEntry) BoolValue(toggleKey string, defaultValue bool) (bool, bool) { - cBool, ok := c.GetToggleValue(toggleKey).(bool) +func (c *ChamberEntry) BoolValue(toggleKey string, defaultValue bool) (bool, error) { + t := c.Get(toggleKey) + if t == nil { + return defaultValue, &ErrToggleNotFound{toggleKey} + } + v, ok := t.BoolValue(c.version, defaultValue) if !ok { - return defaultValue, ok + return defaultValue, &ErrCouldNotConvertToggle{toggleKey, t.Type} } - return cBool, ok + return v, nil } // Float64Value retrieves a float64 by the key of the toggle // and returns the default value if it does not exist and a bool on whether or not the toggle exists -func (c *ChamberEntry) Float64Value(toggleKey string, defaultValue float64) (float64, bool) { - cFloat64, ok := c.GetToggleValue(toggleKey).(float64) +func (c *ChamberEntry) Float64Value(toggleKey string, defaultValue float64) (float64, error) { + t := c.Get(toggleKey) + if t == nil { + return defaultValue, &ErrToggleNotFound{toggleKey} + } + v, ok := t.Float64Value(c.version, defaultValue) if !ok { - return defaultValue, ok + return defaultValue, &ErrCouldNotConvertToggle{toggleKey, t.Type} } - return cFloat64, ok + return v, nil } // CustomValue retrieves a json.RawMessage by the key of the toggle // and returns a bool on whether or not the toggle exists and is the proper type -func (c *ChamberEntry) CustomValue(toggleKey string, version string) (*json.RawMessage, bool) { - t, ok := c.GetToggleValue(toggleKey).(*json.RawMessage) - if !ok { - return nil, ok +func (c *ChamberEntry) CustomValue(toggleKey string, v any) error { + t := c.Get(toggleKey) + if t == nil { + return &ErrToggleNotFound{toggleKey} + } + err := t.CustomValue(c.version, v) + if err != nil { + return &ErrCouldNotConvertToggle{toggleKey, t.Type} } - return t, ok + return nil } diff --git a/pkg/chamber_test.go b/pkg/chamber_test.go index a5e59a5..5b0222f 100644 --- a/pkg/chamber_test.go +++ b/pkg/chamber_test.go @@ -1,6 +1,7 @@ package realm import ( + "encoding/json" "strconv" "sync" "testing" @@ -59,8 +60,8 @@ func TestInheritWith(t *testing.T) { } } -func BenchmarkStringValue(b *testing.B) { - m := make(map[string]*OverrideableToggle) +func BenchmarkChamberStringValue(b *testing.B) { + m := make(map[string]*OverrideableToggle, 100000) for i := 1; i < 10000; i++ { m[strconv.Itoa(i)] = &OverrideableToggle{Toggle: &Toggle{Type: "string", Value: "string"}} } @@ -72,14 +73,16 @@ func BenchmarkStringValue(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - chamber.StringValue("1", "") + _, err := chamber.StringValue("1", "") + if err != nil { + b.Errorf("should not be failing benchmark with error: %v", err) + } } }) } -func BenchmarkBoolValue(b *testing.B) { - - m := make(map[string]*OverrideableToggle) +func BenchmarkChamberBoolValue(b *testing.B) { + m := make(map[string]*OverrideableToggle, 100000) for i := 1; i < 100000; i++ { m[strconv.Itoa(i)] = &OverrideableToggle{Toggle: &Toggle{Type: "boolean", Value: false}} } @@ -90,7 +93,57 @@ func BenchmarkBoolValue(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - chamber.BoolValue("1", false) + _, err := chamber.BoolValue("1", false) + if err != nil { + b.Errorf("should not be failing benchmark with error: %v", err) + } + } + }) +} + +func BenchmarkChamberFloat64Value(b *testing.B) { + m := make(map[string]*OverrideableToggle, 100000) + for i := 1; i < 100000; i++ { + m[strconv.Itoa(i)] = &OverrideableToggle{Toggle: &Toggle{Type: "number", Value: float64(10)}} + } + chamber := NewChamberEntry(&Chamber{ + Toggles: m, + }, "") + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, err := chamber.Float64Value("1", 100) + if err != nil { + b.Errorf("should not be failing benchmark with error: %v", err) + } + } + }) +} + +func BenchmarkChamberCustomValue(b *testing.B) { + type CustomStruct struct { + Test string + } + + m := make(map[string]*OverrideableToggle, 100000) + for i := 0; i < 100000; i++ { + raw := json.RawMessage(`{"Test":"test"}`) + m[strconv.Itoa(i)] = &OverrideableToggle{Toggle: &Toggle{Type: "custom", Value: &raw}} + } + + chamber := NewChamberEntry(&Chamber{ + Toggles: m, + }, "") + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var v CustomStruct + err := chamber.CustomValue("1", &v) + if err != nil { + b.Errorf("should not be failing benchmark with error: %v", err) + } } }) } diff --git a/pkg/realm.go b/pkg/realm.go index 8fe3802..61100d5 100644 --- a/pkg/realm.go +++ b/pkg/realm.go @@ -1,6 +1,7 @@ package realm import ( + "context" "encoding/json" "errors" "fmt" @@ -38,6 +39,16 @@ const ( DefaultRefreshInterval time.Duration = 15 * time.Minute ) +type contextKey struct { + name string +} + +var ( + // RequestContextKey is a context key. It is used to store the root chamber of the current context + // such that toggle retrievals will be consistent throughout the consumers request + RequestContextKey = &contextKey{"realm"} +) + // NewRealm returns a new Realm struct that carries out all of the core features func NewRealm(options RealmOptions) (*Realm, error) { if options.Client == nil { @@ -154,74 +165,60 @@ func (rlm *Realm) getChamber() *ChamberEntry { return rlm.root } +func (rlm *Realm) getChamberFromContext(ctx context.Context) *ChamberEntry { + cChamber, ok := ctx.Value(RequestContextKey).(*ChamberEntry) + if ok && cChamber != nil { + return cChamber + } + return rlm.getChamber() +} + +func (rlm *Realm) NewContext(ctx context.Context) context.Context { + c := rlm.getChamber() + ctx = context.WithValue(ctx, RequestContextKey, c) + return ctx +} + // Bool retrieves a bool by the key of the toggle. // Returns the default value if it does not exist and a bool on whether or not the toggle exists with that type -func (rlm *Realm) Bool(toggleKey string, defaultValue bool) (bool, error) { - c := rlm.getChamber() +func (rlm *Realm) Bool(ctx context.Context, toggleKey string, defaultValue bool) (bool, error) { + c := rlm.getChamberFromContext(ctx) if c == nil { return defaultValue, ErrChamberEmpty } - t := c.Get(toggleKey) - if t == nil { - return defaultValue, &ErrToggleNotFound{toggleKey} - } - v, ok := t.GetValueAt(rlm.applicationVersion).(bool) - if !ok { - return defaultValue, &ErrCouldNotConvertToggle{toggleKey, t.Type} - } - return v, nil + return c.BoolValue(toggleKey, defaultValue) } // String retrieves a string by the key of the toggle. // Returns the default value if it does not exist and a bool on whether or not the toggle exists with that type -func (rlm *Realm) String(toggleKey string, defaultValue string) (string, error) { - c := rlm.getChamber() +func (rlm *Realm) String(ctx context.Context, toggleKey string, defaultValue string) (string, error) { + c := rlm.getChamberFromContext(ctx) if c == nil { return defaultValue, ErrChamberEmpty } - t := c.Get(toggleKey) - if t == nil { - return defaultValue, &ErrToggleNotFound{toggleKey} - } - v, ok := t.GetValueAt(rlm.applicationVersion).(string) - if !ok { - return defaultValue, &ErrCouldNotConvertToggle{toggleKey, t.Type} - } - return v, nil + return c.StringValue(toggleKey, defaultValue) } // Float64 retrieves a float64 by the key of the toggle. // Returns the default value if it does not exist and a bool on whether or not the toggle exists with that type -func (rlm *Realm) Float64(toggleKey string, defaultValue float64) (float64, error) { - c := rlm.getChamber() +func (rlm *Realm) Float64(ctx context.Context, toggleKey string, defaultValue float64) (float64, error) { + c := rlm.getChamberFromContext(ctx) if c == nil { return defaultValue, ErrChamberEmpty } - t := c.Get(toggleKey) - if t == nil { - return defaultValue, &ErrToggleNotFound{toggleKey} - } - v, ok := t.GetValueAt(rlm.applicationVersion).(float64) - if !ok { - return defaultValue, &ErrCouldNotConvertToggle{toggleKey, t.Type} - } - return v, nil + return c.Float64Value(toggleKey, defaultValue) } // CustomValue retrieves an arbitrary value by the key of the toggle // and unmarshals the value into the custom value v -func (rlm *Realm) CustomValue(toggleKey string, v any) error { - c := rlm.getChamber() +func (rlm *Realm) CustomValue(ctx context.Context, toggleKey string, v any) error { + c := rlm.getChamberFromContext(ctx) if c == nil { return ErrChamberEmpty } - t := c.Get(toggleKey) - if t == nil { - return &ErrToggleNotFound{toggleKey} - } - raw, ok := t.GetValueAt(rlm.applicationVersion).(*json.RawMessage) - if !ok { - return fmt.Errorf("could not convert custom toggle %q: it is of type %q", toggleKey, t.Type) + err := c.CustomValue(toggleKey, v) + if err != nil { + return fmt.Errorf("could not convert custom toggle %q: %w", toggleKey, err) } - return json.Unmarshal(*raw, v) + return nil } diff --git a/pkg/storage/bigcache.go b/pkg/storage/bigcache.go index 4c0725c..869ac00 100644 --- a/pkg/storage/bigcache.go +++ b/pkg/storage/bigcache.go @@ -16,14 +16,13 @@ import ( type BigCacheStorage struct { underlying *bigcache.BigCache - logger hclog.Logger } var ( _ Storage = (*BigCacheStorage)(nil) ) -func NewBigCacheStorage(config map[string]string, logger hclog.Logger) (Storage, error) { +func NewBigCacheStorage(config map[string]string) (Storage, error) { // defaults var shards int = 64 lifeWindow := int64(2 * time.Minute) @@ -73,12 +72,12 @@ func NewBigCacheStorage(config map[string]string, logger hclog.Logger) (Storage, return &BigCacheStorage{ underlying: cache, - logger: logger.Named("bigcache"), }, nil } func (f *BigCacheStorage) Get(ctx context.Context, logicalPath string) (*StorageEntry, error) { - f.logger.Debug("get operation", "logicalPath", logicalPath) + logger := hclog.FromContext(ctx).ResetNamed("bigcache") + logger.Debug("get operation", "logicalPath", logicalPath) if err := ValidatePath(logicalPath); err != nil { return nil, err @@ -103,7 +102,8 @@ func (f *BigCacheStorage) Get(ctx context.Context, logicalPath string) (*Storage } func (f *BigCacheStorage) Put(ctx context.Context, e StorageEntry) error { - f.logger.Debug("put operation", "logicalPath", e.Key) + logger := hclog.FromContext(ctx).ResetNamed("bigcache") + logger.Debug("put operation", "logicalPath", e.Key) if err := ValidatePath(e.Key); err != nil { return err @@ -120,7 +120,8 @@ func (f *BigCacheStorage) Put(ctx context.Context, e StorageEntry) error { } func (f *BigCacheStorage) Delete(ctx context.Context, logicalPath string) error { - f.logger.Debug("delete operation", "logicalPath", logicalPath) + logger := hclog.FromContext(ctx).ResetNamed("bigcache") + logger.Debug("delete operation", "logicalPath", logicalPath) if err := ValidatePath(logicalPath); err != nil { return err @@ -137,7 +138,8 @@ func (f *BigCacheStorage) Delete(ctx context.Context, logicalPath string) error } func (f *BigCacheStorage) List(ctx context.Context, prefix string) ([]string, error) { - f.logger.Debug("list operation", "prefix", prefix) + logger := hclog.FromContext(ctx).ResetNamed("bigcache") + logger.Debug("list operation", "prefix", prefix) if err := ValidatePath(prefix); err != nil { return nil, err @@ -148,7 +150,7 @@ func (f *BigCacheStorage) List(ctx context.Context, prefix string) ([]string, er for iterator.SetNext() { record, err := iterator.Value() if err != nil { - f.logger.Error(err.Error()) + logger.Error(err.Error()) return names, err } key := record.Key() diff --git a/pkg/storage/cacheable.go b/pkg/storage/cacheable.go index 349ecac..30677e8 100644 --- a/pkg/storage/cacheable.go +++ b/pkg/storage/cacheable.go @@ -14,7 +14,6 @@ import ( type CacheableStorage struct { cache Storage source Storage - logger hclog.Logger } var ( @@ -22,7 +21,7 @@ var ( ) // NewCacheableStorage returns a write-through cacheable storage. -func NewCacheableStorage(cache Storage, source Storage, logger hclog.Logger) (*CacheableStorage, error) { +func NewCacheableStorage(cache Storage, source Storage) (*CacheableStorage, error) { if cache == nil || source == nil { return nil, fmt.Errorf("storage cannot be nil") } @@ -30,11 +29,10 @@ func NewCacheableStorage(cache Storage, source Storage, logger hclog.Logger) (*C return &CacheableStorage{ cache: cache, source: source, - logger: logger.Named("cacheable"), }, nil } -func NewCacheableStorageWithConf(conf map[string]string, logger hclog.Logger) (Storage, error) { +func NewCacheableStorageWithConf(conf map[string]string) (Storage, error) { cache, ok := conf["cache"] if !ok || cache == "" { cache = "bigcache" @@ -48,7 +46,7 @@ func NewCacheableStorageWithConf(conf map[string]string, logger hclog.Logger) (S if !exists { return nil, fmt.Errorf("storage type %q does not exist", source) } - srcStg, err := sourceCreator(conf, logger) + srcStg, err := sourceCreator(conf) if err != nil { return nil, err } @@ -57,23 +55,24 @@ func NewCacheableStorageWithConf(conf map[string]string, logger hclog.Logger) (S if !exists { return nil, fmt.Errorf("storage type %q does not exist", cache) } - cacheStg, err := cacheCreator(conf, logger) + cacheStg, err := cacheCreator(conf) if err != nil { return nil, err } - return NewCacheableStorage(cacheStg, srcStg, logger) + return NewCacheableStorage(cacheStg, srcStg) } func (c *CacheableStorage) Get(ctx context.Context, logicalPath string) (*StorageEntry, error) { - c.logger.Debug("get operation", "logicalPath", logicalPath) + logger := hclog.FromContext(ctx).ResetNamed("cacheable") + logger.Debug("get operation", "logicalPath", logicalPath) entry, err := c.cache.Get(ctx, logicalPath) if err != nil { var nfError *NotFoundError // cache layer is expected to have missing records so let's only log other errors if !errors.As(err, &nfError) { - c.logger.Error("cache", "error", err.Error()) + logger.Error("cache", "error", err.Error()) } } @@ -83,7 +82,7 @@ func (c *CacheableStorage) Get(ctx context.Context, logicalPath string) (*Storag entry, err = c.source.Get(ctx, logicalPath) if err != nil { - c.logger.Error("source", "error", err.Error()) + logger.Error("source", "error", err.Error()) return nil, err } @@ -95,7 +94,8 @@ func (c *CacheableStorage) Get(ctx context.Context, logicalPath string) (*Storag } func (c *CacheableStorage) Put(ctx context.Context, e StorageEntry) error { - c.logger.Debug("put operation", "logicalPath", e.Key) + logger := hclog.FromContext(ctx).ResetNamed("cacheable") + logger.Debug("put operation", "logicalPath", e.Key) err := c.source.Put(ctx, e) if err == nil { @@ -106,7 +106,8 @@ func (c *CacheableStorage) Put(ctx context.Context, e StorageEntry) error { } func (c *CacheableStorage) Delete(ctx context.Context, logicalPath string) error { - c.logger.Debug("delete operation", "logicalPath", logicalPath) + logger := hclog.FromContext(ctx).ResetNamed("cacheable") + logger.Debug("delete operation", "logicalPath", logicalPath) err := c.source.Delete(ctx, logicalPath) if err == nil { @@ -117,6 +118,7 @@ func (c *CacheableStorage) Delete(ctx context.Context, logicalPath string) error } func (c *CacheableStorage) List(ctx context.Context, prefix string) ([]string, error) { - c.logger.Debug("list operation", "prefix", prefix) + logger := hclog.FromContext(ctx).ResetNamed("cacheable") + logger.Debug("list operation", "prefix", prefix) return c.source.List(ctx, prefix) } diff --git a/pkg/storage/file.go b/pkg/storage/file.go index 02f0d22..6805ca0 100644 --- a/pkg/storage/file.go +++ b/pkg/storage/file.go @@ -14,27 +14,26 @@ import ( ) type FileStorage struct { - logger hclog.Logger - path string + path string } var ( _ Storage = (*FileStorage)(nil) ) -func NewFileStorage(conf map[string]string, logger hclog.Logger) (Storage, error) { +func NewFileStorage(conf map[string]string) (Storage, error) { if conf["path"] == "" { return nil, fmt.Errorf("'path' must be set") } return &FileStorage{ - logger: logger.Named("file"), - path: conf["path"], + path: conf["path"], }, nil } func (f *FileStorage) Get(ctx context.Context, logicalPath string) (*StorageEntry, error) { - f.logger.Debug("get operation", "logicalPath", logicalPath) + logger := hclog.FromContext(ctx).ResetNamed("file") + logger.Debug("get operation", "logicalPath", logicalPath) if err := ValidatePath(logicalPath); err != nil { return nil, err @@ -67,7 +66,8 @@ func (f *FileStorage) Get(ctx context.Context, logicalPath string) (*StorageEntr } func (f *FileStorage) Put(ctx context.Context, e StorageEntry) error { - f.logger.Debug("put operation", "logicalPath", e.Key) + logger := hclog.FromContext(ctx).ResetNamed("file") + logger.Debug("put operation", "logicalPath", e.Key) if err := ValidatePath(e.Key); err != nil { return err @@ -97,7 +97,8 @@ func (f *FileStorage) Put(ctx context.Context, e StorageEntry) error { } func (f *FileStorage) Delete(ctx context.Context, logicalPath string) error { - f.logger.Debug("delete operation", "logicalPath", logicalPath) + logger := hclog.FromContext(ctx).ResetNamed("file") + logger.Debug("delete operation", "logicalPath", logicalPath) if err := ValidatePath(logicalPath); err != nil { return err @@ -121,7 +122,8 @@ func (f *FileStorage) Delete(ctx context.Context, logicalPath string) error { } func (f *FileStorage) List(ctx context.Context, prefix string) ([]string, error) { - f.logger.Debug("list operation", "prefix", prefix) + logger := hclog.FromContext(ctx).ResetNamed("file") + logger.Debug("list operation", "prefix", prefix) if err := ValidatePath(prefix); err != nil { return nil, err diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 17a2ccf..4d7b8bc 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -5,8 +5,6 @@ import ( "encoding/json" "errors" "strings" - - "github.com/hashicorp/go-hclog" ) type StorageEntry struct { @@ -21,7 +19,7 @@ type Storage interface { List(ctx context.Context, prefix string) ([]string, error) } -type StorageCreator func(conf map[string]string, logger hclog.Logger) (Storage, error) +type StorageCreator func(conf map[string]string) (Storage, error) var StorageOptions = map[string]StorageCreator{ "file": NewFileStorage, diff --git a/pkg/toggle.go b/pkg/toggle.go index aa3effb..c4d5bf2 100644 --- a/pkg/toggle.go +++ b/pkg/toggle.go @@ -131,3 +131,42 @@ func (t *OverrideableToggle) GetValueAt(version string) interface{} { return v } + +// StringValue retrieves a string value of the toggle +// and returns the default value if it does not exist and a bool on whether or not the toggle exists +func (t *OverrideableToggle) StringValue(version string, defaultValue string) (string, bool) { + v, ok := t.GetValueAt(version).(string) + if !ok { + return defaultValue, ok + } + return v, ok +} + +// BoolValue retrieves a bool value of the toggle +// and returns the default value if it does not exist and a bool on whether or not the toggle exists +func (t *OverrideableToggle) BoolValue(version string, defaultValue bool) (bool, bool) { + v, ok := t.GetValueAt(version).(bool) + if !ok { + return defaultValue, ok + } + return v, ok +} + +// Float64Value retrieves a float64 value of the toggle +// and returns the default value if it does not exist and a bool on whether or not the toggle exists +func (t *OverrideableToggle) Float64Value(version string, defaultValue float64) (float64, bool) { + v, ok := t.GetValueAt(version).(float64) + if !ok { + return defaultValue, ok + } + return v, ok +} + +// CustomValue unmarshals v into the value of the toggle +func (t *OverrideableToggle) CustomValue(version string, v any) error { + raw, ok := t.GetValueAt(version).(*json.RawMessage) + if !ok { + return fmt.Errorf("toggle with type %q could not be converted for unmarshalling", t.Type) + } + return json.Unmarshal(*raw, v) +} diff --git a/pkg/toggle_test.go b/pkg/toggle_test.go index c3b890e..0d08d9f 100644 --- a/pkg/toggle_test.go +++ b/pkg/toggle_test.go @@ -6,7 +6,6 @@ import ( ) func TestAssertType(t *testing.T) { - tests := []struct { assertedType string input json.RawMessage @@ -28,3 +27,78 @@ func TestAssertType(t *testing.T) { } } } + +func TestGetValueAt(t *testing.T) { + tests := []struct { + version string + output string + }{ + {"", "default"}, + {"v1.0.0-pre.0", "default"}, + {"v1.0.0", "override1"}, + {"v1.0.1", "override1"}, + {"v1.0.2-pre.0", "override2"}, + {"v1.0.2", "override2"}, + {"v1.0.3-pre.0", "default"}, + } + toggle := &OverrideableToggle{Toggle: &Toggle{Type: "string", Value: "default"}, Overrides: []*Override{{Toggle: &Toggle{Type: "string", Value: "override1"}, MinimumVersion: "v1.0.0", MaximumVersion: "v1.0.1"}, {Toggle: &Toggle{Type: "string", Value: "override2"}, MinimumVersion: "v1.0.1", MaximumVersion: "v1.0.2"}}} + + for _, test := range tests { + val := toggle.GetValueAt(test.version) + if val != test.output { + t.Errorf("version: %q should return %q but returned %q", test.version, test.output, val) + } + } +} + +func BenchmarkToggleStringValue(b *testing.B) { + t := &OverrideableToggle{Toggle: &Toggle{Type: "string", Value: "string"}} + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + t.StringValue("v1.0.0", "") + } + }) +} + +func BenchmarkToggleBoolValue(b *testing.B) { + t := &OverrideableToggle{Toggle: &Toggle{Type: "boolean", Value: false}} + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + t.BoolValue("v1.0.0", false) + } + }) +} + +func BenchmarkToggleFloat64Value(b *testing.B) { + t := &OverrideableToggle{Toggle: &Toggle{Type: "number", Value: float64(10)}} + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + t.Float64Value("v1.0.0", 15) + } + }) +} + +func BenchmarkToggleCustomValue(b *testing.B) { + type CustomStruct struct { + Test string + } + raw := json.RawMessage(`{"Test":"test"}`) + toggle := &OverrideableToggle{Toggle: &Toggle{Type: "custom", Value: &raw}} + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + var v CustomStruct + err := toggle.CustomValue("v1.0.0", &v) + if err != nil { + b.Errorf("something went wrong: %v", err) + } + } + }) +}