Skip to content

Commit

Permalink
fix: new changes for feature flag
Browse files Browse the repository at this point in the history
  • Loading branch information
nityanandagohain committed Feb 4, 2025
1 parent ab6ff22 commit be989bb
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 83 deletions.
2 changes: 1 addition & 1 deletion ee/featureflag/zeus/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ var basePlanFeatures = []featureflag.Feature{
var proPlanFeatures = []featureflag.Feature{
{
Name: featureflag.FeatureSSO,
IsActive: false,
IsActive: true,
},
{
Name: featureflag.FeatureCustomMetricsFunc,
Expand Down
2 changes: 1 addition & 1 deletion ee/featureflag/zeus/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
return &Provider{}, nil
}

func (p *Provider) GetFeatures() []featureflag.Feature {
func (p *Provider) GetFeatures(orgId string) []featureflag.Feature {
// TODO : get the features from zeus
return basePlanFeatures
}
65 changes: 39 additions & 26 deletions pkg/featureflag/base/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,68 @@ import (

var defaultFeatures = []featureflag.Feature{
{
Name: featureflag.FeatureOSS,
IsActive: true,
Name: featureflag.FeatureOSS,
IsActive: true,
IsChangeable: false,
},
{
Name: featureflag.FeatureDisableUpsell,
IsActive: false,
Name: featureflag.FeatureDisableUpsell,
IsActive: false,
IsChangeable: false,
},
{
Name: featureflag.FeatureCustomMetricsFunc,
IsActive: false,
Name: featureflag.FeatureCustomMetricsFunc,
IsActive: false,
IsChangeable: false,
},
{
Name: featureflag.FeatureQueryBuilderPanels,
IsActive: true,
Name: featureflag.FeatureQueryBuilderPanels,
IsActive: true,
IsChangeable: false,
},
{
Name: featureflag.FeatureQueryBuilderAlerts,
IsActive: true,
Name: featureflag.FeatureQueryBuilderAlerts,
IsActive: true,
IsChangeable: false,
},
{
Name: featureflag.FeatureUseSpanMetrics,
IsActive: false,
Name: featureflag.FeatureUseSpanMetrics,
IsActive: false,
IsChangeable: false,
},
{
Name: featureflag.FeatureAlertChannelSlack,
IsActive: true,
Name: featureflag.FeatureAlertChannelSlack,
IsActive: true,
IsChangeable: false,
},
{
Name: featureflag.FeatureAlertChannelWebhook,
IsActive: true,
Name: featureflag.FeatureAlertChannelWebhook,
IsActive: true,
IsChangeable: false,
},
{
Name: featureflag.FeatureAlertChannelPagerduty,
IsActive: true,
Name: featureflag.FeatureAlertChannelPagerduty,
IsActive: true,
IsChangeable: false,
},
{
Name: featureflag.FeatureAlertChannelOpsgenie,
IsActive: true,
Name: featureflag.FeatureAlertChannelOpsgenie,
IsActive: true,
IsChangeable: false,
},
{
Name: featureflag.FeatureAlertChannelEmail,
IsActive: true,
Name: featureflag.FeatureAlertChannelEmail,
IsActive: true,
IsChangeable: false,
},
{
Name: featureflag.FeatureAlertChannelMsTeams,
IsActive: false,
Name: featureflag.FeatureAlertChannelMsTeams,
IsActive: false,
IsChangeable: false,
},
{
Name: featureflag.FeatureAnomalyDetection,
IsActive: false,
Name: featureflag.FeatureAnomalyDetection,
IsActive: false,
IsChangeable: false,
},
}
5 changes: 2 additions & 3 deletions pkg/featureflag/base/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ type Provider struct {
}

func NewFactory() factory.ProviderFactory[featureflag.FeatureFlag, featureflag.Config] {
return factory.NewProviderFactory(factory.MustNewName("base-features"), New)
return factory.NewProviderFactory(factory.MustNewName("base"), New)
}

func New(ctx context.Context, providerSettings factory.ProviderSettings, config featureflag.Config) (featureflag.FeatureFlag, error) {
return &Provider{}, nil
}

func (p *Provider) GetFeatures() []featureflag.Feature {
// TODO : update from sqlite
func (p *Provider) GetFeatures(orgId string) []featureflag.Feature {
return defaultFeatures
}
2 changes: 0 additions & 2 deletions pkg/featureflag/config.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package featureflag

type Config struct {
// Enable is a list of features to enable
Enable []string `mapstructure:"enable"`
}

func (c Config) Validate() error {
Expand Down
56 changes: 48 additions & 8 deletions pkg/featureflag/featureflag.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
package featureflag

import (
"database/sql/driver"
"fmt"

"github.com/uptrace/bun"
)

// FeatureFlag is the interface that all feature flag providers must implement
type FeatureFlag interface {
//pass context
GetFeatures() []Feature
GetFeatures(orgId string) []Feature
}

// Feature is the struct that holds the feature flag data
type Feature struct {
Name Flag
Description string
Stage Stage
IsActive bool
bun.BaseModel `bun:"table:feature_flag"` // This specifies the table name as "features"

OrgId string `bun:"org_id"`
Name Flag `bun:"name"`
Description string `bun:"description"`
Stage Stage `bun:"stage"`
IsActive bool `bun:"is_active"`
IsChanged bool `bun:"is_changed"`
IsChangeable bool `bun:"is_changeable"`

RequiresRestart bool `bun:"requires_restart"`
}

// For future use
// eg:- from OSS to enterprise
RequiresRestart bool
// Add a method to Feature for comparison
func (f Feature) Equals(other Feature) bool {
return f.Name == other.Name &&
f.OrgId == other.OrgId &&
f.Description == other.Description &&
f.Stage == other.Stage &&
f.IsActive == other.IsActive &&
f.IsChangeable == other.IsChangeable &&
f.RequiresRestart == other.RequiresRestart
}

// It represents the stage of the feature
Expand All @@ -31,6 +52,25 @@ func (s Stage) String() string {
return s.s
}

func (s *Stage) Scan(value interface{}) error {
if value == nil {
*s = Stage{}
return nil
}

strValue, ok := value.(string)
if !ok {
return fmt.Errorf("expected string but got %T", value)
}

*s = NewStage(strValue)
return nil
}

func (s Stage) Value() (driver.Value, error) {
return s.String(), nil
}

var (
StageAlpha = NewStage("alpha")
StageBeta = NewStage("beta")
Expand Down
26 changes: 26 additions & 0 deletions pkg/featureflag/flags.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package featureflag

import (
"database/sql/driver"
"fmt"
)

type Flag struct {
s string
}
Expand All @@ -12,6 +17,27 @@ func (f Flag) String() string {
return f.s
}

// Implement the sql.Scanner interface
func (f *Flag) Scan(value interface{}) error {
if value == nil {
*f = Flag{}
return nil
}

strValue, ok := value.(string)
if !ok {
return fmt.Errorf("expected string but got %T", value)
}

*f = NewFlag(strValue)
return nil
}

// Implement the driver.Valuer interface
func (f Flag) Value() (driver.Value, error) {
return f.String(), nil
}

// Flag variables
var (
FeatureOSS = NewFlag("oss")
Expand Down
94 changes: 63 additions & 31 deletions pkg/featureflag/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package featureflag

import (
"context"
"log/slog"
"sync"
"time"

"github.com/jmoiron/sqlx"
"github.com/uptrace/bun"
"go.signoz.io/signoz/pkg/factory"
)

Expand All @@ -12,60 +15,89 @@ var _ factory.Service = (*FeatureFlagManager)(nil)

// FeatureFlagManager implements the FeatureFlagService interface
type FeatureFlagManager struct {
logger *slog.Logger
providers []FeatureFlag
storage *FeatureStorage
features map[Flag]Feature
storage Store
ticker *time.Ticker
cancel context.CancelFunc
}

// NewFeatureFlagManager creates a new FeatureFlagManager instance
func NewFeatureFlagManager(ctx context.Context, sqlxDB *sqlx.DB, factories ...FeatureFlag) *FeatureFlagManager {
func NewFeatureFlagManager(ctx context.Context, logger *slog.Logger, bunDB *bun.DB, factories ...FeatureFlag) *FeatureFlagManager {
ctx, cancel := context.WithCancel(ctx)
return &FeatureFlagManager{
logger: logger,
providers: factories,
storage: NewFeatureStorage(sqlxDB),
features: make(map[Flag]Feature),
storage: NewFeatureStorage(bunDB),
ticker: time.NewTicker(24 * time.Hour), // not taking from config
cancel: cancel,
}
}

// Start initializes the feature flag manager
func (fm *FeatureFlagManager) Start(ctx context.Context) error {
fm.InitializeFeatures()
// save as well
go fm.SaveAllFeatures()
// Run RefreshFeatureFlags once at the start
fm.RefreshFeatureFlags()

// Set up a ticker to run RefreshFeatureFlags periodically
go func() {
for {
select {
case <-ctx.Done():
return
case <-fm.ticker.C:
fm.RefreshFeatureFlags()
}
}
}()

return nil
}

// Stop performs any necessary cleanup
func (fm *FeatureFlagManager) Stop(ctx context.Context) error {
// Implement any cleanup logic if necessary
// Stop the ticker
fm.ticker.Stop()

// idempotent call to make sure we stop the context
fm.cancel()
return nil
}

// GetAllFeatures returns all features
func (fm *FeatureFlagManager) GetAllFeatures() []Feature {
var featureList []Feature
for _, feature := range fm.features {
featureList = append(featureList, feature)
func (fm *FeatureFlagManager) RefreshFeatureFlags() {
var wg sync.WaitGroup
orgIds, err := fm.storage.ListOrgIDs(context.Background())
if err != nil {
fm.logger.Error("Error listing orgIds", "error", err)
return
}
return featureList
for _, orgId := range orgIds {
for _, provider := range fm.providers {
wg.Add(1)
go func(orgId string, provider FeatureFlag) {
defer wg.Done()
features := provider.GetFeatures(orgId)
err := fm.storage.SaveFeatureFlags(context.Background(), orgId, features)
if err != nil {
fm.logger.Error("Failed to save features", "orgId", orgId, "error", err)
}
}(orgId, provider)
}
}
wg.Wait()
}

// GetFeature returns a specific feature by flag
func (fm *FeatureFlagManager) GetFeature(flag Flag) Feature {
return fm.features[flag]
// GetFeatureFlags returns all features for an org
func (fm *FeatureFlagManager) ListFeatureFlags(ctx context.Context, orgId string) ([]Feature, error) {
return fm.storage.ListFeatureFlags(ctx, orgId)
}

// SaveAllFeatures saves all features to storage
func (fm *FeatureFlagManager) SaveAllFeatures() error {
features := fm.GetAllFeatures()
return fm.storage.SaveFeatures(features)
// GetFeatureFlag returns a specific feature by flag
func (fm *FeatureFlagManager) GetFeatureFlag(ctx context.Context, orgId string, flag Flag) (Feature, error) {
return fm.storage.GetFeatureFlag(ctx, orgId, flag)
}

// InitializeFeatures initializes features from all providers
func (fm *FeatureFlagManager) InitializeFeatures() {
for _, provider := range fm.providers {
features := provider.GetFeatures()
for _, feature := range features {
fm.features[feature.Name] = feature
}
}
// UpdateFeatureFlag updates a specific feature by flag for an org
func (fm *FeatureFlagManager) UpdateFeatureFlag(ctx context.Context, orgId string, flag Flag, feature Feature) error {
return fm.storage.UpdateFeatureFlag(ctx, orgId, flag, feature)
}
Loading

0 comments on commit be989bb

Please sign in to comment.