Compare commits

...

4 Commits

Author SHA1 Message Date
grandwizard28
9a79764624 feat(flagger): add license and zeus implementations 2025-04-08 16:40:14 +05:30
grandwizard28
62c880faa0 feat(flagger): add a licensing flagger 2025-04-08 13:16:00 +05:30
grandwizard28
d48425d236 feat(openfeature): introduce flagger 2025-04-08 13:07:11 +05:30
grandwizard28
efdede1e25 feat(openfeature): introduce flagger 2025-04-08 13:07:08 +05:30
15 changed files with 1224 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
package licenseprovider
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/open-feature/go-sdk/openfeature"
)
type provider struct {
config flagger.Config
settings factory.ScopedProviderSettings
registry featuretypes.Registry
licensing licensing.Licensing
}
func NewFactory(registry featuretypes.Registry, licensing licensing.Licensing) factory.ProviderFactory[flagger.Provider, flagger.Config] {
return factory.NewProviderFactory(factory.MustNewName("license"), func(ctx context.Context, providerSettings factory.ProviderSettings, config flagger.Config) (flagger.Provider, error) {
return New(ctx, providerSettings, config, registry, licensing)
})
}
func New(ctx context.Context, providerSettings factory.ProviderSettings, config flagger.Config, registry featuretypes.Registry, licensing licensing.Licensing) (flagger.Provider, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/ee/flagger/licenseprovider")
return &provider{
config: config,
settings: settings,
registry: registry,
licensing: licensing,
}, nil
}
func (provider *provider) Metadata() openfeature.Metadata {
return openfeature.Metadata{
Name: "license",
}
}
func (provider *provider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
feature, detail, err := provider.registry.GetByNameString(flag)
if err != nil {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
license, err := provider.licensing.GetActiveLicense(ctx, evalCtx["orgID"].(valuer.UUID))
if err != nil {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
Variant: feature.DefaultVariant,
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
},
}
}
if featureValue, ok := license.FeatureVariants()[feature.Name]; ok {
value, detail, err := featuretypes.GetFeatureVariantValue[bool](feature, featureValue.Variant)
if err != nil {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.BoolResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
feature, detail, err := provider.registry.GetByNameString(flag)
if err != nil {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
license, err := provider.licensing.GetActiveLicense(ctx, evalCtx["orgID"].(valuer.UUID))
if err != nil {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
Variant: feature.DefaultVariant,
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
},
}
}
if featureValue, ok := license.FeatureVariants()[feature.Name]; ok {
value, detail, err := featuretypes.GetFeatureVariantValue[string](feature, featureValue.Variant)
if err != nil {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.StringResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
feature, detail, err := provider.registry.GetByNameString(flag)
if err != nil {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
license, err := provider.licensing.GetActiveLicense(ctx, evalCtx["orgID"].(valuer.UUID))
if err != nil {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
Variant: feature.DefaultVariant,
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
},
}
}
if featureValue, ok := license.FeatureVariants()[feature.Name]; ok {
value, detail, err := featuretypes.GetFeatureVariantValue[float64](feature, featureValue.Variant)
if err != nil {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.FloatResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
feature, detail, err := provider.registry.GetByNameString(flag)
if err != nil {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
license, err := provider.licensing.GetActiveLicense(ctx, evalCtx["orgID"].(valuer.UUID))
if err != nil {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
Variant: feature.DefaultVariant,
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
},
}
}
if featureValue, ok := license.FeatureVariants()[feature.Name]; ok {
value, detail, err := featuretypes.GetFeatureVariantValue[int64](feature, featureValue.Variant)
if err != nil {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.IntResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
feature, detail, err := provider.registry.GetByNameString(flag)
if err != nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
license, err := provider.licensing.GetActiveLicense(ctx, evalCtx["orgID"].(valuer.UUID))
if err != nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
Variant: feature.DefaultVariant,
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
},
}
}
if featureValue, ok := license.FeatureVariants()[feature.Name]; ok {
value, detail, err := featuretypes.GetFeatureVariantValue[interface{}](feature, featureValue.Variant)
if err != nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.InterfaceResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) Hooks() []openfeature.Hook {
return []openfeature.Hook{}
}
func (provider *provider) List(ctx context.Context, evalCtx featuretypes.EvaluationContext) ([]*featuretypes.GettableFeature, error) {
license, err := provider.licensing.GetActiveLicense(ctx, evalCtx.OrgID())
if err != nil {
return nil, err
}
return featuretypes.NewGettableFeatures(provider.registry.List(), license.FeatureVariants()), nil
}

View File

@@ -0,0 +1 @@
package zeusprovider

1
go.mod
View File

@@ -35,6 +35,7 @@ require (
github.com/knadh/koanf/v2 v2.1.1
github.com/mailru/easyjson v0.7.7
github.com/mattn/go-sqlite3 v1.14.24
github.com/open-feature/go-sdk v1.14.1
github.com/open-telemetry/opamp-go v0.5.0
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.111.0
github.com/opentracing/opentracing-go v1.2.0

3
go.sum
View File

@@ -348,6 +348,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -717,6 +718,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo=
github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg=
github.com/open-telemetry/opamp-go v0.5.0 h1:2YFbb6G4qBkq3yTRdVb5Nfz9hKHW/ldUyex352e1J7g=
github.com/open-telemetry/opamp-go v0.5.0/go.mod h1:IMdeuHGVc5CjKSu5/oNV0o+UmiXuahoHvoZ4GOmAI9M=
github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.111.0 h1:n1p2DedLvPEN1XEx26s1PR1PCuXTgCY4Eo+kDTq7q0s=

31
pkg/flagger/config.go Normal file
View File

@@ -0,0 +1,31 @@
package flagger
import "github.com/SigNoz/signoz/pkg/factory"
var _ factory.Config = Config{}
type Config struct {
Provider string `json:"provider"`
Boolean Boolean `json:"boolean"`
}
type Boolean struct {
Enabled []string `json:"enabled"`
Disabled []string `json:"disabled"`
}
func NewConfigFactory() factory.ConfigFactory {
return factory.NewConfigFactory(factory.MustNewName("flagger"), newConfig)
}
func newConfig() factory.Config {
return &Config{
Provider: "memory",
Boolean: Boolean{},
}
}
func (c Config) Validate() error {
return nil
}

227
pkg/flagger/flagger.go Normal file
View File

@@ -0,0 +1,227 @@
package flagger
import (
"context"
"sync"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/open-feature/go-sdk/openfeature"
)
type Provider interface {
openfeature.FeatureProvider
List(ctx context.Context, evalCtx featuretypes.EvaluationContext) ([]*featuretypes.GettableFeature, error)
}
type Flagger interface {
BooleanValue(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.EvaluationContext) (bool, error)
StringValue(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.EvaluationContext) (string, error)
FloatValue(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.EvaluationContext) (float64, error)
IntValue(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.EvaluationContext) (int64, error)
ObjectValue(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.EvaluationContext) (interface{}, error)
List(ctx context.Context, evalCtx featuretypes.EvaluationContext) ([]*featuretypes.GettableFeature, error)
}
type flagger struct {
registry featuretypes.Registry
settings factory.ScopedProviderSettings
providers map[string]Provider
clients map[string]*openfeature.Client
}
func New(ctx context.Context, registry featuretypes.Registry, cfg Config, providerSettings factory.ProviderSettings, factories ...factory.ProviderFactory[Provider, Config]) (Flagger, error) {
providers := make(map[string]Provider)
clients := make(map[string]*openfeature.Client)
for _, factory := range factories {
provider, err := factory.New(ctx, providerSettings, cfg)
if err != nil {
return nil, err
}
providers[provider.Metadata().Name] = provider
clients[provider.Metadata().Name] = openfeature.NewClient(provider.Metadata().Name)
if err := openfeature.SetNamedProviderAndWait(provider.Metadata().Name, provider); err != nil {
return nil, err
}
}
return &flagger{
registry: registry,
settings: factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/flagger"),
providers: providers,
clients: clients,
}, nil
}
func (flagger *flagger) BooleanValue(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.EvaluationContext) (bool, error) {
feature, _, err := flagger.registry.Get(flag)
if err != nil {
flagger.settings.Logger().ErrorContext(ctx, "failed to get feature from registry, defaulting to false", "error", err)
return false, err
}
defaultValue, _, err := featuretypes.GetFeatureVariantValue[bool](feature, feature.DefaultVariant)
if err != nil {
// This should never happen
flagger.settings.Logger().ErrorContext(ctx, "failed to get default variant value from registry, defaulting to false", "error", err)
return false, err
}
for _, client := range flagger.clients {
featureValue, err := client.BooleanValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
if err != nil {
continue
}
if featureValue != defaultValue {
return featureValue, nil
}
}
return defaultValue, nil
}
func (flagger *flagger) StringValue(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.EvaluationContext) (string, error) {
feature, _, err := flagger.registry.Get(flag)
if err != nil {
flagger.settings.Logger().ErrorContext(ctx, "failed to get feature from registry, defaulting to empty string", "error", err)
return "", err
}
defaultValue, _, err := featuretypes.GetFeatureVariantValue[string](feature, feature.DefaultVariant)
if err != nil {
// This should never happen
flagger.settings.Logger().ErrorContext(ctx, "failed to get default variant value from registry, defaulting to empty string", "error", err)
return "", err
}
for _, client := range flagger.clients {
featureValue, err := client.StringValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
if err != nil {
continue
}
if featureValue != defaultValue {
return featureValue, nil
}
}
return defaultValue, nil
}
func (flagger *flagger) FloatValue(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.EvaluationContext) (float64, error) {
feature, _, err := flagger.registry.Get(flag)
if err != nil {
flagger.settings.Logger().ErrorContext(ctx, "failed to get feature from registry, defaulting to 0", "error", err)
return 0, err
}
defaultValue, _, err := featuretypes.GetFeatureVariantValue[float64](feature, feature.DefaultVariant)
if err != nil {
// This should never happen
flagger.settings.Logger().ErrorContext(ctx, "failed to get default variant value from registry, defaulting to 0", "error", err)
return 0, err
}
for _, client := range flagger.clients {
featureValue, err := client.FloatValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
if err != nil {
continue
}
if featureValue != defaultValue {
return featureValue, nil
}
}
return defaultValue, nil
}
func (flagger *flagger) IntValue(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.EvaluationContext) (int64, error) {
feature, _, err := flagger.registry.Get(flag)
if err != nil {
flagger.settings.Logger().ErrorContext(ctx, "failed to get feature from registry, defaulting to 0", "error", err)
return 0, err
}
defaultValue, _, err := featuretypes.GetFeatureVariantValue[int64](feature, feature.DefaultVariant)
if err != nil {
// This should never happen
flagger.settings.Logger().ErrorContext(ctx, "failed to get default variant value from registry, defaulting to 0", "error", err)
return 0, err
}
for _, client := range flagger.clients {
featureValue, err := client.IntValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
if err != nil {
continue
}
if featureValue != defaultValue {
return featureValue, nil
}
}
return defaultValue, nil
}
func (flagger *flagger) ObjectValue(ctx context.Context, flag featuretypes.Name, evalCtx featuretypes.EvaluationContext) (interface{}, error) {
feature, _, err := flagger.registry.Get(flag)
if err != nil {
flagger.settings.Logger().ErrorContext(ctx, "failed to get feature from registry, defaulting to empty slice", "error", err)
return []any{}, err
}
defaultValue, _, err := featuretypes.GetFeatureVariantValue[interface{}](feature, feature.DefaultVariant)
if err != nil {
// This should never happen
flagger.settings.Logger().ErrorContext(ctx, "failed to get default variant value from registry, defaulting to empty slice", "error", err)
return []any{}, err
}
for _, client := range flagger.clients {
featureValue, err := client.ObjectValue(ctx, flag.String(), defaultValue, evalCtx.Ctx())
if err != nil {
continue
}
if featureValue != defaultValue {
return featureValue, nil
}
}
return defaultValue, nil
}
func (flagger *flagger) List(ctx context.Context, evalCtx featuretypes.EvaluationContext) ([]*featuretypes.GettableFeature, error) {
features := make([]*featuretypes.GettableFeature, 0)
wg := sync.WaitGroup{}
mtx := sync.Mutex{}
for _, provider := range flagger.providers {
wg.Add(1)
go func(provider Provider) {
defer wg.Done()
providerFeatures, err := provider.List(ctx, evalCtx)
if err != nil {
flagger.settings.Logger().ErrorContext(ctx, "failed to get feature list from provider", "error", err)
return
}
mtx.Lock()
features = append(features, providerFeatures...)
mtx.Unlock()
}(provider)
}
wg.Wait()
return features, nil
}

View File

@@ -0,0 +1,245 @@
package memoryprovider
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/open-feature/go-sdk/openfeature"
)
type provider struct {
config flagger.Config
settings factory.ScopedProviderSettings
featureVariants map[featuretypes.Name]*featuretypes.FeatureVariant
registry featuretypes.Registry
}
func NewFactory(registry featuretypes.Registry) factory.ProviderFactory[flagger.Provider, flagger.Config] {
return factory.NewProviderFactory(factory.MustNewName("memory"), func(ctx context.Context, providerSettings factory.ProviderSettings, config flagger.Config) (flagger.Provider, error) {
return New(ctx, providerSettings, config, registry)
})
}
func New(ctx context.Context, providerSettings factory.ProviderSettings, config flagger.Config, registry featuretypes.Registry) (flagger.Provider, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/flagger/memoryprovider")
featureVariants := make(map[featuretypes.Name]*featuretypes.FeatureVariant)
for _, flag := range config.Boolean.Enabled {
name, err := featuretypes.NewName(flag)
if err != nil {
settings.Logger().Error("invalid flag name encountered, skipping", "flag", flag, "error", err)
continue
}
featureVariants[name] = &featuretypes.FeatureVariant{
Variant: featuretypes.KindBooleanVariantEnabled,
Value: true,
}
}
for _, flag := range config.Boolean.Disabled {
name, err := featuretypes.NewName(flag)
if err != nil {
settings.Logger().Error("invalid flag name encountered, skipping", "flag", flag, "error", err)
continue
}
if _, ok := featureVariants[name]; ok {
settings.Logger().Error("flag already exists and has been enabled", "flag", flag)
continue
}
featureVariants[name] = &featuretypes.FeatureVariant{
Variant: featuretypes.KindBooleanVariantDisabled,
Value: false,
}
}
return &provider{
config: config,
settings: settings,
featureVariants: featureVariants,
registry: registry,
}, nil
}
func (provider *provider) Metadata() openfeature.Metadata {
return openfeature.Metadata{
Name: "memory",
}
}
func (provider *provider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
feature, detail, err := provider.registry.GetByNameString(flag)
if err != nil {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
if featureValue, ok := provider.featureVariants[feature.Name]; ok {
value, detail, err := featuretypes.GetFeatureVariantValue[bool](feature, featureValue.Variant)
if err != nil {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.BoolResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
feature, detail, err := provider.registry.GetByNameString(flag)
if err != nil {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
if featureValue, ok := provider.featureVariants[feature.Name]; ok {
value, detail, err := featuretypes.GetFeatureVariantValue[string](feature, featureValue.Variant)
if err != nil {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.StringResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
feature, detail, err := provider.registry.GetByNameString(flag)
if err != nil {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
if featureValue, ok := provider.featureVariants[feature.Name]; ok {
value, detail, err := featuretypes.GetFeatureVariantValue[float64](feature, featureValue.Variant)
if err != nil {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.FloatResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
feature, detail, err := provider.registry.GetByNameString(flag)
if err != nil {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
if featureValue, ok := provider.featureVariants[feature.Name]; ok {
value, detail, err := featuretypes.GetFeatureVariantValue[int64](feature, featureValue.Variant)
if err != nil {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.IntResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
feature, detail, err := provider.registry.GetByNameString(flag)
if err != nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
if featureValue, ok := provider.featureVariants[feature.Name]; ok {
value, detail, err := featuretypes.GetFeatureVariantValue[interface{}](feature, featureValue.Variant)
if err != nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
return openfeature.InterfaceResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: feature.DefaultVariant,
},
}
}
func (provider *provider) Hooks() []openfeature.Hook {
return []openfeature.Hook{}
}
func (provider *provider) List(ctx context.Context, evalCtx featuretypes.EvaluationContext) ([]*featuretypes.GettableFeature, error) {
return featuretypes.NewGettableFeatures(provider.registry.List(), provider.featureVariants), nil
}

34
pkg/flagger/registry.go Normal file
View File

@@ -0,0 +1,34 @@
package flagger
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
var (
FeatureUseTracesNewSchema = featuretypes.MustNewName("use_traces_new_schema")
FeatureUseLogsNewSchema = featuretypes.MustNewName("use_logs_new_schema")
)
func MustNewRegistry() featuretypes.Registry {
registry, err := featuretypes.NewRegistry(
&featuretypes.Feature{
Name: FeatureUseTracesNewSchema,
Kind: featuretypes.KindBoolean,
Description: "Use new traces schema",
Stage: featuretypes.StageStable,
DefaultVariant: featuretypes.KindBooleanVariantDisabled,
Variants: featuretypes.NewKindBooleanFeatureVariants(),
},
&featuretypes.Feature{
Name: FeatureUseLogsNewSchema,
Kind: featuretypes.KindBoolean,
Description: "Use new logs schema",
Stage: featuretypes.StageStable,
DefaultVariant: featuretypes.KindBooleanVariantDisabled,
Variants: featuretypes.NewKindBooleanFeatureVariants(),
},
)
if err != nil {
panic(err)
}
return registry
}

View File

@@ -0,0 +1,15 @@
package licensing
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Licensing interface {
factory.Service
GetActiveLicense(context.Context, valuer.UUID) (licensetypes.License, error)
}

View File

@@ -0,0 +1,32 @@
package featuretypes
import (
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/open-feature/go-sdk/openfeature"
)
type EvaluationContext struct {
ctx openfeature.EvaluationContext
}
func NewEvaluationContext(orgID valuer.UUID) EvaluationContext {
return EvaluationContext{
ctx: openfeature.NewTargetlessEvaluationContext(map[string]interface{}{
"orgId": orgID,
}),
}
}
func (ctx EvaluationContext) Ctx() openfeature.EvaluationContext {
return ctx.ctx
}
func (ctx EvaluationContext) OrgID() valuer.UUID {
orgId, ok := ctx.ctx.Attribute("orgId").(valuer.UUID)
if !ok {
// This should never happen
return valuer.UUID{}
}
return orgId
}

View File

@@ -0,0 +1,35 @@
package featuretypes
import "github.com/SigNoz/signoz/pkg/valuer"
const (
KindBooleanVariantEnabled string = "enabled"
KindBooleanVariantDisabled string = "disabled"
)
var (
KindBoolean Kind = Kind{valuer.NewString("bool")}
KindString Kind = Kind{valuer.NewString("string")}
KindInt Kind = Kind{valuer.NewString("int")}
KindFloat Kind = Kind{valuer.NewString("float")}
KindObject Kind = Kind{valuer.NewString("object")}
)
// Kind is the kind of the feature flag.
type Kind struct{ valuer.String }
var (
// Is the feature experimental?
StageExperimental = Stage{valuer.NewString("experimental")}
// Does the feature work but is not ready for production?
StagePreview = Stage{valuer.NewString("preview")}
// Is the feature stable and ready for production?
StageStable = Stage{valuer.NewString("stable")}
// Is the feature deprecated and will be removed in the future?
StageDeprecated = Stage{valuer.NewString("deprecated")}
)
type Stage struct{ valuer.String }

View File

@@ -0,0 +1,101 @@
package featuretypes
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/open-feature/go-sdk/openfeature"
)
var (
ErrCodeFeatureNotFound = errors.MustNewCode("feature_not_found")
ErrCodeFeatureKindMismatch = errors.MustNewCode("feature_kind_mismatch")
ErrCodeFeatureVariantNotFound = errors.MustNewCode("feature_variant_not_found")
)
type GettableFeature struct {
*Feature
*FeatureVariant
}
type Feature struct {
// Name is the name of the feature flag.
Name Name `json:"name"`
// Kind is the kind of the feature flag.
Kind Kind `json:"kind"`
// Description is the description of the feature flag.
Description string `json:"description"`
// Stage is the stage of the feature flag.
Stage Stage `json:"stage"`
// DefaultVariant is the default variant of the feature flag.
DefaultVariant string `json:"defaultVariant"`
// Variants is the variants of the feature flag.
Variants map[string]any `json:"variants"`
}
type FeatureVariant struct {
// Variant is the variant of the feature flag.
Variant string `json:"variant"`
// Value is the value of the feature flag.
Value any `json:"value"`
}
func NewKindBooleanFeatureVariants() map[string]any {
return map[string]any{
KindBooleanVariantEnabled: true,
KindBooleanVariantDisabled: false,
}
}
func GetFeatureVariantValue[T any](feature *Feature, variant string) (t T, detail openfeature.ProviderResolutionDetail, err error) {
value, ok := feature.Variants[variant]
if !ok {
err = errors.Newf(errors.TypeNotFound, ErrCodeFeatureVariantNotFound, "variant %s not found for feature %s", variant, feature.Name.String())
detail = openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
Variant: feature.DefaultVariant,
}
return
}
t, ok = value.(T)
if !ok {
err = errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureKindMismatch, "variant %s has type %T, expected %T", variant, value, t)
detail = openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewTypeMismatchResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
Variant: feature.DefaultVariant,
}
return
}
detail = openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: variant,
}
return
}
func NewGettableFeatures(features []*Feature, featureVariants map[Name]*FeatureVariant) []*GettableFeature {
gettableFeatures := make([]*GettableFeature, 0)
for _, feature := range features {
if featureVariant, ok := featureVariants[feature.Name]; ok {
gettableFeatures = append(gettableFeatures, &GettableFeature{Feature: feature, FeatureVariant: featureVariant})
continue
}
gettableFeatures = append(gettableFeatures, &GettableFeature{Feature: feature, FeatureVariant: &FeatureVariant{
Variant: feature.DefaultVariant,
Value: feature.Variants[feature.DefaultVariant],
}})
}
return gettableFeatures
}

View File

@@ -0,0 +1,35 @@
package featuretypes
import (
"fmt"
"regexp"
)
var (
nameRegex = regexp.MustCompile(`^[a-z][a-z0-9_]+$`)
)
type Name struct {
s string
}
func NewName(s string) (Name, error) {
if !nameRegex.MatchString(s) {
return Name{}, fmt.Errorf("invalid feature name: %s", s)
}
return Name{s: s}, nil
}
func MustNewName(s string) Name {
name, err := NewName(s)
if err != nil {
panic(err)
}
return name
}
func (n Name) String() string {
return n.s
}

View File

@@ -0,0 +1,153 @@
package featuretypes
import (
"fmt"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/open-feature/go-sdk/openfeature"
)
type Registry interface {
// Merge merges the given registry with the current registry and returns a new registry.
// The input registry takes precedence over the current registry.
MergeOrOverride(Registry) Registry
// Get returns the feature with the given name.
Get(Name) (*Feature, openfeature.ProviderResolutionDetail, error)
// GetByNameString returns the feature with the given name string.
GetByNameString(string) (*Feature, openfeature.ProviderResolutionDetail, error)
// List returns all the features in the registry.
List() []*Feature
}
type registry struct {
features map[Name]*Feature
}
func NewRegistry(features ...*Feature) (Registry, error) {
registry := &registry{features: make(map[Name]*Feature)}
for _, feature := range features {
// Name must be unique
if _, ok := registry.features[feature.Name]; ok {
return nil, fmt.Errorf("cannot build registry, duplicate name %q found", feature.Name.String())
}
// Default variant must be present in the variants
if _, ok := feature.Variants[feature.DefaultVariant]; !ok {
return nil, fmt.Errorf("cannot build registry, default variant %q not found in variants %v", feature.DefaultVariant, feature.Variants)
}
// Check that the type of the default variant and the variants match the kind of the feature
switch feature.Kind {
case KindBoolean:
_, _, err := GetFeatureVariantValue[bool](feature, feature.DefaultVariant)
if err != nil {
return nil, err
}
for variant := range feature.Variants {
_, _, err := GetFeatureVariantValue[bool](feature, variant)
if err != nil {
return nil, err
}
}
case KindString:
_, _, err := GetFeatureVariantValue[string](feature, feature.DefaultVariant)
if err != nil {
return nil, err
}
for variant := range feature.Variants {
_, _, err := GetFeatureVariantValue[string](feature, variant)
if err != nil {
return nil, err
}
}
case KindInt:
_, _, err := GetFeatureVariantValue[int](feature, feature.DefaultVariant)
if err != nil {
return nil, err
}
for variant := range feature.Variants {
_, _, err := GetFeatureVariantValue[int](feature, variant)
if err != nil {
return nil, err
}
}
case KindFloat:
_, _, err := GetFeatureVariantValue[float64](feature, feature.DefaultVariant)
if err != nil {
return nil, err
}
for variant := range feature.Variants {
_, _, err := GetFeatureVariantValue[float64](feature, variant)
if err != nil {
return nil, err
}
}
case KindObject:
_, _, err := GetFeatureVariantValue[map[string]any](feature, feature.DefaultVariant)
if err != nil {
return nil, err
}
for variant := range feature.Variants {
_, _, err := GetFeatureVariantValue[map[string]any](feature, variant)
if err != nil {
return nil, err
}
}
}
registry.features[feature.Name] = feature
}
return registry, nil
}
func (registry *registry) MergeOrOverride(other Registry) Registry {
for _, feature := range other.List() {
registry.features[feature.Name] = feature
}
return registry
}
func (registry *registry) Get(name Name) (*Feature, openfeature.ProviderResolutionDetail, error) {
feature, ok := registry.features[name]
if !ok {
err := errors.Newf(errors.TypeNotFound, ErrCodeFeatureNotFound, "feature %s not found in registry", name.String())
return nil, openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewFlagNotFoundResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
}, err
}
return feature, openfeature.ProviderResolutionDetail{}, nil
}
func (registry *registry) GetByNameString(nameString string) (*Feature, openfeature.ProviderResolutionDetail, error) {
name, err := NewName(nameString)
if err != nil {
return nil, openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewFlagNotFoundResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
}, err
}
return registry.Get(name)
}
func (registry *registry) List() []*Feature {
features := make([]*Feature, 0, len(registry.features))
for _, feature := range registry.features {
features = append(features, feature)
}
return features
}

View File

@@ -0,0 +1,31 @@
package licensetypes
import (
"time"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type License interface {
// ID returns the unique identifier for the license
ID() valuer.UUID
// OrgID returns the organization ID for the license
OrgID() valuer.UUID
// Contents returns the raw data for the license
Contents() []byte
// Key returns the key for the license
Key() string
// CreatedAt returns the creation time for the license
CreatedAt() time.Time
// UpdatedAt returns the last update time for the license
UpdatedAt() time.Time
// FeatureValues returns the feature values for the license
FeatureVariants() map[featuretypes.Name]*featuretypes.FeatureVariant
}