Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: support for multiple exporters #2535

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion cmd/relayproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@
// Exporter is the configuration on how to export data
Exporter *ExporterConf `mapstructure:"exporter" koanf:"exporter"`

// Exporters is the exact same things than Exporter but allows to give more than 1 exporter at the time.
Exporters *[]ExporterConf `mapstructure:"exporters" koanf:"exporters"`

// Notifiers is the configuration on where to notify a flag change
Notifiers []NotifierConf `mapstructure:"notifier" koanf:"notifier"`

Expand Down Expand Up @@ -414,6 +417,28 @@
return fmt.Errorf("invalid port %d", c.ListenPort)
}

if err := c.validateRetrievers(); err != nil {
return err
}

if err := c.validateExporters(); err != nil {
return err
}

if err := c.validateNotifiers(); err != nil {
return err
}

if c.LogLevel != "" {
if _, err := zapcore.ParseLevel(c.LogLevel); err != nil {
return err
}

Check warning on line 435 in cmd/relayproxy/config/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/config/config.go#L434-L435

Added lines #L434 - L435 were not covered by tests
}

return nil
}

func (c *Config) validateRetrievers() error {
if c.Retriever == nil && c.Retrievers == nil {
return fmt.Errorf("no retriever available in the configuration")
}
Expand All @@ -432,13 +457,28 @@
}
}

// Exporter is optional
return nil
}

func (c *Config) validateExporters() error {
if c.Exporter != nil {
if err := c.Exporter.IsValid(); err != nil {
return err
}
}

if c.Exporters != nil {
for _, exporter := range *c.Exporters {
if err := exporter.IsValid(); err != nil {
return err
}
}
}

return nil
}

