diff --git a/cmd/relayproxy/controller/all_flags_test.go b/cmd/relayproxy/controller/all_flags_test.go index f371d4ac700..5b9fdf45d16 100644 --- a/cmd/relayproxy/controller/all_flags_test.go +++ b/cmd/relayproxy/controller/all_flags_test.go @@ -4,7 +4,7 @@ import ( "context" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric" "io" - "log" + "log/slog" "net/http" "net/http/httptest" "os" @@ -81,7 +81,7 @@ func Test_all_flag_Handler(t *testing.T) { // init go-feature-flag goFF, _ := ffclient.New(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: tt.args.configFlagsLocation, diff --git a/cmd/relayproxy/controller/collect_eval_data_test.go b/cmd/relayproxy/controller/collect_eval_data_test.go index fd2cba0a8a7..a3652778aa0 100644 --- a/cmd/relayproxy/controller/collect_eval_data_test.go +++ b/cmd/relayproxy/controller/collect_eval_data_test.go @@ -3,7 +3,7 @@ package controller_test import ( "context" "io" - "log" + "log/slog" "net/http" "net/http/httptest" "os" @@ -96,7 +96,7 @@ func Test_collect_eval_data_Handler(t *testing.T) { // init go-feature-flag goFF, _ := ffclient.New(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: configFlagsLocation, diff --git a/cmd/relayproxy/controller/flag_eval_test.go b/cmd/relayproxy/controller/flag_eval_test.go index 2fdcaf90a01..28f0c481f82 100644 --- a/cmd/relayproxy/controller/flag_eval_test.go +++ b/cmd/relayproxy/controller/flag_eval_test.go @@ -5,7 +5,7 @@ import ( "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric" "io" "io/ioutil" - "log" + "log/slog" "net/http" "net/http/httptest" "os" @@ -149,7 +149,7 @@ func Test_flag_eval_Handler(t *testing.T) { // init go-feature-flag goFF, _ := ffclient.New(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: configFlagsLocation, diff --git a/cmd/relayproxy/ofrep/configuration_test.go b/cmd/relayproxy/ofrep/configuration_test.go index 548f53f4530..383f2489f87 100644 --- a/cmd/relayproxy/ofrep/configuration_test.go +++ b/cmd/relayproxy/ofrep/configuration_test.go @@ -1,7 +1,6 @@ package ofrep_test import ( - "fmt" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" ffclient "github.com/thomaspoignant/go-feature-flag" @@ -63,7 +62,6 @@ func Test_Configuration(t *testing.T) { req := httptest.NewRequest(echo.GET, "/ofrep/v1/configuration", nil) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) e.ServeHTTP(rec, req) - fmt.Println(rec.Body.String()) assert.Equal(t, tt.want.httpCode, rec.Code, "Invalid HTTP Code") assert.JSONEq(t, tt.want.response, rec.Body.String(), "Invalid response wantBody") }) diff --git a/cmd/relayproxy/ofrep/evaluate_test.go b/cmd/relayproxy/ofrep/evaluate_test.go index ca2ea07b0de..50aa235c203 100644 --- a/cmd/relayproxy/ofrep/evaluate_test.go +++ b/cmd/relayproxy/ofrep/evaluate_test.go @@ -2,11 +2,10 @@ package ofrep_test import ( "context" - "fmt" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/ofrep" "io" - "log" + "log/slog" "net/http" "net/http/httptest" "os" @@ -92,7 +91,7 @@ func Test_Bulk_Evaluation(t *testing.T) { // init go-feature-flag goFF, _ := ffclient.New(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: tt.args.configFlagsLocation, @@ -138,8 +137,6 @@ func Test_Bulk_Evaluation(t *testing.T) { wantBody, err := os.ReadFile(tt.want.bodyFile) - fmt.Println(rec.Header()) - assert.NoError(t, err, "Impossible the expected wantBody file %s", tt.want.bodyFile) assert.Equal(t, tt.want.httpCode, rec.Code, "Invalid HTTP Code") assert.JSONEq(t, string(wantBody), rec.Body.String(), "Invalid response wantBody") @@ -242,7 +239,7 @@ func Test_Evaluate(t *testing.T) { // init go-feature-flag goFF, _ := ffclient.New(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: tt.args.configFlagsLocation, diff --git a/cmd/relayproxy/service/gofeatureflag.go b/cmd/relayproxy/service/gofeatureflag.go index eaa0f9c69be..31764bd95bd 100644 --- a/cmd/relayproxy/service/gofeatureflag.go +++ b/cmd/relayproxy/service/gofeatureflag.go @@ -2,9 +2,11 @@ package service import ( "fmt" + "log/slog" "time" awsConf "github.com/aws/aws-sdk-go-v2/config" + slogzap "github.com/samber/slog-zap/v2" ffclient "github.com/thomaspoignant/go-feature-flag" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config" "github.com/thomaspoignant/go-feature-flag/exporter" @@ -53,7 +55,7 @@ func NewGoFeatureFlagClient( } } - // Manage if we have more than 1 retriver + // Manage if we have more than 1 retriever retrievers := make([]retriever.Retriever, 0) if proxyConf.Retrievers != nil { for _, r := range *proxyConf.Retrievers { @@ -85,7 +87,7 @@ func NewGoFeatureFlagClient( f := ffclient.Config{ PollingInterval: time.Duration(proxyConf.PollingInterval) * time.Millisecond, - Logger: zap.NewStdLog(logger), + LeveledLogger: slog.New(slogzap.Option{Level: slog.LevelDebug, Logger: logger}.NewZapHandler()), Context: context.Background(), Retriever: mainRetriever, Retrievers: retrievers, diff --git a/config.go b/config.go index 9d5692e833f..1d1609da0bd 100644 --- a/config.go +++ b/config.go @@ -3,7 +3,9 @@ package ffclient import ( "context" "errors" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" "log" + "log/slog" "sync" "time" @@ -27,15 +29,20 @@ type Config struct { // Default: false EnablePollingJitter bool + // Deprecated: Use LeveledLogger instead // Logger (optional) logger use by the library // Default: No log Logger *log.Logger + // LeveledLogger (optional) logger use by the library + // Default: No log + LeveledLogger *slog.Logger + // Context (optional) used to call other services (HTTP, S3 ...) // Default: context.Background() Context context.Context - // Environment (optional), can be checked in feature flag rules + // Environment (optional) can be checked in feature flag rules // Default: "" Environment string @@ -81,6 +88,10 @@ type Config struct { // offlineMutex is a mutex to protect the Offline field. offlineMutex *sync.RWMutex + + // internalLogger is the logger used by the library everywhere + // this logger is a superset of the logging system to be able to migrate easily to slog. + internalLogger *fflog.FFLogger } // GetRetrievers returns a retriever.Retriever configure with the retriever available in the config. diff --git a/examples/data_export_file/main.go b/examples/data_export_file/main.go index dad7930783b..ae7c7b594d0 100644 --- a/examples/data_export_file/main.go +++ b/examples/data_export_file/main.go @@ -3,7 +3,7 @@ package main import ( "context" "log" - "os" + "log/slog" "time" "github.com/thomaspoignant/go-feature-flag/ffcontext" @@ -18,7 +18,7 @@ func main() { // Init ffclient with a file retriever. err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: "examples/data_export_file/flags.goff.yaml", diff --git a/examples/data_export_googlecloudstorage/main.go b/examples/data_export_googlecloudstorage/main.go index 9157dee3bb4..aec0dd4b40e 100644 --- a/examples/data_export_googlecloudstorage/main.go +++ b/examples/data_export_googlecloudstorage/main.go @@ -3,7 +3,7 @@ package main import ( "context" "log" - "os" + "log/slog" "time" "github.com/thomaspoignant/go-feature-flag/ffcontext" @@ -26,7 +26,7 @@ func main() { */ err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: "examples/data_export_s3/flags.goff.yaml", diff --git a/examples/data_export_kafka/main.go b/examples/data_export_kafka/main.go index 578c54815c1..664d83e5659 100644 --- a/examples/data_export_kafka/main.go +++ b/examples/data_export_kafka/main.go @@ -4,7 +4,7 @@ import ( "context" "github.com/thomaspoignant/go-feature-flag/exporter/kafkaexporter" "log" - "os" + "log/slog" "time" ffclient "github.com/thomaspoignant/go-feature-flag" @@ -22,7 +22,7 @@ func main() { */ err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: "examples/data_export_s3/flags.goff.yaml", diff --git a/examples/data_export_s3/main.go b/examples/data_export_s3/main.go index b92362fabcf..869db918cd3 100644 --- a/examples/data_export_s3/main.go +++ b/examples/data_export_s3/main.go @@ -3,7 +3,7 @@ package main import ( "context" "log" - "os" + "log/slog" "time" "github.com/aws/aws-sdk-go-v2/config" @@ -25,7 +25,7 @@ func main() { awsConfig, _ := config.LoadDefaultConfig(context.Background()) err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: "examples/data_export_s3/flags.goff.yaml", diff --git a/examples/retriever_configmap/main.go b/examples/retriever_configmap/main.go index 00dcb9b3247..8074ee2d6e5 100644 --- a/examples/retriever_configmap/main.go +++ b/examples/retriever_configmap/main.go @@ -5,8 +5,8 @@ import ( "fmt" "github.com/thomaspoignant/go-feature-flag/ffcontext" "log" + "log/slog" "net/http" - "os" "time" "github.com/thomaspoignant/go-feature-flag/retriever/k8sretriever" @@ -24,7 +24,7 @@ func main() { err = ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &k8sretriever.Retriever{ Namespace: "default", diff --git a/examples/retriever_file/main.go b/examples/retriever_file/main.go index 6cc89b8e5a0..c7e8b12ee94 100644 --- a/examples/retriever_file/main.go +++ b/examples/retriever_file/main.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/thomaspoignant/go-feature-flag/ffcontext" "log" - "os" + "log/slog" "time" "github.com/thomaspoignant/go-feature-flag/exporter/fileexporter" @@ -18,7 +18,7 @@ func main() { // Init ffclient with a file retriever. err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: "examples/retriever_file/flags.goff.yaml", diff --git a/examples/retriever_github/main.go b/examples/retriever_github/main.go index f1db3140164..fc8468643ee 100644 --- a/examples/retriever_github/main.go +++ b/examples/retriever_github/main.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/thomaspoignant/go-feature-flag/ffcontext" "log" - "os" + "log/slog" "time" "github.com/thomaspoignant/go-feature-flag/retriever/githubretriever" @@ -17,7 +17,7 @@ func main() { // Init ffclient with a GitHub retriever. err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &githubretriever.Retriever{ RepositorySlug: "thomaspoignant/go-feature-flag", diff --git a/examples/retriever_http/main.go b/examples/retriever_http/main.go index 62678f661c7..e253e2fe1e6 100644 --- a/examples/retriever_http/main.go +++ b/examples/retriever_http/main.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/thomaspoignant/go-feature-flag/ffcontext" "log" - "os" + "log/slog" "time" "github.com/thomaspoignant/go-feature-flag/retriever/httpretriever" @@ -17,7 +17,7 @@ func main() { // Init ffclient with a HTTP retriever. err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &httpretriever.Retriever{ URL: "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/examples/retriever_http/flags.goff.yaml", diff --git a/examples/retriever_mongodb/main.go b/examples/retriever_mongodb/main.go index b4544e87cc1..e0d8fe74f7a 100644 --- a/examples/retriever_mongodb/main.go +++ b/examples/retriever_mongodb/main.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "log" - "os" + "log/slog" "time" "github.com/thomaspoignant/go-feature-flag/ffcontext" @@ -19,12 +19,12 @@ func main() { // Init ffclient with a file retriever. err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &mongodbretriever.Retriever{ Collection: "featureFlags", - Database: "appConfig", - URI: "mongodb://root:example@127.0.0.1:27017/", + Database: "appConfig", + URI: "mongodb://root:example@127.0.0.1:27017/", }, DataExporter: ffclient.DataExporter{ FlushInterval: 1 * time.Second, diff --git a/examples/retriever_multiple_config_files/main.go b/examples/retriever_multiple_config_files/main.go index a2d3dac39fa..fd21e911196 100644 --- a/examples/retriever_multiple_config_files/main.go +++ b/examples/retriever_multiple_config_files/main.go @@ -6,7 +6,7 @@ import ( "github.com/thomaspoignant/go-feature-flag/ffcontext" "github.com/thomaspoignant/go-feature-flag/retriever" "log" - "os" + "log/slog" "time" "github.com/thomaspoignant/go-feature-flag/exporter/fileexporter" @@ -19,7 +19,7 @@ func main() { // Init ffclient with multiple file retrievers. err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retrievers: []retriever.Retriever{ &fileretriever.Retriever{ diff --git a/examples/retriever_s3/main.go b/examples/retriever_s3/main.go index d9386054678..fed2131a47b 100644 --- a/examples/retriever_s3/main.go +++ b/examples/retriever_s3/main.go @@ -7,7 +7,7 @@ import ( "github.com/thomaspoignant/go-feature-flag/ffcontext" "github.com/thomaspoignant/go-feature-flag/retriever/s3retrieverv2" "log" - "os" + "log/slog" "time" ffclient "github.com/thomaspoignant/go-feature-flag" @@ -22,7 +22,7 @@ func main() { } err = ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &s3retrieverv2.Retriever{ Bucket: "goff-test", diff --git a/examples/rollout_experimentation/main.go b/examples/rollout_experimentation/main.go index d663cb87646..66f6f6dc42c 100644 --- a/examples/rollout_experimentation/main.go +++ b/examples/rollout_experimentation/main.go @@ -4,7 +4,7 @@ import ( "context" "github.com/thomaspoignant/go-feature-flag/ffcontext" "log" - "os" + "log/slog" "time" "github.com/thomaspoignant/go-feature-flag/exporter/logsexporter" @@ -20,7 +20,7 @@ func main() { // Init ffclient with a file retriever. err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: "examples/rollout_experimentation/flags.goff.yaml", diff --git a/examples/rollout_progressive/main.go b/examples/rollout_progressive/main.go index 51a10634309..e8c0026e8dc 100644 --- a/examples/rollout_progressive/main.go +++ b/examples/rollout_progressive/main.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/thomaspoignant/go-feature-flag/ffcontext" "log" - "os" + "log/slog" "time" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" @@ -19,7 +19,7 @@ func main() { err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: "examples/rollout_progressive/flags.goff.yaml", diff --git a/examples/rollout_scheduled/main.go b/examples/rollout_scheduled/main.go index 23c0633cd21..5037a90fa13 100644 --- a/examples/rollout_scheduled/main.go +++ b/examples/rollout_scheduled/main.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/thomaspoignant/go-feature-flag/ffcontext" "log" - "os" + "log/slog" "time" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" @@ -19,7 +19,7 @@ func main() { err := ffclient.Init(ffclient.Config{ PollingInterval: 10 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Retriever: &fileretriever.Retriever{ Path: "examples/rollout_scheduled/flags.goff.yaml", diff --git a/exporter/data_exporter.go b/exporter/data_exporter.go index 1dfc2125aa3..1eaca455852 100644 --- a/exporter/data_exporter.go +++ b/exporter/data_exporter.go @@ -4,6 +4,7 @@ import ( "context" "github.com/thomaspoignant/go-feature-flag/utils/fflog" "log" + "log/slog" "sync" "time" ) @@ -13,9 +14,9 @@ const ( defaultMaxEventInMemory = int64(100000) ) -// NewScheduler allows to create a new instance of Scheduler ready to be used to export data. +// NewScheduler allows creating a new instance of Scheduler ready to be used to export data. func NewScheduler(ctx context.Context, flushInterval time.Duration, maxEventInMemory int64, - exp Exporter, logger *log.Logger, + exp Exporter, logger *fflog.FFLogger, ) *Scheduler { if ctx == nil { ctx = context.Background() @@ -49,11 +50,11 @@ type Scheduler struct { ticker *time.Ticker maxEventInCache int64 exporter Exporter - logger *log.Logger + logger *fflog.FFLogger ctx context.Context } -// AddEvent allow to add an event to the local cache and to call the exporter if we reach +// AddEvent allow adding an event to the local cache and to call the exporter if we reach // the maximum number of events that can be present in the cache. func (dc *Scheduler) AddEvent(event FeatureEvent) { if !dc.exporter.IsBulk() { @@ -104,16 +105,22 @@ func (dc *Scheduler) Close() { dc.mutex.Unlock() } +// GetLogger will return the logger used by the scheduler +func (dc *Scheduler) GetLogger(level slog.Level) *log.Logger { + if dc.logger == nil { + return nil + } + return dc.logger.GetLogLogger(level) +} + // flush will call the data exporter and clear the cache -// this method should be always called with a mutex func (dc *Scheduler) flush() { if len(dc.localCache) > 0 { - err := dc.exporter.Export(dc.ctx, dc.logger, dc.localCache) + err := dc.exporter.Export(dc.ctx, dc.GetLogger(slog.LevelError), dc.localCache) if err != nil { - fflog.Printf(dc.logger, "error while exporting data: %v\n", err) + dc.logger.Error("error while exporting data", slog.Any("err", err)) return } } - // Clear the cache dc.localCache = make([]FeatureEvent, 0) } diff --git a/exporter/data_exporter_test.go b/exporter/data_exporter_test.go index 3acfdd72bb3..13c086168ff 100644 --- a/exporter/data_exporter_test.go +++ b/exporter/data_exporter_test.go @@ -3,23 +3,23 @@ package exporter_test import ( "context" "errors" - "log" + "github.com/thejerf/slogassert" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" + "log/slog" "os" "testing" "time" + "github.com/stretchr/testify/assert" "github.com/thomaspoignant/go-feature-flag/exporter" "github.com/thomaspoignant/go-feature-flag/ffcontext" - "github.com/thomaspoignant/go-feature-flag/testutils" - - "github.com/stretchr/testify/assert" "github.com/thomaspoignant/go-feature-flag/testutils/mock" ) func TestDataExporterScheduler_flushWithTime(t *testing.T) { mockExporter := mock.Exporter{Bulk: true} dc := exporter.NewScheduler( - context.Background(), 10*time.Millisecond, 1000, &mockExporter, log.New(os.Stdout, "", 0)) + context.Background(), 10*time.Millisecond, 1000, &mockExporter, nil) go dc.StartDaemon() defer dc.Close() @@ -41,7 +41,7 @@ func TestDataExporterScheduler_flushWithTime(t *testing.T) { func TestDataExporterScheduler_flushWithNumberOfEvents(t *testing.T) { mockExporter := mock.Exporter{Bulk: true} dc := exporter.NewScheduler( - context.Background(), 10*time.Minute, 100, &mockExporter, log.New(os.Stdout, "", 0)) + context.Background(), 10*time.Minute, 100, &mockExporter, nil) go dc.StartDaemon() defer dc.Close() @@ -61,7 +61,7 @@ func TestDataExporterScheduler_flushWithNumberOfEvents(t *testing.T) { func TestDataExporterScheduler_defaultFlush(t *testing.T) { mockExporter := mock.Exporter{Bulk: true} dc := exporter.NewScheduler( - context.Background(), 0, 0, &mockExporter, log.New(os.Stdout, "", 0)) + context.Background(), 0, 0, &mockExporter, nil) go dc.StartDaemon() defer dc.Close() @@ -84,10 +84,11 @@ func TestDataExporterScheduler_exporterReturnError(t *testing.T) { file, _ := os.CreateTemp("", "log") defer file.Close() defer os.Remove(file.Name()) - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) dc := exporter.NewScheduler( - context.Background(), 0, 100, &mockExporter, logger) + context.Background(), 0, 100, &mockExporter, &fflog.FFLogger{LeveledLogger: logger}) go dc.StartDaemon() defer dc.Close() @@ -102,16 +103,13 @@ func TestDataExporterScheduler_exporterReturnError(t *testing.T) { dc.AddEvent(event) } assert.Equal(t, inputEvents[:201], mockExporter.GetExportedEvents()) - - // read log - logs, _ := os.ReadFile(file.Name()) - assert.Regexp(t, "\\["+testutils.RFC3339Regex+"\\] error while exporting data: random err\n", string(logs)) + handler.AssertMessage("error while exporting data") } func TestDataExporterScheduler_nonBulkExporter(t *testing.T) { mockExporter := mock.Exporter{Bulk: false} dc := exporter.NewScheduler( - context.Background(), 0, 0, &mockExporter, log.New(os.Stdout, "", 0)) + context.Background(), 0, 0, &mockExporter, nil) defer dc.Close() // Initialize inputEvents slice diff --git a/exporter/exporter.go b/exporter/exporter.go index cc501803885..3fa03678d8d 100644 --- a/exporter/exporter.go +++ b/exporter/exporter.go @@ -5,7 +5,7 @@ import ( "log" ) -// Exporter is an interface to describe how a exporter looks like. +// Exporter is an interface to describe how an exporter looks like. type Exporter interface { // Export will send the data to the exporter. Export(context.Context, *log.Logger, []FeatureEvent) error diff --git a/exporter/gcstorageexporter/exporter.go b/exporter/gcstorageexporter/exporter.go index 54da9edf509..c5e2b742950 100644 --- a/exporter/gcstorageexporter/exporter.go +++ b/exporter/gcstorageexporter/exporter.go @@ -6,6 +6,7 @@ import ( "github.com/thomaspoignant/go-feature-flag/utils/fflog" "io" "log" + "log/slog" "os" "github.com/thomaspoignant/go-feature-flag/exporter" @@ -95,13 +96,13 @@ func (f *Exporter) Export(ctx context.Context, logger *log.Logger, featureEvents } for _, file := range files { - // read file of, err := os.Open(outputDir + "/" + file.Name()) if err != nil { - fflog.Printf(logger, "error: [Exporter] impossible to open the file %s/%s", outputDir, file.Name()) + fflog.ConvertToFFLogger(logger).Error("[GCP Exporter] impossible to open the file", + slog.String("path", outputDir+"/"+file.Name())) continue } - defer of.Close() + defer func() { _ = of.Close() }() // prepend the path source := file.Name() @@ -116,7 +117,6 @@ func (f *Exporter) Export(ctx context.Context, logger *log.Logger, featureEvents return fmt.Errorf("error: [Exporter] impossible to copy the file from %s to bucket %s: %v", source, f.Bucket, err) } - fflog.Printf(logger, "info: [Exporter] file %s uploaded.", file.Name()) } return nil diff --git a/exporter/kafkaexporter/exporter.go b/exporter/kafkaexporter/exporter.go index accf88f1388..f5c2fd80f88 100644 --- a/exporter/kafkaexporter/exporter.go +++ b/exporter/kafkaexporter/exporter.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/IBM/sarama" "github.com/thomaspoignant/go-feature-flag/exporter" - "github.com/thomaspoignant/go-feature-flag/utils/fflog" "log" ) @@ -45,7 +44,7 @@ type Exporter struct { // Export will produce a message to the Kafka topic. The message's value will contain the event encoded in the // selected format. Messages are published synchronously and will error immediately on failure. -func (e *Exporter) Export(_ context.Context, logger *log.Logger, featureEvents []exporter.FeatureEvent) error { +func (e *Exporter) Export(_ context.Context, _ *log.Logger, featureEvents []exporter.FeatureEvent) error { if e.sender == nil { err := e.initializeProducer() if err != nil { @@ -71,8 +70,6 @@ func (e *Exporter) Export(_ context.Context, logger *log.Logger, featureEvents [ if err != nil { return fmt.Errorf("send: %w", err) } - - fflog.Printf(logger, "info: [KafkaExporter] sent %d messages", len(messages)) return nil } diff --git a/exporter/logsexporter/exporter.go b/exporter/logsexporter/exporter.go index d4834e6bfe3..3c0b639bf53 100644 --- a/exporter/logsexporter/exporter.go +++ b/exporter/logsexporter/exporter.go @@ -3,6 +3,7 @@ package logsexporter import ( "bytes" "context" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" "log" "sync" "text/template" @@ -48,7 +49,7 @@ func (f *Exporter) Export(_ context.Context, logger *log.Logger, featureEvents [ FormattedDate string }{FeatureEvent: event, FormattedDate: time.Unix(event.CreationDate, 0).Format(time.RFC3339)}) - logger.Print(log.String()) + fflog.ConvertToFFLogger(logger).Info(log.String()) if err != nil { return err } diff --git a/exporter/logsexporter/exporter_test.go b/exporter/logsexporter/exporter_test.go index d6a5fb23d98..031eb32a0fb 100644 --- a/exporter/logsexporter/exporter_test.go +++ b/exporter/logsexporter/exporter_test.go @@ -99,7 +99,9 @@ func TestLog_Export(t *testing.T) { assert.NoError(t, err, "Exporter should not throw errors") logContent, _ := os.ReadFile(logFile.Name()) - assert.Regexp(t, tt.expectedLog, string(logContent)) + // we remove the prefix of the log (date + level) + withoutPrefix := string(logContent)[25:] + assert.Regexp(t, tt.expectedLog, withoutPrefix) }) } } diff --git a/exporter/s3exporter/exporter.go b/exporter/s3exporter/exporter.go index 1ae1d63cd33..36bfebda372 100644 --- a/exporter/s3exporter/exporter.go +++ b/exporter/s3exporter/exporter.go @@ -4,6 +4,7 @@ import ( "context" "github.com/thomaspoignant/go-feature-flag/utils/fflog" "log" + "log/slog" "os" "sync" @@ -102,7 +103,8 @@ func (f *Exporter) Export(ctx context.Context, logger *log.Logger, featureEvents // read file of, err := os.Open(outputDir + "/" + file.Name()) if err != nil { - fflog.Printf(logger, "error: [S3Exporter] impossible to open the file %s/%s", outputDir, file.Name()) + fflog.ConvertToFFLogger(logger).Error("[S3Exporter] impossible to open the file", + slog.String("directory", outputDir), slog.String("filePath", file.Name())) continue } @@ -117,7 +119,8 @@ func (f *Exporter) Export(ctx context.Context, logger *log.Logger, featureEvents return err } - fflog.Printf(logger, "info: [S3Exporter] file %s uploaded.", result.Location) + fflog.ConvertToFFLogger(logger).Info("[S3Exporter] file uploaded.", + slog.String("fileLocation", result.Location)) } return nil } diff --git a/exporter/s3exporterv2/exporter.go b/exporter/s3exporterv2/exporter.go index 1d1bfce88ce..aff2f0cdbb7 100644 --- a/exporter/s3exporterv2/exporter.go +++ b/exporter/s3exporterv2/exporter.go @@ -11,6 +11,7 @@ import ( "github.com/thomaspoignant/go-feature-flag/exporter/fileexporter" "github.com/thomaspoignant/go-feature-flag/utils/fflog" "log" + "log/slog" "os" "sync" ) @@ -52,6 +53,7 @@ type Exporter struct { s3Uploader UploaderAPI init sync.Once + ffLogger *fflog.FFLogger } func (f *Exporter) initializeUploader(ctx context.Context) error { @@ -80,6 +82,7 @@ func (f *Exporter) Export(ctx context.Context, logger *log.Logger, featureEvents return initErr } } + f.ffLogger = fflog.ConvertToFFLogger(logger) // Create a temp directory to store the file we will produce outputDir, err := os.MkdirTemp("", "go_feature_flag_s3_export") @@ -111,7 +114,7 @@ func (f *Exporter) Export(ctx context.Context, logger *log.Logger, featureEvents // read file of, err := os.Open(outputDir + "/" + file.Name()) if err != nil { - fflog.Printf(logger, "error: [S3Exporter] impossible to open the file %s/%s", outputDir, file.Name()) + f.ffLogger.Error("[S3Exporter] impossible to open the file", slog.String("path", outputDir+"/"+file.Name())) continue } @@ -125,7 +128,7 @@ func (f *Exporter) Export(ctx context.Context, logger *log.Logger, featureEvents return err } - fflog.Printf(logger, "info: [S3Exporter] file %s uploaded.", result.Location) + f.ffLogger.Info("[S3Exporter] file uploaded.", slog.String("location", result.Location)) } return nil } diff --git a/exporter/webhookexporter/exporter.go b/exporter/webhookexporter/exporter.go index dedb2b044e4..35ba35c0155 100644 --- a/exporter/webhookexporter/exporter.go +++ b/exporter/webhookexporter/exporter.go @@ -91,7 +91,7 @@ func (f *Exporter) Export(ctx context.Context, _ *log.Logger, featureEvents []ex } f.Headers["Content-Type"] = []string{"application/json"} - // if a secret is provided we sign the body and add this signature as a header. + // if a secret is provided, we sign the body and add this signature as a header. if f.Secret != "" { f.Headers["X-Hub-Signature-256"] = []string{signer.Sign(payload, []byte(f.Secret))} } diff --git a/feature_flag.go b/feature_flag.go index b5e9699b0b9..b403c235373 100644 --- a/feature_flag.go +++ b/feature_flag.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "log/slog" "sync" "time" @@ -58,7 +59,7 @@ var onceFF sync.Once func New(config Config) (*GoFeatureFlag, error) { switch { case config.PollingInterval == 0: - // The default value for poll interval is 60 seconds + // The default value for the poll interval is 60 seconds config.PollingInterval = 60 * time.Second case config.PollingInterval < 0: // Check that value is not negative @@ -74,25 +75,28 @@ func New(config Config) (*GoFeatureFlag, error) { config.offlineMutex = &sync.RWMutex{} } + config.internalLogger = &fflog.FFLogger{ + LeveledLogger: config.LeveledLogger, + LegacyLogger: config.Logger, + } + goFF := &GoFeatureFlag{ config: config, } if !config.Offline { notifiers := config.Notifiers - if config.Logger != nil { - notifiers = append(notifiers, &logsnotifier.Notifier{Logger: config.Logger}) - } + notifiers = append(notifiers, &logsnotifier.Notifier{Logger: config.internalLogger}) notificationService := cache.NewNotificationService(notifiers) goFF.bgUpdater = newBackgroundUpdater(config.PollingInterval, config.EnablePollingJitter) - goFF.cache = cache.New(notificationService, config.Logger) + goFF.cache = cache.New(notificationService, config.internalLogger) retrievers, err := config.GetRetrievers() if err != nil { return nil, err } - goFF.retrieverManager = retriever.NewManager(config.Context, retrievers, config.Logger) + goFF.retrieverManager = retriever.NewManager(config.Context, retrievers, config.internalLogger) err = goFF.retrieverManager.Init(config.Context) if err != nil && !config.StartWithRetrieverError { return nil, fmt.Errorf("impossible to initialize the retrievers, please check your configuration: %v", err) @@ -107,7 +111,7 @@ func New(config Config) (*GoFeatureFlag, error) { if goFF.config.DataExporter.Exporter != nil { // init the data exporter goFF.dataExporter = exporter.NewScheduler(goFF.config.Context, goFF.config.DataExporter.FlushInterval, - goFF.config.DataExporter.MaxEventInMemory, goFF.config.DataExporter.Exporter, goFF.config.Logger) + goFF.config.DataExporter.MaxEventInMemory, goFF.config.DataExporter.Exporter, goFF.config.internalLogger) // we start the daemon only if we have a bulk exporter if goFF.config.DataExporter.Exporter.IsBulk() { @@ -115,6 +119,7 @@ func New(config Config) (*GoFeatureFlag, error) { } } } + config.internalLogger.Debug("GO Feature Flag is initialized") return goFF, nil } @@ -138,7 +143,7 @@ func (g *GoFeatureFlag) Close() { } } -// startFlagUpdaterDaemon is the daemon that refresh the cache every X seconds. +// startFlagUpdaterDaemon is the daemon that refreshes the cache every X seconds. func (g *GoFeatureFlag) startFlagUpdaterDaemon() { for { select { @@ -146,7 +151,7 @@ func (g *GoFeatureFlag) startFlagUpdaterDaemon() { if !g.IsOffline() { err := retrieveFlagsAndUpdateCache(g.config, g.cache, g.retrieverManager) if err != nil { - fflog.Printf(g.config.Logger, "error while updating the cache: %v\n", err) + g.config.internalLogger.Error("error while updating the cache: %v\n", slog.Any("error", err)) } } case <-g.bgUpdater.updaterChan: @@ -184,7 +189,7 @@ func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager, retrieverMa defer wg.Done() // If the retriever is not ready, we ignore it - if rr, ok := r.(retriever.InitializableRetriever); ok && rr.Status() != retriever.RetrieverReady { + if rr, ok := r.(retriever.CommonInitializableRetriever); ok && rr.Status() != retriever.RetrieverReady { resultsChan <- Results{Error: nil, Value: map[string]dto.DTO{}, Index: index} return } @@ -215,7 +220,7 @@ func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager, retrieverMa } } - err := cache.UpdateCache(newFlags, config.Logger) + err := cache.UpdateCache(newFlags, config.internalLogger) if err != nil { log.Printf("error: impossible to update the cache of the flags: %v", err) return err @@ -223,7 +228,7 @@ func retrieveFlagsAndUpdateCache(config Config, cache cache.Manager, retrieverMa return nil } -// GetCacheRefreshDate gives the date of the latest refresh of the cache +// GetCacheRefreshDate gives the last refresh date of the cache func (g *GoFeatureFlag) GetCacheRefreshDate() time.Time { if g.config.Offline { return time.Time{} @@ -240,7 +245,7 @@ func (g *GoFeatureFlag) ForceRefresh() bool { } err := retrieveFlagsAndUpdateCache(g.config, g.cache, g.retrieverManager) if err != nil { - fflog.Printf(g.config.Logger, "error while force updating the cache: %v\n", err) + g.config.internalLogger.Error("error while force updating the cache: %v\n", slog.Any("error", err)) return false } return true @@ -256,7 +261,7 @@ func (g *GoFeatureFlag) IsOffline() bool { return g.config.IsOffline() } -// GetPollingInterval is the polling interval between 2 refreshes of the cache +// GetPollingInterval is the polling interval between two refreshes of the cache func (g *GoFeatureFlag) GetPollingInterval() int64 { return g.config.PollingInterval.Milliseconds() } @@ -271,7 +276,7 @@ func IsOffline() bool { return ff.IsOffline() } -// GetCacheRefreshDate gives the date of the latest refresh of the cache +// GetCacheRefreshDate gives the last refresh date of the cache func GetCacheRefreshDate() time.Time { return ff.GetCacheRefreshDate() } diff --git a/feature_flag_test.go b/feature_flag_test.go index 1a3de95000d..24f79c166ef 100644 --- a/feature_flag_test.go +++ b/feature_flag_test.go @@ -3,6 +3,7 @@ package ffclient_test import ( "errors" "log" + "log/slog" "os" "testing" "time" @@ -22,7 +23,7 @@ import ( func TestStartWithoutRetriever(t *testing.T) { _, err := ffclient.New(ffclient.Config{ PollingInterval: 60 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), }) assert.Error(t, err) } @@ -30,7 +31,7 @@ func TestStartWithoutRetriever(t *testing.T) { func TestMultipleRetrievers(t *testing.T) { client, err := ffclient.New(ffclient.Config{ PollingInterval: 60 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Retrievers: []retriever.Retriever{ &fileretriever.Retriever{Path: "testdata/flag-config-2nd-file.yaml"}, &fileretriever.Retriever{Path: "testdata/flag-config.yaml"}, @@ -52,7 +53,7 @@ func TestMultipleRetrievers(t *testing.T) { func TestMultipleRetrieversWithOverrideFlag(t *testing.T) { client, err := ffclient.New(ffclient.Config{ PollingInterval: 60 * time.Second, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Retriever: &fileretriever.Retriever{Path: "testdata/multiple_files/config-1.yaml"}, Retrievers: []retriever.Retriever{ &fileretriever.Retriever{Path: "testdata/multiple_files/config-2.yaml"}, @@ -76,7 +77,7 @@ func TestStartWithNegativeInterval(t *testing.T) { _, err := ffclient.New(ffclient.Config{ PollingInterval: -60 * time.Second, Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), }) assert.Error(t, err) } @@ -85,7 +86,7 @@ func TestStartWithMinInterval(t *testing.T) { _, err := ffclient.New(ffclient.Config{ PollingInterval: 2, Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), }) assert.NoError(t, err) } @@ -95,7 +96,7 @@ func TestValidUseCase(t *testing.T) { err := ffclient.Init(ffclient.Config{ PollingInterval: 5 * time.Second, Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), DataExporter: ffclient.DataExporter{ FlushInterval: 10 * time.Second, MaxEventInMemory: 1000, @@ -144,7 +145,7 @@ func TestValidUseCaseToml(t *testing.T) { gffClient, err := ffclient.New(ffclient.Config{ PollingInterval: 5 * time.Second, Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.toml"}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), FileFormat: "toml", }) defer gffClient.Close() @@ -162,7 +163,7 @@ func TestValidUseCaseJson(t *testing.T) { gffClient, err := ffclient.New(ffclient.Config{ PollingInterval: 5 * time.Second, Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.json"}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), FileFormat: "json", }) defer gffClient.Close() @@ -181,7 +182,7 @@ func TestValidUseCaseMultilineQueryJson(t *testing.T) { gffClient, err := ffclient.New(ffclient.Config{ PollingInterval: 5 * time.Second, Retriever: &fileretriever.Retriever{Path: "testdata/flag-config-multiline-query.json"}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), FileFormat: "json", }) defer gffClient.Close() @@ -212,14 +213,14 @@ func Test2GoFeatureFlagInstance(t *testing.T) { gffClient1, err := ffclient.New(ffclient.Config{ PollingInterval: 5 * time.Second, Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), }) defer gffClient1.Close() gffClient2, err2 := ffclient.New(ffclient.Config{ PollingInterval: 10 * time.Second, Retriever: &fileretriever.Retriever{Path: "testdata/test-instance2.yaml"}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), }) defer gffClient2.Close() @@ -258,7 +259,7 @@ test-flag: gffClient1, _ := ffclient.New(ffclient.Config{ PollingInterval: 1 * time.Second, Retriever: &fileretriever.Retriever{Path: flagFile.Name()}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), }) defer gffClient1.Close() @@ -309,7 +310,7 @@ test-flag: gffClient1, _ := ffclient.New(ffclient.Config{ PollingInterval: 1 * time.Second, Retriever: &fileretriever.Retriever{Path: flagFile.Name()}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), }) defer gffClient1.Close() @@ -475,7 +476,7 @@ func TestGoFeatureFlag_SetOffline(t *testing.T) { gffClient, err := ffclient.New(ffclient.Config{ PollingInterval: 1 * time.Second, Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.yaml"}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Offline: false, }) assert.NoError(t, err) @@ -529,7 +530,7 @@ func Test_ForceRefreshCache(t *testing.T) { gffClient, err := ffclient.New(ffclient.Config{ PollingInterval: 15 * time.Minute, Retriever: &fileretriever.Retriever{Path: tempFile.Name()}, - Logger: log.New(os.Stdout, "", 0), + LeveledLogger: slog.Default(), Offline: false, }) assert.NoError(t, err) diff --git a/go.mod b/go.mod index fdd906af680..2962755bdf9 100644 --- a/go.mod +++ b/go.mod @@ -38,12 +38,14 @@ require ( github.com/prometheus/client_golang v1.19.1 github.com/r3labs/diff/v3 v3.0.1 github.com/redis/go-redis/v9 v9.5.3 + github.com/samber/slog-zap/v2 v2.4.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 github.com/swaggo/echo-swagger v1.4.1 github.com/swaggo/swag v1.16.3 github.com/testcontainers/testcontainers-go v0.31.0 github.com/testcontainers/testcontainers-go/modules/redis v0.31.0 + github.com/thejerf/slogassert v0.3.2 github.com/xitongsys/parquet-go v1.6.2 github.com/xitongsys/parquet-go-source v0.0.0-20230830030807-0dd610dbff1d go.mongodb.org/mongo-driver v1.15.0 @@ -179,6 +181,8 @@ require ( github.com/prometheus/common v0.53.0 // indirect github.com/prometheus/procfs v0.13.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/samber/lo v1.38.1 // indirect + github.com/samber/slog-common v0.16.0 // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -202,7 +206,7 @@ require ( go.opentelemetry.io/otel/metric v1.27.0 // indirect go.opentelemetry.io/otel/trace v1.27.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect - go.uber.org/multierr v1.10.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/mod v0.17.0 // indirect diff --git a/go.sum b/go.sum index c013b7754c3..eb7a286c636 100644 --- a/go.sum +++ b/go.sum @@ -776,6 +776,12 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/samber/slog-common v0.16.0 h1:2/t1EcFd1Ru77mh2ab+8B6NBHnEXsBBHtOJc7PSH0aI= +github.com/samber/slog-common v0.16.0/go.mod h1:Qjrfhwk79XiCIhBj8+jTq1Cr0u9rlWbjawh3dWXzaHk= +github.com/samber/slog-zap/v2 v2.4.0 h1:K27wPwrbmqUvfcbyoA+pAeP6UUQmtF2F1htxc9bU+jA= +github.com/samber/slog-zap/v2 v2.4.0/go.mod h1:a63mgEd8LorYUg94RIaGdAdc98lV/PZf7rhlbOfc3rc= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= @@ -822,6 +828,8 @@ github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jX github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI= github.com/testcontainers/testcontainers-go/modules/redis v0.31.0 h1:5X6GhOdLwV86zcW8sxppJAMtsDC9u+r9tb3biBc9GKs= github.com/testcontainers/testcontainers-go/modules/redis v0.31.0/go.mod h1:dKi5xBwy1k4u8yb3saQHu7hMEJwewHXxzbcMAuLiA6o= +github.com/thejerf/slogassert v0.3.2 h1:sE5f1vdrPr4EFkMW75s9stRePRn4zYpRGpeUbkCR+rc= +github.com/thejerf/slogassert v0.3.2/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -914,8 +922,8 @@ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= diff --git a/internal/cache/cache_manager.go b/internal/cache/cache_manager.go index c0296a60486..501efd7bb70 100644 --- a/internal/cache/cache_manager.go +++ b/internal/cache/cache_manager.go @@ -3,7 +3,7 @@ package cache import ( "encoding/json" "errors" - "log" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" "strings" "sync" "time" @@ -18,7 +18,7 @@ import ( type Manager interface { ConvertToFlagStruct(loadedFlags []byte, fileFormat string) (map[string]dto.DTO, error) - UpdateCache(newFlags map[string]dto.DTO, log *log.Logger) error + UpdateCache(newFlags map[string]dto.DTO, log *fflog.FFLogger) error Close() GetFlag(key string) (flag.Flag, error) AllFlags() (map[string]flag.Flag, error) @@ -30,10 +30,10 @@ type cacheManagerImpl struct { mutex sync.RWMutex notificationService Service latestUpdate time.Time - logger *log.Logger + logger *fflog.FFLogger } -func New(notificationService Service, logger *log.Logger) Manager { +func New(notificationService Service, logger *fflog.FFLogger) Manager { return &cacheManagerImpl{ logger: logger, inMemoryCache: NewInMemoryCache(logger), @@ -57,7 +57,7 @@ func (c *cacheManagerImpl) ConvertToFlagStruct(loadedFlags []byte, fileFormat st return newFlags, err } -func (c *cacheManagerImpl) UpdateCache(newFlags map[string]dto.DTO, log *log.Logger) error { +func (c *cacheManagerImpl) UpdateCache(newFlags map[string]dto.DTO, log *fflog.FFLogger) error { newCache := NewInMemoryCache(c.logger) newCache.Init(newFlags) newCacheFlags := newCache.All() diff --git a/internal/cache/cache_manager_test.go b/internal/cache/cache_manager_test.go index 923b566bd3a..d7763be1617 100644 --- a/internal/cache/cache_manager_test.go +++ b/internal/cache/cache_manager_test.go @@ -1,8 +1,8 @@ package cache_test import ( - "log" - "os" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" + "log/slog" "testing" "github.com/thomaspoignant/go-feature-flag/internal/flag" @@ -244,13 +244,14 @@ variation = "false_var" for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - fCache := cache.New(cache.NewNotificationService([]notifier.Notifier{}), nil) + fCache := cache.New(cache.NewNotificationService([]notifier.Notifier{}), + &fflog.FFLogger{LeveledLogger: slog.Default()}) newFlags, err := fCache.ConvertToFlagStruct(tt.args.loadedFlags, tt.flagFormat) if tt.wantErr { assert.Error(t, err) return } - err = fCache.UpdateCache(newFlags, log.New(os.Stdout, "", 0)) + err = fCache.UpdateCache(newFlags, nil) if tt.wantErr { assert.Error(t, err, "UpdateCache() error = %v, wantErr %v", err, tt.wantErr) return @@ -413,7 +414,7 @@ test-flag2: assert.Error(t, err) return } - err = fCache.UpdateCache(newFlags, log.New(os.Stdout, "", 0)) + err = fCache.UpdateCache(newFlags, &fflog.FFLogger{LeveledLogger: slog.Default()}) assert.NoError(t, err) allFlags, err := fCache.AllFlags() @@ -451,7 +452,7 @@ func Test_cacheManagerImpl_GetLatestUpdateDate(t *testing.T) { fCache := cache.New(cache.NewNotificationService([]notifier.Notifier{}), nil) timeBefore := fCache.GetLatestUpdateDate() newFlags, _ := fCache.ConvertToFlagStruct(loadedFlags, "yaml") - _ = fCache.UpdateCache(newFlags, log.New(os.Stdout, "", 0)) + _ = fCache.UpdateCache(newFlags, &fflog.FFLogger{LeveledLogger: slog.Default()}) timeAfter := fCache.GetLatestUpdateDate() assert.True(t, timeBefore.Before(timeAfter)) diff --git a/internal/cache/in_memory_cache.go b/internal/cache/in_memory_cache.go index d4183874c4e..af182a75a08 100644 --- a/internal/cache/in_memory_cache.go +++ b/internal/cache/in_memory_cache.go @@ -2,20 +2,19 @@ package cache import ( "fmt" - "github.com/thomaspoignant/go-feature-flag/utils/fflog" - "log" - "github.com/thomaspoignant/go-feature-flag/internal/dto" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" + "log/slog" "github.com/thomaspoignant/go-feature-flag/internal/flag" ) type InMemoryCache struct { Flags map[string]flag.InternalFlag - Logger *log.Logger + Logger *fflog.FFLogger } -func NewInMemoryCache(logger *log.Logger) *InMemoryCache { +func NewInMemoryCache(logger *fflog.FFLogger) *InMemoryCache { return &InMemoryCache{ Flags: map[string]flag.InternalFlag{}, Logger: logger, @@ -26,7 +25,8 @@ func (fc *InMemoryCache) addFlag(key string, value flag.InternalFlag) { if err := value.IsValid(); err == nil { fc.Flags[key] = value } else { - fflog.Printf(fc.Logger, "error: [cache] invalid configuration for flag %s: %s", key, err) + fc.Logger.Error("[cache] invalid configuration for flag", + slog.String("key", key), slog.Any("error", err)) } } @@ -70,7 +70,8 @@ func (fc *InMemoryCache) Init(flags map[string]dto.DTO) { if err := flagToAdd.IsValid(); err == nil { cache[key] = flagDto.Convert() } else { - fflog.Printf(fc.Logger, "error: [cache] invalid configuration for flag %s: %s", key, err) + fc.Logger.Error("[cache] invalid configuration for flag", + slog.String("key", key), slog.Any("error", err)) } } fc.Flags = cache diff --git a/internal/cache/notification_service.go b/internal/cache/notification_service.go index a9926817742..0688af21606 100644 --- a/internal/cache/notification_service.go +++ b/internal/cache/notification_service.go @@ -2,7 +2,7 @@ package cache import ( "github.com/thomaspoignant/go-feature-flag/utils/fflog" - "log" + "log/slog" "sync" "github.com/google/go-cmp/cmp" @@ -12,7 +12,7 @@ import ( type Service interface { Close() - Notify(oldCache map[string]flag.Flag, newCache map[string]flag.Flag, log *log.Logger) + Notify(oldCache map[string]flag.Flag, newCache map[string]flag.Flag, log *fflog.FFLogger) } func NewNotificationService(notifiers []notifier.Notifier) Service { @@ -27,7 +27,10 @@ type notificationService struct { waitGroup *sync.WaitGroup } -func (c *notificationService) Notify(oldCache map[string]flag.Flag, newCache map[string]flag.Flag, log *log.Logger) { +func (c *notificationService) Notify( + oldCache map[string]flag.Flag, + newCache map[string]flag.Flag, + log *fflog.FFLogger) { diff := c.getDifferences(oldCache, newCache) if diff.HasDiff() { for _, n := range c.Notifiers { @@ -37,7 +40,7 @@ func (c *notificationService) Notify(oldCache map[string]flag.Flag, newCache map defer c.waitGroup.Done() err := notif.Notify(diff) if err != nil { - fflog.Printf(log, "error while calling the notifier: %v", err) + log.Error("error while calling the notifier", slog.Any("err", err)) } }() } diff --git a/internal/cache/notification_service_priv_test.go b/internal/cache/notification_service_priv_test.go index 0176151fa96..8e6f08bd97b 100644 --- a/internal/cache/notification_service_priv_test.go +++ b/internal/cache/notification_service_priv_test.go @@ -5,13 +5,13 @@ package cache_test import ( "fmt" "github.com/stretchr/testify/assert" + "github.com/thejerf/slogassert" "github.com/thomaspoignant/go-feature-flag/internal/cache" "github.com/thomaspoignant/go-feature-flag/internal/flag" "github.com/thomaspoignant/go-feature-flag/notifier" - "github.com/thomaspoignant/go-feature-flag/testutils" "github.com/thomaspoignant/go-feature-flag/testutils/testconvert" - "log" - "os" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" + "log/slog" "testing" "time" ) @@ -45,8 +45,8 @@ func Test_notificationService_no_difference(t *testing.T) { } func Test_notificationService_with_error(t *testing.T) { - tempFile, _ := os.CreateTemp("", "tempFile") - logger := log.New(tempFile, "", 0) + handler := slogassert.New(t, slog.LevelDebug, nil) + logger := slog.New(handler) n := &NotifierMock{WithError: true} c := cache.NewNotificationService([]notifier.Notifier{n}) oldCache := map[string]flag.Flag{ @@ -55,11 +55,10 @@ func Test_notificationService_with_error(t *testing.T) { newCache := map[string]flag.Flag{ "yo-new": &flag.InternalFlag{Version: testconvert.String("1.0")}, } - c.Notify(oldCache, newCache, logger) + c.Notify(oldCache, newCache, &fflog.FFLogger{LeveledLogger: logger}) time.Sleep(100 * time.Millisecond) - content, _ := os.ReadFile(tempFile.Name()) - assert.Regexp(t, "\\["+testutils.RFC3339Regex+"\\] error while calling the notifier: error\n", string(content)) + handler.AssertMessage("error while calling the notifier") assert.False(t, n.HasBeenCalled) } diff --git a/internal/flag/internal_flag.go b/internal/flag/internal_flag.go index 66e95ef2b3d..dc13c0154fe 100644 --- a/internal/flag/internal_flag.go +++ b/internal/flag/internal_flag.go @@ -229,7 +229,7 @@ func (f *InternalFlag) IsValid() error { return fmt.Errorf("no variation available") } - // Check that all variation have the same types + // Check that all variation has the same types expectedVarType := "" for _, value := range f.GetVariations() { if expectedVarType != "" { diff --git a/notifier/logsnotifier/notifier.go b/notifier/logsnotifier/notifier.go index 53b71b84390..b68e4e4d639 100644 --- a/notifier/logsnotifier/notifier.go +++ b/notifier/logsnotifier/notifier.go @@ -3,34 +3,34 @@ package logsnotifier import ( "github.com/thomaspoignant/go-feature-flag/notifier" "github.com/thomaspoignant/go-feature-flag/utils/fflog" - "log" + "log/slog" ) type Notifier struct { - Logger *log.Logger + Logger *fflog.FFLogger } func (c *Notifier) Notify(diff notifier.DiffCache) error { for key := range diff.Deleted { - fflog.Printf(c.Logger, "flag %v removed\n", key) + c.Logger.Info("flag removed", slog.String("key", key)) } for key := range diff.Added { - fflog.Printf(c.Logger, "flag %v added\n", key) + c.Logger.Info("flag added", slog.String("key", key)) } for key, flagDiff := range diff.Updated { if flagDiff.After.IsDisable() != flagDiff.Before.IsDisable() { if flagDiff.After.IsDisable() { // Flag is disabled - fflog.Printf(c.Logger, "flag %v is turned OFF\n", key) + c.Logger.Info("flag is turned OFF", slog.String("key", key)) continue } - fflog.Printf(c.Logger, "flag %v is turned ON\n", key) + c.Logger.Info("flag is turned ON", slog.String("key", key)) continue } // key has changed in cache - fflog.Printf(c.Logger, "flag %s updated\n", key) + c.Logger.Info("flag updated", slog.String("key", key)) } return nil diff --git a/notifier/logsnotifier/notifier_test.go b/notifier/logsnotifier/notifier_test.go index 22b91c2e044..62b3fa88b8b 100644 --- a/notifier/logsnotifier/notifier_test.go +++ b/notifier/logsnotifier/notifier_test.go @@ -1,26 +1,24 @@ package logsnotifier import ( - "log" - "os" + "github.com/thejerf/slogassert" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" + "log/slog" "testing" - "github.com/stretchr/testify/assert" "github.com/thomaspoignant/go-feature-flag/internal/flag" "github.com/thomaspoignant/go-feature-flag/notifier" "github.com/thomaspoignant/go-feature-flag/testutils/testconvert" - - "github.com/thomaspoignant/go-feature-flag/testutils" ) func TestLogNotifier_Notify(t *testing.T) { type args struct { } tests := []struct { - name string - args args - diff notifier.DiffCache - expected string + name string + args args + diff notifier.DiffCache + expectedLog *slogassert.LogMessageMatch }{ { name: "Flag deleted", @@ -44,7 +42,14 @@ func TestLogNotifier_Notify(t *testing.T) { Updated: map[string]notifier.DiffUpdated{}, Added: map[string]flag.Flag{}, }, - expected: "^\\[" + testutils.RFC3339Regex + "\\] flag test-flag removed", + expectedLog: &slogassert.LogMessageMatch{ + Message: "flag removed", + Level: slog.LevelInfo, + Attrs: map[string]any{ + "key": "test-flag", + }, + AllAttrsMatch: true, + }, }, { name: "Update flag", @@ -91,7 +96,14 @@ func TestLogNotifier_Notify(t *testing.T) { }, Added: map[string]flag.Flag{}, }, - expected: "^\\[" + testutils.RFC3339Regex + "\\] flag test-flag updated", + expectedLog: &slogassert.LogMessageMatch{ + Message: "flag updated", + Level: slog.LevelInfo, + Attrs: map[string]any{ + "key": "test-flag", + }, + AllAttrsMatch: true, + }, }, { name: "Disable flag", @@ -146,7 +158,14 @@ func TestLogNotifier_Notify(t *testing.T) { }, Added: map[string]flag.Flag{}, }, - expected: "^\\[" + testutils.RFC3339Regex + "\\] flag test-flag is turned OFF", + expectedLog: &slogassert.LogMessageMatch{ + Message: "flag is turned OFF", + Level: slog.LevelInfo, + Attrs: map[string]any{ + "key": "test-flag", + }, + AllAttrsMatch: true, + }, }, { name: "Add flag", @@ -177,7 +196,14 @@ func TestLogNotifier_Notify(t *testing.T) { }, }, }, - expected: "^\\[" + testutils.RFC3339Regex + "\\] flag add-test-flag added", + expectedLog: &slogassert.LogMessageMatch{ + Message: "flag added", + Level: slog.LevelInfo, + Attrs: map[string]any{ + "key": "add-test-flag", + }, + AllAttrsMatch: true, + }, }, { name: "Enable flag", @@ -232,20 +258,25 @@ func TestLogNotifier_Notify(t *testing.T) { }, Added: map[string]flag.Flag{}, }, - expected: "^\\[" + testutils.RFC3339Regex + "\\] flag test-flag is turned ON", + expectedLog: &slogassert.LogMessageMatch{ + Message: "flag is turned ON", + Level: slog.LevelInfo, + Attrs: map[string]any{ + "key": "test-flag", + }, + AllAttrsMatch: true, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - logOutput, _ := os.CreateTemp("", "") - defer os.Remove(logOutput.Name()) - + handler := slogassert.New(t, slog.LevelDebug, nil) + logger := slog.New(handler) c := &Notifier{ - Logger: log.New(logOutput, "", 0), + Logger: &fflog.FFLogger{LeveledLogger: logger}, } _ = c.Notify(tt.diff) - log, _ := os.ReadFile(logOutput.Name()) - assert.Regexp(t, tt.expected, string(log)) + handler.AssertPrecise(*tt.expectedLog) }) } } diff --git a/retriever/manager.go b/retriever/manager.go index 29b5dfad621..320dc7922f5 100644 --- a/retriever/manager.go +++ b/retriever/manager.go @@ -3,7 +3,8 @@ package retriever import ( "context" "fmt" - "log" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" + "log/slog" ) // Manager is a struct that managed the retrievers. @@ -11,11 +12,11 @@ type Manager struct { ctx context.Context retrievers []Retriever onErrorRetriever []Retriever - logger *log.Logger + logger *fflog.FFLogger } // NewManager create a new Manager. -func NewManager(ctx context.Context, retrievers []Retriever, logger *log.Logger) *Manager { +func NewManager(ctx context.Context, retrievers []Retriever, logger *fflog.FFLogger) *Manager { return &Manager{ ctx: ctx, retrievers: retrievers, @@ -34,6 +35,13 @@ func (m *Manager) Init(ctx context.Context) error { func (m *Manager) initRetrievers(ctx context.Context, retrieversToInit []Retriever) error { m.onErrorRetriever = make([]Retriever, 0) for _, retriever := range retrieversToInit { + if r, ok := retriever.(InitializableRetrieverLegacy); ok { + err := r.Init(ctx, m.logger.GetLogLogger(slog.LevelError)) + if err != nil { + m.onErrorRetriever = append(m.onErrorRetriever, retriever) + } + } + if r, ok := retriever.(InitializableRetriever); ok { err := r.Init(ctx, m.logger) if err != nil { @@ -52,7 +60,7 @@ func (m *Manager) initRetrievers(ctx context.Context, retrieversToInit []Retriev func (m *Manager) Shutdown(ctx context.Context) error { onErrorRetriever := make([]Retriever, 0) for _, retriever := range m.retrievers { - if r, ok := retriever.(InitializableRetriever); ok { + if r, ok := retriever.(CommonInitializableRetriever); ok { err := r.Shutdown(ctx) if err != nil { onErrorRetriever = append(onErrorRetriever, retriever) diff --git a/retriever/mongodbretriever/retriever.go b/retriever/mongodbretriever/retriever.go index df7cc8961b0..8bca8691753 100644 --- a/retriever/mongodbretriever/retriever.go +++ b/retriever/mongodbretriever/retriever.go @@ -3,10 +3,8 @@ package mongodbretriever import ( "context" "encoding/json" - "github.com/thomaspoignant/go-feature-flag/utils/fflog" - "log" - "github.com/thomaspoignant/go-feature-flag/retriever" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -23,10 +21,10 @@ type Retriever struct { dbConnection *mongo.Database dbClient *mongo.Client status string - logger *log.Logger + logger *fflog.FFLogger } -func (r *Retriever) Init(ctx context.Context, logger *log.Logger) error { +func (r *Retriever) Init(ctx context.Context, logger *fflog.FFLogger) error { r.logger = logger if r.dbConnection == nil { r.status = retriever.RetrieverNotReady @@ -43,18 +41,18 @@ func (r *Retriever) Init(ctx context.Context, logger *log.Logger) error { return nil } -// returns the current status of the retriever +// Status returns the current status of the retriever func (r *Retriever) Status() retriever.Status { return r.status } -// disconnects the retriever from Mongodb instance +// Shutdown disconnects the retriever from Mongodb instance func (r *Retriever) Shutdown(ctx context.Context) error { return r.dbClient.Disconnect(ctx) } -// Reads flag configuration from mongodb and returns it -// if a document does not comply with specification it will be ignored +// Retrieve Reads flag configuration from mongodb and returns it +// if a document does not comply with the specification it will be ignored func (r *Retriever) Retrieve(ctx context.Context) ([]byte, error) { opt := options.CollectionOptions{} opt.SetBSONOptions(&options.BSONOptions{OmitZeroStruct: true}) @@ -81,10 +79,10 @@ func (r *Retriever) Retrieve(ctx context.Context) ([]byte, error) { if str, ok := val.(string); ok { ffDocs[str] = doc } else { - fflog.Printf(r.logger, "ERROR: flag key does not have a string as value") + r.logger.Error("flag key does not have a string as value") } } else { - fflog.Printf(r.logger, "ERROR: no 'flag' entry found") + r.logger.Error("no 'flag' entry found") } } diff --git a/retriever/mongodbretriever/retriever_test.go b/retriever/mongodbretriever/retriever_test.go index b85739f9901..d4022a0b1c5 100644 --- a/retriever/mongodbretriever/retriever_test.go +++ b/retriever/mongodbretriever/retriever_test.go @@ -3,6 +3,7 @@ package mongodbretriever import ( "context" "encoding/json" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" "testing" "github.com/stretchr/testify/assert" @@ -57,6 +58,7 @@ func Test_MongoDBRetriever_Retrieve(t *testing.T) { (*tt.mocker)(t) } + _ = mdb.Init(context.TODO(), &fflog.FFLogger{}) got, err := mdb.Retrieve(context.Background()) if tt.wantErr { diff --git a/retriever/redisretriever/retriever.go b/retriever/redisretriever/retriever.go index 68b4d826447..f244f620796 100644 --- a/retriever/redisretriever/retriever.go +++ b/retriever/redisretriever/retriever.go @@ -6,7 +6,7 @@ import ( "fmt" redis "github.com/redis/go-redis/v9" "github.com/thomaspoignant/go-feature-flag/retriever" - "log" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" "strings" ) @@ -23,7 +23,7 @@ type Retriever struct { client *redis.Client } -func (r *Retriever) Init(ctx context.Context, _ *log.Logger) error { +func (r *Retriever) Init(ctx context.Context, _ *fflog.FFLogger) error { r.status = retriever.RetrieverNotReady client := redis.NewClient(r.Options) diff --git a/retriever/retriever.go b/retriever/retriever.go index 7e5c4ed5c35..7afde62cb8c 100644 --- a/retriever/retriever.go +++ b/retriever/retriever.go @@ -2,6 +2,7 @@ package retriever import ( "context" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" "log" ) @@ -11,10 +12,20 @@ type Retriever interface { Retrieve(ctx context.Context) ([]byte, error) } +// InitializableRetrieverLegacy is an extended version of the retriever that can be initialized and shutdown. +type InitializableRetrieverLegacy interface { + CommonInitializableRetriever + Init(ctx context.Context, logger *log.Logger) error +} + // InitializableRetriever is an extended version of the retriever that can be initialized and shutdown. type InitializableRetriever interface { - Retrieve(ctx context.Context) ([]byte, error) - Init(ctx context.Context, logger *log.Logger) error + CommonInitializableRetriever + Init(ctx context.Context, logger *fflog.FFLogger) error +} + +type CommonInitializableRetriever interface { + Retriever Shutdown(ctx context.Context) error Status() Status } diff --git a/retriever/retriever_test.go b/retriever/retriever_test.go new file mode 100644 index 00000000000..5e6b0a00f11 --- /dev/null +++ b/retriever/retriever_test.go @@ -0,0 +1,103 @@ +package retriever_test + +import ( + "github.com/stretchr/testify/assert" + ffclient "github.com/thomaspoignant/go-feature-flag" + "github.com/thomaspoignant/go-feature-flag/retriever" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" + "golang.org/x/net/context" + "log" + "testing" + "time" +) + +func TestMixLegacyTypesOfRetrievers(t *testing.T) { + sr := &simpleRetriever{} + ilr := &initializableRetrieverLegacy{} + il := &initializableRetriever{} + goff, err := ffclient.New(ffclient.Config{ + PollingInterval: 10 * time.Second, + Retrievers: []retriever.Retriever{ + sr, + ilr, + il, + }, + }) + assert.NoError(t, err) + goff.Close() + + assert.True(t, sr.retrieveCalled) + + assert.True(t, ilr.initCalled) + assert.True(t, ilr.statusCalled) + assert.True(t, ilr.retrieveCalled) + assert.True(t, ilr.shutdownCalled) + + assert.True(t, il.initCalled) + assert.True(t, il.statusCalled) + assert.True(t, il.retrieveCalled) + assert.True(t, il.shutdownCalled) +} + +type simpleRetriever struct { + retrieveCalled bool +} + +func (s *simpleRetriever) Retrieve(_ context.Context) ([]byte, error) { + s.retrieveCalled = true + return []byte{}, nil +} + +type initializableRetrieverLegacy struct { + retrieveCalled bool + initCalled bool + shutdownCalled bool + statusCalled bool +} + +func (i *initializableRetrieverLegacy) Retrieve(_ context.Context) ([]byte, error) { + i.retrieveCalled = true + return []byte{}, nil +} + +func (i *initializableRetrieverLegacy) Init(_ context.Context, _ *log.Logger) error { + i.initCalled = true + return nil +} + +func (i *initializableRetrieverLegacy) Shutdown(_ context.Context) error { + i.shutdownCalled = true + return nil +} + +func (i *initializableRetrieverLegacy) Status() retriever.Status { + i.statusCalled = true + return retriever.RetrieverReady +} + +type initializableRetriever struct { + retrieveCalled bool + initCalled bool + shutdownCalled bool + statusCalled bool +} + +func (i *initializableRetriever) Retrieve(_ context.Context) ([]byte, error) { + i.retrieveCalled = true + return []byte{}, nil +} + +func (i *initializableRetriever) Init(_ context.Context, _ *fflog.FFLogger) error { + i.initCalled = true + return nil +} + +func (i *initializableRetriever) Shutdown(_ context.Context) error { + i.shutdownCalled = true + return nil +} + +func (i *initializableRetriever) Status() retriever.Status { + i.statusCalled = true + return retriever.RetrieverReady +} diff --git a/retriever/s3retrieverv2/retriever.go b/retriever/s3retrieverv2/retriever.go index a22d3351f3f..13350dc8331 100644 --- a/retriever/s3retrieverv2/retriever.go +++ b/retriever/s3retrieverv2/retriever.go @@ -3,12 +3,11 @@ package s3retrieverv2 import ( "context" "fmt" - "log" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" "github.com/thomaspoignant/go-feature-flag/retriever" ) @@ -30,7 +29,7 @@ type Retriever struct { status retriever.Status } -func (s *Retriever) Init(ctx context.Context, _ *log.Logger) error { +func (s *Retriever) Init(ctx context.Context, _ *fflog.FFLogger) error { s.status = retriever.RetrieverNotReady if s.downloader == nil { if s.AwsConfig == nil { diff --git a/testutils/initializableretriever/retriever.go b/testutils/initializableretriever/retriever.go index ce062468947..c4713165db4 100644 --- a/testutils/initializableretriever/retriever.go +++ b/testutils/initializableretriever/retriever.go @@ -3,7 +3,7 @@ package initializableretriever import ( "context" "github.com/thomaspoignant/go-feature-flag/retriever" - "log" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" "os" ) @@ -31,7 +31,7 @@ func (r *Retriever) Retrieve(_ context.Context) ([]byte, error) { return content, nil } -func (r *Retriever) Init(_ context.Context, _ *log.Logger) error { +func (r *Retriever) Init(_ context.Context, _ *fflog.FFLogger) error { yamlString := `flag-xxxx-123: variations: A: true diff --git a/utils/fflog/log.go b/utils/fflog/log.go index a3a37d43d52..82f6512b546 100644 --- a/utils/fflog/log.go +++ b/utils/fflog/log.go @@ -1,16 +1,73 @@ package fflog import ( + "fmt" "log" + "log/slog" + "strings" "time" ) const LogDateFormat = time.RFC3339 -func Printf(logger *log.Logger, format string, v ...interface{}) { - if logger != nil { - date := time.Now().Format(LogDateFormat) - v = append([]interface{}{date}, v...) - logger.Printf("[%v] "+format, v...) +type FFLogger struct { + LeveledLogger *slog.Logger + LegacyLogger *log.Logger +} + +func (f *FFLogger) Error(msg string, keysAndValues ...any) { + if f != nil && f.LeveledLogger != nil { + f.LeveledLogger.Error(msg, keysAndValues...) + return + } + f.legacyLog("ERROR", msg, keysAndValues...) +} +func (f *FFLogger) Info(msg string, keysAndValues ...any) { + if f != nil && f.LeveledLogger != nil { + f.LeveledLogger.Info(msg, keysAndValues...) + return + } + f.legacyLog("INFO", msg, keysAndValues...) +} +func (f *FFLogger) Debug(msg string, keysAndValues ...any) { + if f != nil && f.LeveledLogger != nil { + f.LeveledLogger.Debug(msg, keysAndValues...) + return + } + f.legacyLog("DEBUG", msg, keysAndValues...) +} +func (f *FFLogger) Warn(msg string, keysAndValues ...any) { + if f != nil && f.LeveledLogger != nil { + f.LeveledLogger.Warn(msg, keysAndValues...) + return + } + f.legacyLog("WARN", msg, keysAndValues...) +} + +func (f *FFLogger) legacyLog(level string, msg string, keysAndValues ...any) { + if f != nil && f.LegacyLogger != nil { + if len(keysAndValues) == 0 { + f.LegacyLogger.Printf("%s %s %s", time.Now().Format("2006/01/02 15:04:05"), level, msg) + return + } + + attrs := make([]string, 0) + for _, attr := range keysAndValues { + attrs = append(attrs, fmt.Sprintf("%v", attr)) + } + f.LegacyLogger.Printf("%s %s %s %v", time.Now().Format("2006/01/02 15:04:05"), level, msg, strings.Join(attrs, " ")) + } +} + +func (f *FFLogger) GetLogLogger(level slog.Level) *log.Logger { + if f.LeveledLogger != nil { + return slog.NewLogLogger(f.LeveledLogger.Handler(), level) + } + return f.LegacyLogger +} + +func ConvertToFFLogger(logger *log.Logger) *FFLogger { + return &FFLogger{ + LegacyLogger: logger, } } diff --git a/utils/fflog/log_test.go b/utils/fflog/log_test.go index 46ec442b0c3..2cefc1c7707 100644 --- a/utils/fflog/log_test.go +++ b/utils/fflog/log_test.go @@ -1,44 +1,460 @@ package fflog_test import ( + "github.com/stretchr/testify/assert" "github.com/thomaspoignant/go-feature-flag/utils/fflog" "log" + "log/slog" "os" "testing" - - "github.com/stretchr/testify/assert" ) -func TestPrintf(t *testing.T) { - type args struct { - logger *log.Logger - format string - v []interface{} +func TestFFLogger_Error(t *testing.T) { + type fields struct { + msg string + keysAndValues []interface{} + } + tests := []struct { + name string + logger *fflog.FFLogger + fields fields + expectedLog string + }{ + { + name: "Test Happy Path - slog", + logger: &fflog.FFLogger{ + LeveledLogger: slog.Default(), + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Error message", + keysAndValues: nil, + }, + expectedLog: "ERROR Error message" + "\n", + }, + { + name: "Test Happy Path - slog with keys and values", + logger: &fflog.FFLogger{ + LeveledLogger: slog.Default(), + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Error message", + keysAndValues: []interface{}{slog.String("test", "toto"), slog.String("toto", "test")}, + }, + expectedLog: "ERROR Error message test=toto toto=test" + "\n", + }, + { + name: "Test Happy Path - legacy", + logger: &fflog.FFLogger{ + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Error message", + keysAndValues: nil, + }, + expectedLog: "ERROR Error message" + "\n", + }, + { + name: "Test Happy Path - legacy with keys and values", + logger: &fflog.FFLogger{ + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Error message", + keysAndValues: []interface{}{slog.String("test", "toto"), slog.String("toto", "test")}, + }, + expectedLog: "ERROR Error message test=toto toto=test" + "\n", + }, + { + name: "FFLogger nil", + logger: nil, + fields: fields{ + msg: "Error message", + keysAndValues: nil, + }, + expectedLog: "", + }, + { + name: "FFLogger no logger configured", + logger: &fflog.FFLogger{}, + fields: fields{ + msg: "Error message", + keysAndValues: nil, + }, + expectedLog: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + file, err := os.CreateTemp("", "") + assert.NoError(t, err) + defer func() { + _ = os.Remove(file.Name()) + }() + + log.SetOutput(file) + if tt.logger != nil && tt.logger.LegacyLogger != nil { + tt.logger.LegacyLogger.SetOutput(file) + } + + if tt.fields.keysAndValues != nil { + tt.logger.Error(tt.fields.msg, tt.fields.keysAndValues...) + } else { + tt.logger.Error(tt.fields.msg) + } + + content, err := os.ReadFile(file.Name()) + assert.NoError(t, err) + if len(string(content)) >= 21 { + actualWithoutTimestamp := string(content)[20:] + assert.Equal(t, tt.expectedLog, actualWithoutTimestamp) + } else { + assert.Equal(t, tt.expectedLog, string(content)) + } + }) + } +} + +func TestFFLogger_Warn(t *testing.T) { + type fields struct { + msg string + keysAndValues []interface{} } tests := []struct { - name string - args args + name string + logger *fflog.FFLogger + fields fields + expectedLog string }{ { - name: "no logger", - args: args{ - logger: nil, - format: "Toto", - v: nil, + name: "Test Happy Path - slog", + logger: &fflog.FFLogger{ + LeveledLogger: slog.Default(), + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Warn message", + keysAndValues: nil, + }, + expectedLog: "WARN Warn message" + "\n", + }, + { + name: "Test Happy Path - slog with keys and values", + logger: &fflog.FFLogger{ + LeveledLogger: slog.Default(), + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Warn message", + keysAndValues: []interface{}{slog.String("test", "toto"), slog.String("toto", "test")}, + }, + expectedLog: "WARN Warn message test=toto toto=test" + "\n", + }, + { + name: "Test Happy Path - legacy", + logger: &fflog.FFLogger{ + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Warn message", + keysAndValues: nil, + }, + expectedLog: "WARN Warn message" + "\n", + }, + { + name: "Test Happy Path - legacy with keys and values", + logger: &fflog.FFLogger{ + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Warn message", + keysAndValues: []interface{}{slog.String("test", "toto"), slog.String("toto", "test")}, + }, + expectedLog: "WARN Warn message test=toto toto=test" + "\n", + }, + { + name: "FFLogger nil", + logger: nil, + fields: fields{ + msg: "Warn message", + keysAndValues: nil, }, + expectedLog: "", }, { - name: "with logger", - args: args{ - logger: log.New(os.Stdout, "", 0), - format: "Toto %v", - v: []interface{}{"toto"}, + name: "FFLogger no logger configured", + logger: &fflog.FFLogger{}, + fields: fields{ + msg: "Warn message", + keysAndValues: nil, }, + expectedLog: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.NotPanics(t, func() { fflog.Printf(tt.args.logger, tt.args.format, tt.args.v) }) + file, err := os.CreateTemp("", "") + assert.NoError(t, err) + defer func() { + _ = os.Remove(file.Name()) + }() + + log.SetOutput(file) + if tt.logger != nil && tt.logger.LegacyLogger != nil { + tt.logger.LegacyLogger.SetOutput(file) + } + + if tt.fields.keysAndValues != nil { + tt.logger.Warn(tt.fields.msg, tt.fields.keysAndValues...) + } else { + tt.logger.Warn(tt.fields.msg) + } + + content, err := os.ReadFile(file.Name()) + assert.NoError(t, err) + if len(string(content)) >= 21 { + actualWithoutTimestamp := string(content)[20:] + assert.Equal(t, tt.expectedLog, actualWithoutTimestamp) + } else { + assert.Equal(t, tt.expectedLog, string(content)) + } }) } } + +func TestFFLogger_Info(t *testing.T) { + type fields struct { + msg string + keysAndValues []interface{} + } + tests := []struct { + name string + logger *fflog.FFLogger + fields fields + expectedLog string + }{ + { + name: "Test Happy Path - slog", + logger: &fflog.FFLogger{ + LeveledLogger: slog.Default(), + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Info message", + keysAndValues: nil, + }, + expectedLog: "INFO Info message" + "\n", + }, + { + name: "Test Happy Path - slog with keys and values", + logger: &fflog.FFLogger{ + LeveledLogger: slog.Default(), + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Info message", + keysAndValues: []interface{}{slog.String("test", "toto"), slog.String("toto", "test")}, + }, + expectedLog: "INFO Info message test=toto toto=test" + "\n", + }, + { + name: "Test Happy Path - legacy", + logger: &fflog.FFLogger{ + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Info message", + keysAndValues: nil, + }, + expectedLog: "INFO Info message" + "\n", + }, + { + name: "Test Happy Path - legacy with keys and values", + logger: &fflog.FFLogger{ + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Info message", + keysAndValues: []interface{}{slog.String("test", "toto"), slog.String("toto", "test")}, + }, + expectedLog: "INFO Info message test=toto toto=test" + "\n", + }, + { + name: "FFLogger nil", + logger: nil, + fields: fields{ + msg: "Info message", + keysAndValues: nil, + }, + expectedLog: "", + }, + { + name: "FFLogger no logger configured", + logger: &fflog.FFLogger{}, + fields: fields{ + msg: "Info message", + keysAndValues: nil, + }, + expectedLog: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + file, err := os.CreateTemp("", "") + assert.NoError(t, err) + defer func() { + _ = os.Remove(file.Name()) + }() + + log.SetOutput(file) + if tt.logger != nil && tt.logger.LegacyLogger != nil { + tt.logger.LegacyLogger.SetOutput(file) + } + + if tt.fields.keysAndValues != nil { + tt.logger.Info(tt.fields.msg, tt.fields.keysAndValues...) + } else { + tt.logger.Info(tt.fields.msg) + } + + content, err := os.ReadFile(file.Name()) + assert.NoError(t, err) + if len(string(content)) >= 21 { + actualWithoutTimestamp := string(content)[20:] + assert.Equal(t, tt.expectedLog, actualWithoutTimestamp) + } else { + assert.Equal(t, tt.expectedLog, string(content)) + } + }) + } +} + +func TestFFLogger_Debug(t *testing.T) { + type fields struct { + msg string + keysAndValues []interface{} + } + tests := []struct { + name string + logger *fflog.FFLogger + fields fields + expectedLog string + }{ + { + name: "Test Happy Path - slog", + logger: &fflog.FFLogger{ + LeveledLogger: slog.Default(), + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Debug message", + keysAndValues: nil, + }, + expectedLog: "DEBUG Debug message" + "\n", + }, + { + name: "Test Happy Path - slog with keys and values", + logger: &fflog.FFLogger{ + LeveledLogger: slog.Default(), + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Debug message", + keysAndValues: []interface{}{slog.String("test", "toto"), slog.String("toto", "test")}, + }, + expectedLog: "DEBUG Debug message test=toto toto=test" + "\n", + }, + { + name: "Test Happy Path - legacy", + logger: &fflog.FFLogger{ + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Debug message", + keysAndValues: nil, + }, + expectedLog: "DEBUG Debug message" + "\n", + }, + { + name: "Test Happy Path - legacy with keys and values", + logger: &fflog.FFLogger{ + LegacyLogger: log.New(os.Stdout, "", 0), + }, + fields: fields{ + msg: "Debug message", + keysAndValues: []interface{}{slog.String("test", "toto"), slog.String("toto", "test")}, + }, + expectedLog: "DEBUG Debug message test=toto toto=test" + "\n", + }, + { + name: "FFLogger nil", + logger: nil, + fields: fields{ + msg: "Debug message", + keysAndValues: nil, + }, + expectedLog: "", + }, + { + name: "FFLogger no logger configured", + logger: &fflog.FFLogger{}, + fields: fields{ + msg: "Debug message", + keysAndValues: nil, + }, + expectedLog: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + slog.SetLogLoggerLevel(slog.LevelDebug) + file, err := os.CreateTemp("", "") + assert.NoError(t, err) + defer func() { + _ = os.Remove(file.Name()) + }() + + log.SetOutput(file) + if tt.logger != nil && tt.logger.LegacyLogger != nil { + tt.logger.LegacyLogger.SetOutput(file) + } + + if tt.fields.keysAndValues != nil { + tt.logger.Debug(tt.fields.msg, tt.fields.keysAndValues...) + } else { + tt.logger.Debug(tt.fields.msg) + } + + content, err := os.ReadFile(file.Name()) + assert.NoError(t, err) + if len(string(content)) >= 21 { + actualWithoutTimestamp := string(content)[20:] + assert.Equal(t, tt.expectedLog, actualWithoutTimestamp) + } else { + assert.Equal(t, tt.expectedLog, string(content)) + } + }) + } +} + +func TestConvertToFFLogger(t *testing.T) { + l := log.New(os.Stdout, "", 0) + ffl := fflog.ConvertToFFLogger(l) + assert.Equal(t, ffl.GetLogLogger(slog.LevelInfo), l) +} + +func TestGetLogLogger(t *testing.T) { + l := log.New(os.Stdout, "", 0) + ffl := &fflog.FFLogger{ + LeveledLogger: slog.Default(), + LegacyLogger: l, + } + + ffl2 := &fflog.FFLogger{ + LegacyLogger: l, + } + + assert.NotEqual(t, ffl.GetLogLogger(slog.LevelInfo), l) + assert.Equal(t, ffl2.GetLogLogger(slog.LevelInfo), l) +} diff --git a/variation_test.go b/variation_test.go index 4035c979d3c..8882bd21b76 100644 --- a/variation_test.go +++ b/variation_test.go @@ -5,13 +5,8 @@ import ( "encoding/json" "errors" "fmt" - "log" - "os" - "runtime" - "testing" - "time" - "github.com/stretchr/testify/assert" + "github.com/thejerf/slogassert" "github.com/thomaspoignant/go-feature-flag/exporter" "github.com/thomaspoignant/go-feature-flag/exporter/fileexporter" "github.com/thomaspoignant/go-feature-flag/exporter/logsexporter" @@ -24,6 +19,13 @@ import ( "github.com/thomaspoignant/go-feature-flag/testutils" "github.com/thomaspoignant/go-feature-flag/testutils/flagv1" "github.com/thomaspoignant/go-feature-flag/testutils/testconvert" + "github.com/thomaspoignant/go-feature-flag/utils/fflog" + "log/slog" + "os" + "runtime" + "strings" + "testing" + "time" ) type cacheMock struct { @@ -45,7 +47,7 @@ func (c *cacheMock) GetLatestUpdateDate() time.Time { func (c *cacheMock) ConvertToFlagStruct(loadedFlags []byte, fileFormat string) (map[string]dto.DTO, error) { return nil, nil } -func (c *cacheMock) UpdateCache(newFlags map[string]dto.DTO, log *log.Logger) error { +func (c *cacheMock) UpdateCache(newFlags map[string]dto.DTO, _ *fflog.FFLogger) error { return nil } func (c *cacheMock) Close() {} @@ -100,7 +102,7 @@ func TestBoolVariation(t *testing.T) { }, want: true, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"true\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="true", variation="SdkDefault"`, }, { name: "Get error when cache not init", @@ -114,7 +116,7 @@ func TestBoolVariation(t *testing.T) { }, want: true, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"true\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="true", variation="SdkDefault"`, }, { name: "Get default value with key not exist", @@ -126,7 +128,7 @@ func TestBoolVariation(t *testing.T) { }, want: true, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"true\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="true", variation="SdkDefault"`, }, { name: "Get default value, rule not apply", @@ -158,7 +160,7 @@ func TestBoolVariation(t *testing.T) { }, want: true, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"true\", variation=\"Default\"\n", + expectedLog: `user="random-key", flag="test-flag", value="true", variation="Default"`, }, { name: "Get true value, rule apply", @@ -190,7 +192,7 @@ func TestBoolVariation(t *testing.T) { }, want: true, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"true\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="true", variation="True"`, }, { name: "Get false value, rule apply", @@ -222,7 +224,7 @@ func TestBoolVariation(t *testing.T) { }, want: false, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"false\", variation=\"False\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="false", variation="False"`, }, { name: "Get default value, when rule apply and not right type", @@ -254,7 +256,7 @@ func TestBoolVariation(t *testing.T) { }, want: true, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"true\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="true", variation="SdkDefault"`, }, { name: "No exported log", @@ -287,7 +289,7 @@ func TestBoolVariation(t *testing.T) { }, want: true, wantErr: false, - expectedLog: "^$", + expectedLog: "", }, { name: "Get sdk default value if offline", @@ -307,9 +309,8 @@ func TestBoolVariation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -317,14 +318,14 @@ func TestBoolVariation(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } @@ -332,8 +333,17 @@ func TestBoolVariation(t *testing.T) { if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { @@ -344,7 +354,6 @@ func TestBoolVariation(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } @@ -401,7 +410,7 @@ func TestBoolVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"true\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="true", variation="SdkDefault"`, }, { name: "Get error when cache not init", @@ -414,7 +423,7 @@ func TestBoolVariationDetails(t *testing.T) { errors.New("impossible to read the toggle before the initialisation")), }, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"true\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="true", variation="SdkDefault"`, }, { name: "Get default value with key not exist", @@ -425,7 +434,7 @@ func TestBoolVariationDetails(t *testing.T) { cacheMock: NewCacheMock(&flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists")), }, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"true\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="true", variation="SdkDefault"`, }, { name: "Get default value, rule not apply", @@ -464,7 +473,7 @@ func TestBoolVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"true\", variation=\"Default\"\n", + expectedLog: `user="random-key", flag="test-flag", value="true", variation="Default"`, }, { name: "Get true value, rule apply", @@ -506,7 +515,7 @@ func TestBoolVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"true\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="true", variation="True"`, }, { name: "Get rule name on metadata, rule apply", @@ -548,7 +557,7 @@ func TestBoolVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"true\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="true", variation="True"`, }, { name: "Get no rule name on metadata, rule apply has not name", @@ -586,7 +595,7 @@ func TestBoolVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"true\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="true", variation="True"`, }, { name: "Get false value, rule apply", @@ -628,7 +637,7 @@ func TestBoolVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"false\", variation=\"False\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="false", variation="False"`, }, { name: "Get default value, when rule apply and not right type", @@ -667,7 +676,7 @@ func TestBoolVariationDetails(t *testing.T) { TrackEvents: true, }, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"true\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="true", variation="SdkDefault"`, }, { name: "Get sdk default value if offline", @@ -693,9 +702,8 @@ func TestBoolVariationDetails(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -703,14 +711,14 @@ func TestBoolVariationDetails(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } @@ -718,8 +726,17 @@ func TestBoolVariationDetails(t *testing.T) { if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { @@ -730,7 +747,6 @@ func TestBoolVariationDetails(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } @@ -781,7 +797,7 @@ func TestFloat64Variation(t *testing.T) { }, want: 120.12, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"120.12\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="120.12", variation="SdkDefault"`, }, { name: "Get error when cache not init", @@ -795,7 +811,7 @@ func TestFloat64Variation(t *testing.T) { }, want: 118.12, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"118.12\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="118.12", variation="SdkDefault"`, }, { name: "Get default value with key not exist", @@ -807,7 +823,7 @@ func TestFloat64Variation(t *testing.T) { }, want: 118.12, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"118.12\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="118.12", variation="SdkDefault"`, }, { name: "Get default value, rule not apply", @@ -839,7 +855,7 @@ func TestFloat64Variation(t *testing.T) { }, want: 119.12, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"119.12\", variation=\"Default\"\n", + expectedLog: `user="random-key", flag="test-flag", value="119.12", variation="Default"`, }, { name: "Get true value, rule apply", @@ -871,7 +887,7 @@ func TestFloat64Variation(t *testing.T) { }, want: 120.12, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"120.12\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="120.12", variation="True"`, }, { name: "Get false value, rule apply", @@ -903,7 +919,7 @@ func TestFloat64Variation(t *testing.T) { }, want: 121.12, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"121.12\", variation=\"False\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="121.12", variation="False"`, }, { name: "Get default value, when rule apply and not right type", @@ -935,7 +951,7 @@ func TestFloat64Variation(t *testing.T) { }, want: 118.12, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"118.12\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="118.12", variation="SdkDefault"`, }, { name: "No exported log", @@ -968,7 +984,7 @@ func TestFloat64Variation(t *testing.T) { }, want: 120.12, wantErr: false, - expectedLog: "^$", + expectedLog: "", }, { name: "Get sdk default value if offline", @@ -988,9 +1004,8 @@ func TestFloat64Variation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -998,14 +1013,14 @@ func TestFloat64Variation(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } @@ -1013,8 +1028,17 @@ func TestFloat64Variation(t *testing.T) { if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { assert.Error(t, err, "Float64Variation() error = %v, wantErr %v", err, tt.wantErr) @@ -1024,7 +1048,6 @@ func TestFloat64Variation(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } @@ -1081,7 +1104,7 @@ func TestFloat64VariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"120.12\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="120.12", variation="SdkDefault"`, }, { name: "Get error when cache not init", @@ -1094,7 +1117,7 @@ func TestFloat64VariationDetails(t *testing.T) { errors.New("impossible to read the toggle before the initialisation")), }, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"118.12\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="118.12", variation="SdkDefault"`, }, { name: "Get default value with key not exist", @@ -1105,7 +1128,7 @@ func TestFloat64VariationDetails(t *testing.T) { cacheMock: NewCacheMock(&flag.InternalFlag{}, errors.New("flag [key-not-exist] does not exists")), }, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"118.12\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="118.12", variation="SdkDefault"`, }, { name: "Get default value, rule not apply", @@ -1144,7 +1167,7 @@ func TestFloat64VariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"119.12\", variation=\"Default\"\n", + expectedLog: `user="random-key", flag="test-flag", value="119.12", variation="Default"`, }, { name: "Get true value, rule apply", @@ -1186,7 +1209,7 @@ func TestFloat64VariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"120.12\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="120.12", variation="True"`, }, { name: "Get false value, rule apply", @@ -1228,7 +1251,7 @@ func TestFloat64VariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"121.12\", variation=\"False\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="121.12", variation="False"`, }, { name: "Get default value, when rule apply and not right type", @@ -1266,7 +1289,7 @@ func TestFloat64VariationDetails(t *testing.T) { Reason: flag.ReasonDefault, }, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"118.12\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="118.12", variation="SdkDefault"`, }, { name: "Get sdk default value if offline", @@ -1292,9 +1315,8 @@ func TestFloat64VariationDetails(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -1302,14 +1324,14 @@ func TestFloat64VariationDetails(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } @@ -1317,8 +1339,17 @@ func TestFloat64VariationDetails(t *testing.T) { if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { assert.Error(t, err, "Float64Variation() error = %v, wantErr %v", err, tt.wantErr) @@ -1328,7 +1359,6 @@ func TestFloat64VariationDetails(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } @@ -1379,7 +1409,7 @@ func TestJSONArrayVariation(t *testing.T) { }, want: []interface{}{"toto"}, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"\\[toto\\]\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="[toto]", variation="SdkDefault"`, }, { name: "Get error when cache not init", @@ -1437,7 +1467,7 @@ func TestJSONArrayVariation(t *testing.T) { }, want: []interface{}{"default"}, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"\\[default\\]\"\n", + expectedLog: `user="random-key", flag="test-flag", value="[default]", variation="Default"`, }, { name: "Get true value, rule apply", @@ -1469,7 +1499,7 @@ func TestJSONArrayVariation(t *testing.T) { }, want: []interface{}{"true"}, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"\\[true\\]\"\n", + expectedLog: `user="random-key", flag="test-flag", value="[true]", variation="True"`, }, { name: "Get false value, rule apply", @@ -1494,7 +1524,7 @@ func TestJSONArrayVariation(t *testing.T) { }, want: []interface{}{"false"}, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"\\[false\\]\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="[false]", variation="False"`, }, { name: "Get default value, when rule apply and not right type", @@ -1552,7 +1582,7 @@ func TestJSONArrayVariation(t *testing.T) { }, want: []interface{}{"true"}, wantErr: false, - expectedLog: "^$", + expectedLog: "", }, { name: "Get sdk default value if offline", @@ -1572,9 +1602,8 @@ func TestJSONArrayVariation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -1582,11 +1611,12 @@ func TestJSONArrayVariation(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, - &logsexporter.Exporter{}, logger), + &logsexporter.Exporter{LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\""}, &fflog.FFLogger{LeveledLogger: logger}), } } @@ -1599,12 +1629,20 @@ func TestJSONArrayVariation(t *testing.T) { assert.Equal(t, tt.want, got, "JSONArrayVariation() got = %v, want %v", got, tt.want) if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } // clean logger ff = nil - _ = file.Close() }) } } @@ -1661,7 +1699,7 @@ func TestJSONArrayVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"\\[toto\\]\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="[toto]", variation="SdkDefault"`, }, { name: "Get error when cache not init", @@ -1724,7 +1762,7 @@ func TestJSONArrayVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"\\[default\\]\"\n", + expectedLog: `user="random-key", flag="test-flag", value="[default]", variation="Default"`, }, { name: "Get true value, rule apply", @@ -1766,7 +1804,7 @@ func TestJSONArrayVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"\\[true\\]\"\n", + expectedLog: `user="random-key", flag="test-flag", value="[true]", variation="True"`, }, { name: "Get false value, rule apply", @@ -1798,7 +1836,7 @@ func TestJSONArrayVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"\\[false\\]\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="[false]", variation="False"`, }, { name: "Get default value, when rule apply and not right type", @@ -1862,9 +1900,8 @@ func TestJSONArrayVariationDetails(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -1872,11 +1909,12 @@ func TestJSONArrayVariationDetails(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, - &logsexporter.Exporter{}, logger), + &logsexporter.Exporter{LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\""}, &fflog.FFLogger{LeveledLogger: logger}), } } @@ -1889,12 +1927,20 @@ func TestJSONArrayVariationDetails(t *testing.T) { assert.Equal(t, tt.want, got, "JSONArrayVariation() got = %v, want %v", got, tt.want) if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } // clean logger ff = nil - _ = file.Close() }) } } @@ -1945,7 +1991,7 @@ func TestJSONVariation(t *testing.T) { }, want: map[string]interface{}{"default-notkey": true}, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"map\\[default-notkey:true\\]\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="map[default-notkey:true]", variation="SdkDefault"`, }, { name: "Get error when cache not init", @@ -1959,7 +2005,7 @@ func TestJSONVariation(t *testing.T) { }, want: map[string]interface{}{"default-notkey": true}, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"map\\[default-notkey:true\\]\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="map[default-notkey:true]", variation="SdkDefault"`, }, { name: "Get default value with key not exist", @@ -1971,7 +2017,7 @@ func TestJSONVariation(t *testing.T) { }, want: map[string]interface{}{"default-notkey": true}, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"map\\[default-notkey:true\\]\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="map[default-notkey:true]", variation="SdkDefault"`, }, { name: "Get default value, rule not apply", @@ -2003,7 +2049,7 @@ func TestJSONVariation(t *testing.T) { }, want: map[string]interface{}{"default": true}, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"map\\[default:true\\]\", variation=\"Default\"\n", + expectedLog: `user="random-key", flag="test-flag", value="map[default:true]", variation="Default"`, }, { name: "Get true value, rule apply", @@ -2035,7 +2081,7 @@ func TestJSONVariation(t *testing.T) { }, want: map[string]interface{}{"true": true}, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"map\\[true:true\\]\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="map[true:true]", variation="True"`, }, { name: "Get false value, rule apply", @@ -2067,7 +2113,7 @@ func TestJSONVariation(t *testing.T) { }, want: map[string]interface{}{"false": true}, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"map\\[false:true\\]\", variation=\"False\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="map[false:true]", variation="False"`, }, { name: "Get default value, when rule apply and not right type", @@ -2099,7 +2145,7 @@ func TestJSONVariation(t *testing.T) { }, want: map[string]interface{}{"default-notkey": true}, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"map\\[default-notkey:true\\]\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="map[default-notkey:true]", variation="SdkDefault"`, }, { name: "Get sdk default value if offline", @@ -2119,9 +2165,8 @@ func TestJSONVariation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -2129,14 +2174,14 @@ func TestJSONVariation(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } @@ -2144,8 +2189,17 @@ func TestJSONVariation(t *testing.T) { if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { @@ -2156,7 +2210,6 @@ func TestJSONVariation(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } @@ -2196,7 +2249,7 @@ func TestJSONVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"map\\[default-notkey:true\\]\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="map[default-notkey:true]", variation="SdkDefault"`, }, { name: "Get default value, rule not apply", @@ -2235,7 +2288,7 @@ func TestJSONVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"map\\[default:true\\]\", variation=\"Default\"\n", + expectedLog: `user="random-key", flag="test-flag", value="map[default:true]", variation="Default"`, }, { name: "Get true value, rule apply", @@ -2277,7 +2330,7 @@ func TestJSONVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"map\\[true:true\\]\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="map[true:true]", variation="True"`, }, { name: "Get false value, rule apply", @@ -2319,7 +2372,7 @@ func TestJSONVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"map\\[false:true\\]\", variation=\"False\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="map[false:true]", variation="False"`, }, { name: "Get sdk default value if offline", @@ -2345,9 +2398,8 @@ func TestJSONVariationDetails(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -2355,14 +2407,14 @@ func TestJSONVariationDetails(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } @@ -2370,8 +2422,17 @@ func TestJSONVariationDetails(t *testing.T) { if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { @@ -2382,7 +2443,6 @@ func TestJSONVariationDetails(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } @@ -2433,7 +2493,7 @@ func TestStringVariation(t *testing.T) { }, want: "default-notkey", wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"default-notkey\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="default-notkey", variation="SdkDefault"`, }, { name: "Get error when cache not init", @@ -2447,7 +2507,7 @@ func TestStringVariation(t *testing.T) { }, want: "default-notkey", wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"default-notkey\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="default-notkey", variation="SdkDefault"`, }, { name: "Get default value with key not exist", @@ -2459,7 +2519,7 @@ func TestStringVariation(t *testing.T) { }, want: "default-notkey", wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"default-notkey\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="default-notkey", variation="SdkDefault"`, }, { @@ -2492,7 +2552,7 @@ func TestStringVariation(t *testing.T) { }, want: "default", wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"default\", variation=\"Default\"\n", + expectedLog: `user="random-key", flag="test-flag", value="default", variation="Default"`, }, { name: "Get true value, rule apply", @@ -2524,7 +2584,7 @@ func TestStringVariation(t *testing.T) { }, want: "true", wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"true\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="true", variation="True"`, }, { name: "Get false value, rule apply", @@ -2556,7 +2616,7 @@ func TestStringVariation(t *testing.T) { }, want: "false", wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"false\", variation=\"False\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="false", variation="False"`, }, { name: "Get default value, when rule apply and not right type", @@ -2588,7 +2648,7 @@ func TestStringVariation(t *testing.T) { }, want: "default-notkey", wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"default-notkey\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="default-notkey", variation="SdkDefault"`, }, { name: "Get sdk default value if offline", @@ -2608,9 +2668,8 @@ func TestStringVariation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -2618,22 +2677,31 @@ func TestStringVariation(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } got, err := StringVariation(tt.args.flagKey, tt.args.user, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { @@ -2644,7 +2712,6 @@ func TestStringVariation(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } @@ -2684,7 +2751,7 @@ func TestStringVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"default-notkey\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="default-notkey", variation="SdkDefault"`, }, { name: "Get default value, rule not apply", @@ -2723,7 +2790,7 @@ func TestStringVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"default\", variation=\"Default\"\n", + expectedLog: `user="random-key", flag="test-flag", value="default", variation="Default"`, }, { name: "Get true value, rule apply", @@ -2765,7 +2832,7 @@ func TestStringVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"true\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="true", variation="True"`, }, { name: "Get false value, rule apply", @@ -2807,7 +2874,7 @@ func TestStringVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"false\", variation=\"False\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="false", variation="False"`, }, { name: "Get sdk default value if offline", @@ -2833,9 +2900,8 @@ func TestStringVariationDetails(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -2843,22 +2909,31 @@ func TestStringVariationDetails(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } got, err := StringVariationDetails(tt.args.flagKey, tt.args.user, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { @@ -2869,7 +2944,6 @@ func TestStringVariationDetails(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } @@ -2920,7 +2994,7 @@ func TestIntVariation(t *testing.T) { }, want: 125, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"125\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="125", variation="SdkDefault"`, }, { name: "Get error when cache not init", @@ -2934,7 +3008,7 @@ func TestIntVariation(t *testing.T) { }, want: 118, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"118\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="118", variation="SdkDefault"`, }, { name: "Get default value with key not exist", @@ -2946,7 +3020,7 @@ func TestIntVariation(t *testing.T) { }, want: 118, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"118\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="118", variation="SdkDefault"`, }, { name: "Get default value rule not apply", @@ -2978,7 +3052,7 @@ func TestIntVariation(t *testing.T) { }, want: 119, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"119\", variation=\"Default\"\n", + expectedLog: `user="random-key", flag="test-flag", value="119", variation="Default"`, }, { name: "Get true value, rule apply", @@ -3010,7 +3084,7 @@ func TestIntVariation(t *testing.T) { }, want: 120, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"120\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="120", variation="True"`, }, { name: "Get false value, rule apply", @@ -3042,7 +3116,7 @@ func TestIntVariation(t *testing.T) { }, want: 121, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"121\", variation=\"False\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="121", variation="False"`, }, { name: "Get default value, when rule apply and not right type", @@ -3074,7 +3148,7 @@ func TestIntVariation(t *testing.T) { }, want: 118, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"118\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="118", variation="SdkDefault"`, }, { name: "Convert float to Int", @@ -3106,7 +3180,7 @@ func TestIntVariation(t *testing.T) { }, want: 120, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"120\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="120", variation="True"`, }, { name: "Get sdk default value if offline", @@ -3126,9 +3200,8 @@ func TestIntVariation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -3136,22 +3209,31 @@ func TestIntVariation(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } got, err := IntVariation(tt.args.flagKey, tt.args.user, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { @@ -3162,7 +3244,6 @@ func TestIntVariation(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } @@ -3202,7 +3283,7 @@ func TestIntVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"125\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="125", variation="SdkDefault"`, }, { name: "Get default value rule not apply", @@ -3241,7 +3322,7 @@ func TestIntVariationDetails(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"119\", variation=\"Default\"\n", + expectedLog: `user="random-key", flag="test-flag", value="119", variation="Default"`, }, { name: "Get true value, rule apply", @@ -3283,7 +3364,7 @@ func TestIntVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"120\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="120", variation="True"`, }, { name: "Get false value, rule apply", @@ -3325,7 +3406,7 @@ func TestIntVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"121\", variation=\"False\"\n", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="121", variation="False"`, }, { name: "Convert float to Int", @@ -3367,7 +3448,7 @@ func TestIntVariationDetails(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"120\", variation=\"True\"\n", + expectedLog: `user="random-key", flag="test-flag", value="120", variation="True"`, }, { name: "Get sdk default value if offline", @@ -3393,9 +3474,8 @@ func TestIntVariationDetails(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -3403,22 +3483,31 @@ func TestIntVariationDetails(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } got, err := IntVariationDetails(tt.args.flagKey, tt.args.user, tt.args.defaultValue) if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { @@ -3429,7 +3518,6 @@ func TestIntVariationDetails(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } @@ -3649,7 +3737,7 @@ func TestRawVariation(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"true\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="disable-flag", value="true", variation="SdkDefault"`, }, { name: "Get error when cache not init", @@ -3671,7 +3759,7 @@ func TestRawVariation(t *testing.T) { Cacheable: false, }, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"defaultValue\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="defaultValue", variation="SdkDefault"`, }, { name: "Get default value with key not exist", @@ -3690,7 +3778,7 @@ func TestRawVariation(t *testing.T) { ErrorCode: flag.ErrorCodeFlagNotFound, }, wantErr: true, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"key-not-exist\", value=\"123456\", variation=\"SdkDefault\"\n", + expectedLog: `user="random-key", flag="key-not-exist", value="123456", variation="SdkDefault"`, }, { name: "Get default value, rule not apply", @@ -3729,7 +3817,7 @@ func TestRawVariation(t *testing.T) { Cacheable: true, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"map\\[test:test\\]\", variation=\"Default\"", + expectedLog: `user="random-key", flag="test-flag", value="map[test:test]", variation="Default"`, }, { name: "Get true value, rule apply", @@ -3771,7 +3859,7 @@ func TestRawVariation(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"test-flag\", value=\"map\\[test2:test\\]\", variation=\"True\"", + expectedLog: `user="random-key", flag="test-flag", value="map[test2:test]", variation="True"`, }, { name: "Get false value, rule apply", @@ -3813,7 +3901,7 @@ func TestRawVariation(t *testing.T) { }, }, wantErr: false, - expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key-ssss1\", flag=\"test-flag\", value=\"map\\[test3:test\\]\", variation=\"False\"", + expectedLog: `user="random-key-ssss1", flag="test-flag", value="map[test3:test]", variation="False"`, }, { name: "No exported log", @@ -3856,7 +3944,7 @@ func TestRawVariation(t *testing.T) { }, }, wantErr: false, - expectedLog: "^$", + expectedLog: "", }, { name: "Get sdk default value if offline", @@ -3903,9 +3991,8 @@ func TestRawVariation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // init logger - file, _ := os.CreateTemp("", "log") - logger := log.New(file, "", 0) + handler := slogassert.New(t, slog.LevelInfo, nil) + logger := slog.New(handler) if !tt.args.disableInit { ff = &GoFeatureFlag{ @@ -3913,14 +4000,14 @@ func TestRawVariation(t *testing.T) { cache: tt.args.cacheMock, config: Config{ PollingInterval: 0, - Logger: logger, + LeveledLogger: logger, Offline: tt.args.offline, }, dataExporter: exporter.NewScheduler(context.Background(), 0, 0, &logsexporter.Exporter{ - LogFormat: "[{{ .FormattedDate}}] user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + + LogFormat: "user=\"{{ .UserKey}}\", flag=\"{{ .Key}}\", " + "value=\"{{ .Value}}\", variation=\"{{ .Variation}}\"", - }, logger), + }, &fflog.FFLogger{LeveledLogger: logger}), } } @@ -3928,8 +4015,17 @@ func TestRawVariation(t *testing.T) { if tt.expectedLog != "" { time.Sleep(40 * time.Millisecond) // since the log is async, we are waiting to be sure it's written - content, _ := os.ReadFile(file.Name()) - assert.Regexp(t, tt.expectedLog, string(content)) + if tt.expectedLog == "" { + handler.AssertEmpty() + } else { + handler.Assert(func(message slogassert.LogMessage) bool { + if !strings.Contains(message.Message, tt.expectedLog) { + handler.Fail("impossible to find %s in %s", tt.expectedLog, message.Message) + return false + } + return true + }) + } } if tt.wantErr { @@ -3939,7 +4035,6 @@ func TestRawVariation(t *testing.T) { // clean logger ff = nil - _ = file.Close() }) } } diff --git a/website/docs/go_module/configuration.md b/website/docs/go_module/configuration.md index ab057c70aa9..7da05b152cd 100644 --- a/website/docs/go_module/configuration.md +++ b/website/docs/go_module/configuration.md @@ -31,7 +31,7 @@ During the initialization you must give a [`ffclient.Config{}`](https://pkg.go.d ```go ffclient.Init(ffclient.Config{ PollingInterval: 3 * time.Second, - Logger: log.New(file, "/tmp/log", 0), + LeveledLogger: slog.Default(), Context: context.Background(), Environment: os.Getenv("MYAPP_ENV"), Retriever: &fileretriever.Retriever{Path: "testdata/flag-config.goff.yaml"}, diff --git a/website/docs/go_module/store_file/custom.md b/website/docs/go_module/store_file/custom.md index 7f9ac091261..f930e7f4d9d 100644 --- a/website/docs/go_module/store_file/custom.md +++ b/website/docs/go_module/store_file/custom.md @@ -29,7 +29,7 @@ The only difference with the `Retriever` interface is that the `Init` func of yo ```go type InitializableRetriever interface { Retrieve(ctx context.Context) ([]byte, error) - Init(ctx context.Context, logger *log.Logger) error + Init(ctx context.Context, logger *fflog.FFLogger) error Shutdown(ctx context.Context) error Status() retriever.Status }