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 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
3 changes: 1 addition & 2 deletions .github/workflows/unassign-issue.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ jobs:
const staleIssues = await github.paginate(github.rest.issues.listForRepo, {
owner, repo,
state: 'open',
labels: staleLabel,
assignee: '*'
hoangnv-bkhn marked this conversation as resolved.
Show resolved Hide resolved
labels: staleLabel
});

for (const issue of staleIssues) {
Expand Down
48 changes: 41 additions & 7 deletions cmd/relayproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ type Config struct {
// 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 @@ -306,6 +309,28 @@ func (c *Config) IsValid() error {
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
}
}

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 @@ -324,26 +349,35 @@ func (c *Config) IsValid() error {
}
}

// 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 {
return err
}
}
}
if c.LogLevel != "" {
if _, err := zapcore.ParseLevel(c.LogLevel); err != nil {
return err
}
}

return nil
}

Expand Down
102 changes: 102 additions & 0 deletions cmd/relayproxy/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,86 @@ 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,
RestAPITimeout: 5000,
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,
RestAPITimeout: 5000,
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 @@ -323,6 +403,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 @@ -454,6 +535,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 @@ -508,6 +609,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
97 changes: 69 additions & 28 deletions cmd/relayproxy/service/gofeatureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,62 +41,103 @@ func NewGoFeatureFlagClient(
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
}

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

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

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,
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
}
}

// 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
}
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
}
}

notif, err := initNotifier(proxyConf.Notifiers)
if err != nil {
return nil, err
dataExporters := make([]ffclient.DataExporter, 0)
if proxyConf.Exporters != nil {
for _, e := range *proxyConf.Exporters {
currentExporter, err := initDataExporter(&e)
if err != nil {
return ffclient.DataExporter{}, nil, err
}
dataExporters = append(dataExporters, currentExporter)
}
}
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,
EvaluationContextEnrichment: proxyConf.EvaluationContextEnrichment,
PersistentFlagConfigurationFile: proxyConf.PersistentFlagConfigurationFile,
}
return mainDataExporter, dataExporters, nil
}

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

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