func (c *Config) validateNotifiers() error {
if c.Notifiers != nil {
for _, notif := range c.Notifiers {
if err := notif.IsValid(); err != nil {
Expand Down
100 changes: 100 additions & 0 deletions cmd/relayproxy/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,84 @@ func TestParseConfig_fileFromPflag(t *testing.T) {
},
wantErr: assert.NoError,
},
{
name: "Valid yaml file with multiple exporters",
fileLocation: "../testdata/config/valid-yaml-multiple-exporters.yaml",
want: &config.Config{
ListenPort: 1031,
PollingInterval: 1000,
FileFormat: "yaml",
Host: "localhost",
Retriever: &config.RetrieverConf{
Kind: "http",
URL: "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/examples/retriever_file/flags.goff.yaml",
},
Exporters: &[]config.ExporterConf{
{
Kind: "log",
},
{
Kind: "file",
OutputDir: "./",
},
},
StartWithRetrieverError: false,
Version: "1.X.X",
EnableSwagger: true,
AuthorizedKeys: config.APIKeys{
Admin: []string{
"apikey3",
},
Evaluation: []string{
"apikey1",
"apikey2",
},
},
LogLevel: "info",
},
wantErr: assert.NoError,
},
{
name: "Valid yaml file with both exporter and exporters",
fileLocation: "../testdata/config/valid-yaml-exporter-and-exporters.yaml",
want: &config.Config{
ListenPort: 1031,
PollingInterval: 1000,
FileFormat: "yaml",
Host: "localhost",
Retriever: &config.RetrieverConf{
Kind: "http",
URL: "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/examples/retriever_file/flags.goff.yaml",
},
Exporter: &config.ExporterConf{
Kind: "log",
},
Exporters: &[]config.ExporterConf{
{
Kind: "webhook",
EndpointURL: "https://example.com/webhook",
},
{
Kind: "file",
OutputDir: "./",
},
},
StartWithRetrieverError: false,
Version: "1.X.X",
EnableSwagger: true,
AuthorizedKeys: config.APIKeys{
Admin: []string{
"apikey3",
},
Evaluation: []string{
"apikey1",
"apikey2",
},
},
LogLevel: "info",
},
wantErr: assert.NoError,
},
{
name: "Valid json file",
fileLocation: "../testdata/config/valid-file.json",
Expand Down Expand Up @@ -345,6 +423,7 @@ func TestConfig_IsValid(t *testing.T) {
Retriever *config.RetrieverConf
Retrievers *[]config.RetrieverConf
Exporter *config.ExporterConf
Exporters *[]config.ExporterConf
Notifiers []config.NotifierConf
LogLevel string
Debug bool
Expand Down Expand Up @@ -477,6 +556,26 @@ func TestConfig_IsValid(t *testing.T) {
},
wantErr: assert.Error,
},
{
name: "invalid exporter in the list of exporters",
fields: fields{
ListenPort: 8080,
Retriever: &config.RetrieverConf{
Kind: "file",
Path: "../testdata/config/valid-file.yaml",
},
Exporters: &[]config.ExporterConf{
{
Kind: "webhook",
EndpointURL: "https://example.com/webhook",
},
{
Kind: "file",
},
},
},
wantErr: assert.Error,
},
{
name: "invalid notifier",
fields: fields{
Expand Down Expand Up @@ -544,6 +643,7 @@ func TestConfig_IsValid(t *testing.T) {
StartWithRetrieverError: tt.fields.StartWithRetrieverError,
Retriever: tt.fields.Retriever,
Exporter: tt.fields.Exporter,
Exporters: tt.fields.Exporters,
Notifiers: tt.fields.Notifiers,
Retrievers: tt.fields.Retrievers,
LogLevel: tt.fields.LogLevel,
Expand Down
99 changes: 71 additions & 28 deletions cmd/relayproxy/service/gofeatureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,63 +47,106 @@
logger *zap.Logger,
notifiers []notifier.Notifier,
) (*ffclient.GoFeatureFlag, error) {
var mainRetriever retriever.Retriever
var err error

if proxyConf == nil {
return nil, fmt.Errorf("proxy config is empty")
}

mainRetriever, retrievers, err := initRetrievers(proxyConf)
if err != nil {
return nil, err
}

Check warning on line 57 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L56-L57

Added lines #L56 - L57 were not covered by tests

mainDataExporter, dataExporters, err := initExporters(proxyConf)
if err != nil {
return nil, err
}

Check warning on line 62 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L61-L62

Added lines #L61 - L62 were not covered by tests

notif, err := initNotifiers(proxyConf.Notifiers, notifiers)
if err != nil {
return nil, err
}

Check warning on line 67 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L66-L67

Added lines #L66 - L67 were not covered by tests

f := ffclient.Config{
PollingInterval: time.Duration(proxyConf.PollingInterval) * time.Millisecond,
LeveledLogger: slog.New(slogzap.Option{Level: slog.LevelDebug, Logger: logger}.NewZapHandler()),
Context: context.Background(),
Retriever: mainRetriever,
Retrievers: retrievers,
Notifiers: notif,
FileFormat: proxyConf.FileFormat,
DataExporter: mainDataExporter,
DataExporters: dataExporters,
StartWithRetrieverError: proxyConf.StartWithRetrieverError,
EnablePollingJitter: proxyConf.EnablePollingJitter,
DisableNotifierOnInit: proxyConf.DisableNotifierOnInit,
EvaluationContextEnrichment: proxyConf.EvaluationContextEnrichment,
PersistentFlagConfigurationFile: proxyConf.PersistentFlagConfigurationFile,
}

return ffclient.New(f)
}

func initRetrievers(proxyConf *config.Config) (retriever.Retriever, []retriever.Retriever, error) {
var mainRetriever retriever.Retriever
var err error

if proxyConf.Retriever != nil {
mainRetriever, err = initRetriever(proxyConf.Retriever)
if err != nil {
return nil, err
return nil, nil, err

Check warning on line 96 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L96

Added line #L96 was not covered by tests
}
}

// Manage if we have more than 1 retriever
retrievers := make([]retriever.Retriever, 0)
if proxyConf.Retrievers != nil {
for _, r := range *proxyConf.Retrievers {
currentRetriever, err := initRetriever(&r)
if err != nil {
return nil, err
return nil, nil, err

Check warning on line 105 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L105

Added line #L105 was not covered by tests
}
retrievers = append(retrievers, currentRetriever)
}
}

var exp ffclient.DataExporter
return mainRetriever, retrievers, nil
}

func initExporters(proxyConf *config.Config) (ffclient.DataExporter, []ffclient.DataExporter, error) {
var mainDataExporter ffclient.DataExporter
var err error

if proxyConf.Exporter != nil {
exp, err = initDataExporter(proxyConf.Exporter)
mainDataExporter, err = initDataExporter(proxyConf.Exporter)
if err != nil {
return nil, err
return ffclient.DataExporter{}, nil, err

Check warning on line 121 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L121

Added line #L121 was not covered by tests
}
}

notif, err := initNotifier(proxyConf.Notifiers)
if err != nil {
return nil, err
if proxyConf.Exporters == nil {
return mainDataExporter, nil, nil
}
notif = append(notif, notifiers...)

f := ffclient.Config{
PollingInterval: time.Duration(proxyConf.PollingInterval) * time.Millisecond,
LeveledLogger: slog.New(slogzap.Option{Level: slog.LevelDebug, Logger: logger}.NewZapHandler()),
Context: context.Background(),
Retriever: mainRetriever,
Retrievers: retrievers,
Notifiers: notif,
FileFormat: proxyConf.FileFormat,
DataExporter: exp,
StartWithRetrieverError: proxyConf.StartWithRetrieverError,
EnablePollingJitter: proxyConf.EnablePollingJitter,
DisableNotifierOnInit: proxyConf.DisableNotifierOnInit,
EvaluationContextEnrichment: proxyConf.EvaluationContextEnrichment,
PersistentFlagConfigurationFile: proxyConf.PersistentFlagConfigurationFile,
// Initialize each exporter with its own configuration
dataExporters := make([]ffclient.DataExporter, len(*proxyConf.Exporters))
for i, e := range *proxyConf.Exporters {
dataExporters[i], err = initDataExporter(&e)
if err != nil {
return ffclient.DataExporter{}, nil, err
}

Check warning on line 135 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L134-L135

Added lines #L134 - L135 were not covered by tests
}

return ffclient.New(f)
return mainDataExporter, dataExporters, nil
}

func initNotifiers(
configNotifiers []config.NotifierConf,
additionalNotifiers []notifier.Notifier,
) ([]notifier.Notifier, error) {
notif, err := initNotifier(configNotifiers)
if err != nil {
return nil, err
}

Check warning on line 148 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L147-L148

Added lines #L147 - L148 were not covered by tests
return append(notif, additionalNotifiers...), nil
}

// initRetriever initialize the retriever based on the configuration
Expand Down
Loading
Loading