Compare commits

...

5 Commits

Author SHA1 Message Date
Karan Balani
e6f5b3e840 feat: add api for features and improve flagger config 2025-12-26 17:17:29 +05:30
Karan Balani
bef71a8aa9 feat: introduce flagger 2025-12-24 15:30:14 +05:30
Karan Balani
67243a648e chore: temp commit 2025-12-23 19:06:11 +05:30
Karan Balani
9c5a2aba3d chore: rename flagr to flagger 2025-12-18 15:30:24 +05:30
Karan Balani
ca47e471b2 feat: introduce flagr for feature flags 2025-12-18 14:29:36 +05:30
18 changed files with 1115 additions and 23 deletions

View File

@@ -271,3 +271,9 @@ tokenizer:
token:
# The maximum number of tokens a user can have. This limits the number of concurrent sessions a user can have.
max_per_user: 5
##################### Flagger #####################
flagger:
# Config are the overrides for the feature flags which come directly from the config file.
config:
enable_interpolation: true

16
go.mod
View File

@@ -74,12 +74,12 @@ require (
go.opentelemetry.io/otel/trace v1.38.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.41.0
golang.org/x/crypto v0.46.0
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
golang.org/x/net v0.43.0
golang.org/x/net v0.47.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.17.0
golang.org/x/text v0.28.0
golang.org/x/sync v0.19.0
golang.org/x/text v0.32.0
google.golang.org/protobuf v1.36.9
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
@@ -103,6 +103,7 @@ require (
go.opentelemetry.io/collector/config/configretry v1.34.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/tools/godoc v0.1.0-deprecated // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
@@ -223,6 +224,7 @@ require (
github.com/oklog/run v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/open-feature/go-sdk v1.17.0
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.128.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.128.0 // indirect
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.128.0 // indirect
@@ -336,10 +338,10 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/tools v0.39.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/api v0.236.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect

36
go.sum
View File

@@ -762,6 +762,8 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/open-feature/go-sdk v1.17.0 h1:/OUBBw5d9D61JaNZZxb2Nnr5/EJrEpjtKCTY3rspJQk=
github.com/open-feature/go-sdk v1.17.0/go.mod h1:lPxPSu1UnZ4E3dCxZi5gV3et2ACi8O8P+zsTGVsDZUw=
github.com/open-telemetry/opamp-go v0.19.0 h1:8LvQKDwqi+BU3Yy159SU31e2XB0vgnk+PN45pnKilPs=
github.com/open-telemetry/opamp-go v0.19.0/go.mod h1:9/1G6T5dnJz4cJtoYSr6AX18kHdOxnxxETJPZSHyEUg=
github.com/open-telemetry/opentelemetry-collector-contrib/extension/storage v0.128.0 h1:T5IE0l1qcIg6dkHui4hHe+qj3VzuMwpnhrUyubyCwO0=
@@ -1282,8 +1284,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1321,8 +1323,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1371,8 +1373,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1407,8 +1409,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1495,12 +1497,12 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1511,8 +1513,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1575,8 +1577,10 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

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

@@ -0,0 +1,25 @@
package flagger
import "github.com/SigNoz/signoz/pkg/factory"
type Config struct {
// Config are the overrides for the feature flags which come directly from the config file.
Config map[string]any `mapstructure:"config"`
}
func NewConfigFactory() factory.ConfigFactory {
return factory.NewConfigFactory(
factory.MustNewName("flagger"), newConfig,
)
}
// newConfig creates a new config with the default values.
func newConfig() factory.Config {
return &Config{
Config: make(map[string]any),
}
}
func (c Config) Validate() error {
return nil
}

View File

@@ -0,0 +1,313 @@
package configflagger
import (
"context"
"fmt"
"strconv"
"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
// This is the default registry that will be containing all the supported features along with there all possible variants
defaultRegistry featuretypes.Registry
// These are the feature variants that are configured in the config file and will be used as overrides
featureVariants map[featuretypes.Name]featuretypes.FeatureVariant
}
func NewFactory(defaultRegistry featuretypes.Registry) factory.ProviderFactory[flagger.Provider, flagger.Config] {
return factory.NewProviderFactory(factory.MustNewName("config"), func(ctx context.Context, ps factory.ProviderSettings, c flagger.Config) (flagger.Provider, error) {
return New(ctx, ps, c, defaultRegistry)
})
}
func New(ctx context.Context, ps factory.ProviderSettings, c flagger.Config, defaultRegistry featuretypes.Registry) (flagger.Provider, error) {
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/pkg/flagger/configflagger")
featureVariants := make(map[featuretypes.Name]featuretypes.FeatureVariant)
// read all the values from the config and build the featureVariants map
for key, value := range c.Config {
// Check if the feature is valid
feature, _, err := defaultRegistry.GetByString(key)
if err != nil {
return nil, err
}
if feature.Kind == featuretypes.KindObject {
// simply add the value to the featureVariants map
featureVariants[feature.Name] = featuretypes.FeatureVariant{
Variant: featuretypes.MustNewName("from_config"),
Value: value,
}
continue
}
convertedValue, err := convertValueToKind(value, featuretypes.Kind(feature.Kind))
if err != nil {
return nil, err
}
// check if the value is valid
if ok, err := featuretypes.IsValidValue(feature, convertedValue); err != nil || !ok {
return nil, err
}
// get the variant by value
variant, err := featuretypes.VariantByValue(feature, convertedValue)
if err != nil {
return nil, err
}
// add the variant to the featureVariants map
featureVariants[feature.Name] = *variant
}
return &provider{
config: c,
settings: settings,
defaultRegistry: defaultRegistry,
featureVariants: featureVariants,
}, nil
}
func (provider *provider) Metadata() openfeature.Metadata {
return openfeature.Metadata{
Name: "config",
}
}
func (p *provider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
// check if the feature is present in the default registry
feature, detail, err := p.defaultRegistry.GetByString(flag)
if err != nil {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
// get the default value from the feature from default registry
value, detail, err := featuretypes.VariantValue[bool](feature, feature.DefaultVariant)
if err != nil {
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
// check if the feature is present in the featureVariants map
variant, ok := p.featureVariants[feature.Name]
if ok {
// return early as we have found the value in the featureVariants map
return openfeature.BoolResolutionDetail{
Value: variant.Value.(bool),
ProviderResolutionDetail: detail,
}
}
// return the value from the default registry we found earlier
return openfeature.BoolResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
func (p *provider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
// check if the feature is present in the default registry
feature, detail, err := p.defaultRegistry.GetByString(flag)
if err != nil {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
// get the default value from the feature from default registry
value, detail, err := featuretypes.VariantValue[float64](feature, feature.DefaultVariant)
if err != nil {
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
// check if the feature is present in the featureVariants map
variant, ok := p.featureVariants[feature.Name]
if ok {
// return early as we have found the value in the featureVariants map
return openfeature.FloatResolutionDetail{
Value: variant.Value.(float64),
ProviderResolutionDetail: detail,
}
}
// return the value from the default registry we found earlier
return openfeature.FloatResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
func (p *provider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
// check if the feature is present in the default registry
feature, detail, err := p.defaultRegistry.GetByString(flag)
if err != nil {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
// get the default value from the feature from default registry
value, detail, err := featuretypes.VariantValue[string](feature, feature.DefaultVariant)
if err != nil {
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
// check if the feature is present in the featureVariants map
variant, ok := p.featureVariants[feature.Name]
if ok {
// return early as we have found the value in the featureVariants map
return openfeature.StringResolutionDetail{
Value: variant.Value.(string),
ProviderResolutionDetail: detail,
}
}
// return the value from the default registry we found earlier
return openfeature.StringResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
func (p *provider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
// check if the feature is present in the default registry
feature, detail, err := p.defaultRegistry.GetByString(flag)
if err != nil {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
// get the default value from the feature from default registry
value, detail, err := featuretypes.VariantValue[int64](feature, feature.DefaultVariant)
if err != nil {
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
// check if the feature is present in the featureVariants map
variant, ok := p.featureVariants[feature.Name]
if ok {
// return early as we have found the value in the featureVariants map
return openfeature.IntResolutionDetail{
Value: variant.Value.(int64),
ProviderResolutionDetail: detail,
}
}
// return the value from the default registry we found earlier
return openfeature.IntResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
func (p *provider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
// check if the feature is present in the default registry
feature, detail, err := p.defaultRegistry.GetByString(flag)
if err != nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
// get the default value from the feature from default registry
value, detail, err := featuretypes.VariantValue[any](feature, feature.DefaultVariant)
if err != nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: detail,
}
}
// check if the feature is present in the featureVariants map
variant, ok := p.featureVariants[feature.Name]
if ok {
// return early as we have found the value in the featureVariants map
return openfeature.InterfaceResolutionDetail{
Value: variant.Value,
ProviderResolutionDetail: detail,
}
}
// return the value from the default registry we found earlier
return openfeature.InterfaceResolutionDetail{
Value: value,
ProviderResolutionDetail: detail,
}
}
func (provider *provider) Hooks() []openfeature.Hook {
return []openfeature.Hook{}
}
func (p *provider) List(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
return nil, nil
}
func convertValueToKind(value any, kind featuretypes.Kind) (any, error) {
switch kind {
case featuretypes.KindBoolean:
switch v := value.(type) {
case bool:
return v, nil
case string:
return strconv.ParseBool(v)
default:
return nil, fmt.Errorf("cannot convert %T to bool", value)
}
case featuretypes.KindString:
return fmt.Sprintf("%v", value), nil
case featuretypes.KindInt:
switch v := value.(type) {
case int64:
return v, nil
case int:
return int64(v), nil
case float64:
return int64(v), nil
case string:
return strconv.ParseInt(v, 10, 64)
default:
return nil, fmt.Errorf("cannot convert %T to int64", value)
}
case featuretypes.KindFloat:
switch v := value.(type) {
case float64:
return v, nil
case int:
return float64(v), nil
case string:
return strconv.ParseFloat(v, 64)
default:
return nil, fmt.Errorf("cannot convert %T to float64", value)
}
default:
return value, nil
}
}

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

@@ -0,0 +1,284 @@
package flagger
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/open-feature/go-sdk/openfeature"
)
// This is the consumer facing interface for the Flagger service.
type Flagger interface {
Boolean(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (bool, string, error)
String(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (string, string, error)
Float(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (float64, string, error)
Int(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (int64, string, error)
Object(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (any, string, error)
List(ctx context.Context, evalCtx featuretypes.FlaggerEvaluationContext) ([]*featuretypes.GettableFeatureWithResolution, error)
}
// This is the concrete implementation of the Flagger interface.
type flagger struct {
defaultRegistry featuretypes.Registry
settings factory.ScopedProviderSettings
providers map[string]Provider
clients map[string]*openfeature.Client
}
func New(ctx context.Context, ps factory.ProviderSettings, config Config, defaultRegistry featuretypes.Registry, factories ...factory.ProviderFactory[Provider, Config]) (Flagger, error) {
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/pkg/flagger")
providers := make(map[string]Provider)
clients := make(map[string]*openfeature.Client)
for _, factory := range factories {
provider, err := factory.New(ctx, ps, config)
if err != nil {
return nil, err
}
providers[provider.Metadata().Name] = provider
openfeatureClient := openfeature.NewClient(provider.Metadata().Name)
if err := openfeature.SetNamedProviderAndWait(provider.Metadata().Name, provider); err != nil {
return nil, err
}
clients[provider.Metadata().Name] = openfeatureClient
}
return &flagger{
defaultRegistry: defaultRegistry,
settings: settings,
providers: providers,
clients: clients,
}, nil
}
func (f *flagger) Boolean(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (bool, string, error) {
// check if the feature is present in the default registry
feature, _, err := f.defaultRegistry.GetByString(flag)
if err != nil {
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
return false, "", err
}
// get the default value from the feature from default registry
defaultValue, _, err := featuretypes.VariantValue[bool](feature, feature.DefaultVariant)
if err != nil {
// something which should never happen
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
return false, "", err
}
// * this logic can be optimised based on priority of the clients and short circuiting
// now ask all the available clients for the value
for _, client := range f.clients {
value, err := client.BooleanValue(ctx, flag, defaultValue, evalCtx.Ctx())
if err != nil {
f.settings.Logger().ErrorContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name())
continue
}
if value != defaultValue {
return value, client.Metadata().Domain(), nil
}
}
return defaultValue, "defaultRegistry", nil
}
func (f *flagger) String(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (string, string, error) {
// check if the feature is present in the default registry
feature, _, err := f.defaultRegistry.GetByString(flag)
if err != nil {
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
return "", "", err
}
// get the default value from the feature from default registry
defaultValue, _, err := featuretypes.VariantValue[string](feature, feature.DefaultVariant)
if err != nil {
// something which should never happen
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
return "", "", err
}
// * this logic can be optimised based on priority of the clients and short circuiting
// now ask all the available clients for the value
for _, client := range f.clients {
value, err := client.StringValue(ctx, flag, defaultValue, evalCtx.Ctx())
if err != nil {
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name())
continue
}
if value != defaultValue {
return value, client.Metadata().Domain(), nil
}
}
return defaultValue, "defaultRegistry", nil
}
func (f *flagger) Float(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (float64, string, error) {
// check if the feature is present in the default registry
feature, _, err := f.defaultRegistry.GetByString(flag)
if err != nil {
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
return 0, "", err
}
// get the default value from the feature from default registry
defaultValue, _, err := featuretypes.VariantValue[float64](feature, feature.DefaultVariant)
if err != nil {
// something which should never happen
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
return 0, "", err
}
// * this logic can be optimised based on priority of the clients and short circuiting
// now ask all the available clients for the value
for _, client := range f.clients {
value, err := client.FloatValue(ctx, flag, defaultValue, evalCtx.Ctx())
if err != nil {
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name())
continue
}
if value != defaultValue {
return value, client.Metadata().Domain(), nil
}
}
return defaultValue, "defaultRegistry", nil
}
func (f *flagger) Int(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (int64, string, error) {
// check if the feature is present in the default registry
feature, _, err := f.defaultRegistry.GetByString(flag)
if err != nil {
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
return 0, "", err
}
// get the default value from the feature from default registry
defaultValue, _, err := featuretypes.VariantValue[int64](feature, feature.DefaultVariant)
if err != nil {
// something which should never happen
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
return 0, "", err
}
// * this logic can be optimised based on priority of the clients and short circuiting
// now ask all the available clients for the value
for _, client := range f.clients {
value, err := client.IntValue(ctx, flag, defaultValue, evalCtx.Ctx())
if err != nil {
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name())
continue
}
if value != defaultValue {
return value, client.Metadata().Domain(), nil
}
}
return defaultValue, "defaultRegistry", nil
}
func (f *flagger) Object(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (any, string, error) {
// check if the feature is present in the default registry
feature, _, err := f.defaultRegistry.GetByString(flag)
if err != nil {
f.settings.Logger().ErrorContext(ctx, "failed to get feature from default registry", "error", err, "flag", flag)
return nil, "", err
}
// get the default value from the feature from default registry
defaultValue, _, err := featuretypes.VariantValue[any](feature, feature.DefaultVariant)
if err != nil {
// something which should never happen
f.settings.Logger().ErrorContext(ctx, "failed to get default value from feature", "error", err, "flag", flag)
return nil, "", err
}
// * this logic can be optimised based on priority of the clients and short circuiting
// now ask all the available clients for the value
for _, client := range f.clients {
value, err := client.ObjectValue(ctx, flag, defaultValue, evalCtx.Ctx())
if err != nil {
f.settings.Logger().WarnContext(ctx, "failed to get value from client", "error", err, "flag", flag, "client", client.Metadata().Name())
continue
}
if value != defaultValue {
return value, client.Metadata().Domain(), nil
}
}
return defaultValue, "defaultRegistry", nil
}
func (f *flagger) List(ctx context.Context, evalCtx featuretypes.FlaggerEvaluationContext) ([]*featuretypes.GettableFeatureWithResolution, error) {
// get all the feature from the default registry
features := f.defaultRegistry.List()
result := make([]*featuretypes.GettableFeatureWithResolution, 0, len(features))
for _, feature := range features {
variants := make(map[string]any, len(feature.Variants))
for name, variant := range feature.Variants {
variants[name.String()] = variant.Value
}
var resolvedValue any
var source string
var err error
switch feature.Kind {
case featuretypes.KindBoolean:
resolvedValue, source, err = f.Boolean(ctx, feature.Name.String(), evalCtx)
if err != nil {
return nil, err
}
case featuretypes.KindString:
resolvedValue, source, err = f.Boolean(ctx, feature.Name.String(), evalCtx)
if err != nil {
return nil, err
}
case featuretypes.KindFloat:
resolvedValue, source, err = f.Boolean(ctx, feature.Name.String(), evalCtx)
if err != nil {
return nil, err
}
case featuretypes.KindInt:
resolvedValue, source, err = f.Boolean(ctx, feature.Name.String(), evalCtx)
if err != nil {
return nil, err
}
case featuretypes.KindObject:
resolvedValue, source, err = f.Boolean(ctx, feature.Name.String(), evalCtx)
if err != nil {
return nil, err
}
}
result = append(result, &featuretypes.GettableFeatureWithResolution{
Name: feature.Name.String(),
Kind: feature.Kind.StringValue(),
Stage: feature.Stage.StringValue(),
Description: feature.Description,
DefaultVariant: feature.DefaultVariant.String(),
Variants: variants,
ResolvedValue: resolvedValue,
ValueSource: source,
})
}
return result, nil
}

16
pkg/flagger/provider.go Normal file
View File

@@ -0,0 +1,16 @@
package flagger
import (
"context"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/open-feature/go-sdk/openfeature"
)
// Any feature flag provider has to implement this interface.
type Provider interface {
openfeature.FeatureProvider
// List returns all the feature flags
List(ctx context.Context) ([]*featuretypes.GettableFeature, error)
}

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

@@ -0,0 +1,34 @@
package flagger
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
var (
FeatureEnableInterpolation = featuretypes.MustNewName("enable_interpolation")
)
func MustNewRegistry() featuretypes.Registry {
registry, err := featuretypes.NewRegistry(
&featuretypes.Feature{
Name: FeatureEnableInterpolation,
Kind: featuretypes.KindBoolean,
Stage: featuretypes.StageStable,
Description: "Enable interpolation in statement builder",
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: map[featuretypes.Name]featuretypes.FeatureVariant{
featuretypes.MustNewName("disabled"): {
Variant: featuretypes.MustNewName("disabled"),
Value: false,
},
featuretypes.MustNewName("enabled"): {
Variant: featuretypes.MustNewName("enabled"),
Value: true,
},
},
},
)
if err != nil {
panic(err)
}
return registry
}

View File

@@ -63,6 +63,7 @@ import (
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/opamptypes"
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
@@ -565,6 +566,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/version", am.OpenAccess(aH.getVersion)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/features", am.ViewAccess(aH.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/features", am.ViewAccess(aH.getFlaggerFeatures)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/health", am.OpenAccess(aH.getHealth)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/listErrors", am.ViewAccess(aH.listErrors)).Methods(http.MethodPost)
@@ -2022,6 +2024,21 @@ func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
aH.Respond(w, featureSet)
}
func (aH *APIHandler) getFlaggerFeatures(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Create evaluation context (could get orgID from claims if needed)
evalCtx := featuretypes.NewFlaggerEvaluationContext(valuer.GenerateUUID())
features, err := aH.Signoz.Flagger.List(ctx, evalCtx)
if err != nil {
aH.HandleError(w, err, http.StatusInternalServerError)
return
}
aH.Respond(w, features)
}
// getHealth is used to check the health of the service.
// 'live' query param can be used to check liveliness of
// the service by checking the database connection.

View File

@@ -17,6 +17,7 @@ import (
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/gateway"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
@@ -101,6 +102,9 @@ type Config struct {
// MetricsExplorer config
MetricsExplorer metricsexplorer.Config `mapstructure:"metricsexplorer"`
// Flagger config
Flagger flagger.Config `mapstructure:"flagger"`
}
// DeprecatedFlags are the flags that are deprecated and scheduled for removal.
@@ -161,6 +165,7 @@ func NewConfig(ctx context.Context, logger *slog.Logger, resolverConfig config.R
gateway.NewConfigFactory(),
tokenizer.NewConfigFactory(),
metricsexplorer.NewConfigFactory(),
flagger.NewConfigFactory(),
}
conf, err := config.New(ctx, resolverConfig, configFactories)

View File

@@ -18,6 +18,8 @@ import (
"github.com/SigNoz/signoz/pkg/emailing/noopemailing"
"github.com/SigNoz/signoz/pkg/emailing/smtpemailing"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/flagger/configflagger"
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
@@ -51,6 +53,7 @@ import (
"github.com/SigNoz/signoz/pkg/tokenizer/opaquetokenizer"
"github.com/SigNoz/signoz/pkg/tokenizer/tokenizerstore/sqltokenizerstore"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/web"
"github.com/SigNoz/signoz/pkg/web/noopweb"
@@ -242,3 +245,9 @@ func NewTokenizerProviderFactories(cache cache.Cache, sqlstore sqlstore.SQLStore
jwttokenizer.NewFactory(cache, tokenStore),
)
}
func NewFlaggerProviderFactories(defaultRegistry featuretypes.Registry) factory.NamedMap[factory.ProviderFactory[flagger.Provider, flagger.Config]] {
return factory.MustNewNamedMap(
configflagger.NewFactory(defaultRegistry),
)
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
@@ -66,6 +67,7 @@ type SigNoz struct {
Modules Modules
Handlers Handlers
QueryParser queryparser.QueryParser
Flagger flagger.Flagger
}
func New(
@@ -387,6 +389,20 @@ func New(
return nil, err
}
// Initialize flagger from the available flagger provider factories
defaultRegistry := flagger.MustNewRegistry()
flaggerProviderFactories := NewFlaggerProviderFactories(defaultRegistry)
flagger, err := flagger.New(
ctx,
providerSettings,
config.Flagger,
defaultRegistry,
flaggerProviderFactories.GetInOrder()...,
)
if err != nil {
return nil, err
}
registry, err := factory.NewRegistry(
instrumentation.Logger(),
factory.NewNamedService(factory.MustNewName("instrumentation"), instrumentation),
@@ -423,5 +439,6 @@ func New(
Modules: modules,
Handlers: handlers,
QueryParser: queryParser,
Flagger: flagger,
}, nil
}

View File

@@ -0,0 +1,23 @@
package featuretypes
import (
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/open-feature/go-sdk/openfeature"
)
// A concrete wrapper around the openfeature.EvaluationContext
type FlaggerEvaluationContext struct {
ctx openfeature.EvaluationContext
}
// Creates a new FlaggerEvaluationContext with given details
func NewFlaggerEvaluationContext(orgID valuer.UUID) FlaggerEvaluationContext {
ctx := openfeature.NewTargetlessEvaluationContext(map[string]any{
"orgId": orgID.String(),
})
return FlaggerEvaluationContext{ctx: ctx}
}
func (ctx FlaggerEvaluationContext) Ctx() openfeature.EvaluationContext {
return ctx.ctx
}

View File

@@ -0,0 +1,137 @@
package featuretypes
import (
"slices"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/open-feature/go-sdk/openfeature"
)
var (
ErrCodeFeatureVariantNotFound = errors.MustNewCode("feature_variant_not_found")
ErrCodeFeatureValueNotFound = errors.MustNewCode("feature_value_not_found")
ErrCodeFeatureVariantKindMismatch = errors.MustNewCode("feature_variant_kind_mismatch")
ErrCodeFeatureDefaultVariantNotFound = errors.MustNewCode("feature_default_variant_not_found")
ErrCodeFeatureNotFound = errors.MustNewCode("feature_not_found")
)
// A concrete type for a feature flag
type Feature struct {
// Name of the feature
Name Name `json:"name"`
// Kind of the feature
Kind Kind `json:"kind"`
// Stage of the feature
Stage Stage `json:"stage"`
// Description of the feature
Description string `json:"description"`
// DefaultVariant of the feature
DefaultVariant Name `json:"defaultVariant"`
// Variants of the feature
Variants map[Name]FeatureVariant `json:"variants"`
}
// A concrete type for a feature flag variant
type FeatureVariant struct {
// Name of the variant
Variant Name `json:"variant"`
// Value of the variant
Value any `json:"value"`
}
// Consumer facing feature struct
type GettableFeature struct {
*Feature
*FeatureVariant
}
type GettableFeatureWithResolution struct {
Name string `json:"name"`
Kind string `json:"kind"`
Stage string `json:"stage"`
Description string `json:"description"`
DefaultVariant string `json:"defaultVariant"`
Variants map[string]any `json:"variants"`
ResolvedValue any `json:"resolvedValue"`
ValueSource string `json:"valueSource"`
}
// This is the helper function to get the value of a variant of a feature
func VariantValue[T any](feature *Feature, variant Name) (t T, detail openfeature.ProviderResolutionDetail, err error) {
value, ok := feature.Variants[variant]
if !ok {
err = errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureVariantNotFound, "variant %s not found for feature %s in variants %v", variant.String(), feature.Name.String(), feature.Variants)
detail = openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
Variant: feature.DefaultVariant.String(),
}
return
}
t, ok = value.Value.(T)
if !ok {
err = errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureVariantKindMismatch, "variant %s for feature %s has type %T, expected %T", variant.String(), feature.Name.String(), value.Value, t)
detail = openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewTypeMismatchResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
Variant: variant.String(),
}
return
}
detail = openfeature.ProviderResolutionDetail{
Reason: openfeature.StaticReason,
Variant: variant.String(),
}
return
}
// This is the helper function to get the variant by value for the given feature
func VariantByValue[T comparable](feature *Feature, value T) (featureVariant *FeatureVariant, err error) {
// technically this method should not be called for object kind
// but just for fallback
if feature.Kind == KindObject {
// return the default variant - just for fallback
// ? think more on this
return &FeatureVariant{Variant: feature.DefaultVariant, Value: value}, nil
}
for _, variant := range feature.Variants {
if variant.Value == value {
return &variant, nil
}
}
return
}
func IsValidValue[T comparable](feature *Feature, value T) (bool, error) {
if feature.Kind == KindObject {
return true, nil
}
values, err := allFeatureValues[T](feature)
if err != nil {
return false, err
}
if !slices.Contains(values, value) {
return false, errors.Newf(errors.TypeInvalidInput, ErrCodeFeatureValueNotFound, "value %v not found for feature %s in variants %v", value, feature.Name.String(), feature.Variants)
}
return true, nil
}
func allFeatureValues[T any](feature *Feature) (values []T, err error) {
values = make([]T, 0, len(feature.Variants))
for _, variant := range feature.Variants {
v, _, err := VariantValue[T](feature, variant.Variant)
if err != nil {
return nil, err
}
values = append(values, v)
}
return values, nil
}

View File

@@ -0,0 +1,14 @@
package featuretypes
import "github.com/SigNoz/signoz/pkg/valuer"
// A concrete type for a feature flag kind
type Kind struct{ valuer.String }
var (
KindBoolean = Kind{valuer.NewString("boolean")}
KindString = Kind{valuer.NewString("string")}
KindFloat = Kind{valuer.NewString("float")}
KindInt = Kind{valuer.NewString("int")}
KindObject = Kind{valuer.NewString("object")}
)

View File

@@ -0,0 +1,37 @@
package featuretypes
import (
"regexp"
"github.com/SigNoz/signoz/pkg/errors"
)
var nameRegex = regexp.MustCompile(`^[a-z][a-z0-9_]+$`)
// Name is a concrete type for a feature name.
// We make this abstract to avoid direct use of strings and enforce
// a consistent way to create and validate feature names.
type Name struct {
s string
}
func NewName(s string) (Name, error) {
if !nameRegex.MatchString(s) {
return Name{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "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,129 @@
package featuretypes
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/open-feature/go-sdk/openfeature"
)
// Consumer facing interface for the feature registry
type Registry interface {
// Returns the feature and the resolution detail for the given name
Get(name Name) (*Feature, openfeature.ProviderResolutionDetail, error)
// Returns the feature and the resolution detail for the given string name
GetByString(name string) (*Feature, openfeature.ProviderResolutionDetail, error)
// Returns all the features in the registry
List() []*Feature
}
// Concrete implementation of the Registry interface
type registry struct {
features map[Name]*Feature
}
// Validates and builds a new registry from a list of features
func NewRegistry(features ...*Feature) (Registry, error) {
registry := &registry{features: make(map[Name]*Feature)}
for _, feature := range features {
// Check if the name is unique
if _, ok := registry.features[feature.Name]; ok {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "feature name %s already exists", feature.Name.String())
}
// Default variant should always be present
if _, ok := feature.Variants[feature.DefaultVariant]; !ok {
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "default variant %s not found for feature %s in variants %v", feature.DefaultVariant.String(), feature.Name.String(), feature.Variants)
}
switch feature.Kind {
case KindBoolean:
err := validateFeature[bool](feature)
if err != nil {
return nil, err
}
case KindString:
err := validateFeature[string](feature)
if err != nil {
return nil, err
}
case KindFloat:
err := validateFeature[float64](feature)
if err != nil {
return nil, err
}
case KindInt:
err := validateFeature[int64](feature)
if err != nil {
return nil, err
}
case KindObject:
err := validateFeature[any](feature)
if err != nil {
return nil, err
}
}
registry.features[feature.Name] = feature
}
return registry, nil
}
func validateFeature[T any](feature *Feature) error {
_, _, err := VariantValue[T](feature, feature.DefaultVariant)
if err != nil {
return err
}
for variant := range feature.Variants {
_, _, err := VariantValue[T](feature, variant)
if err != nil {
return err
}
}
return nil
}
func (r *registry) Get(name Name) (f *Feature, detail openfeature.ProviderResolutionDetail, err error) {
feature, ok := r.features[name]
if !ok {
err = errors.Newf(errors.TypeNotFound, ErrCodeFeatureNotFound, "feature %s not found", name.String())
detail = openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewGeneralResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
}
return
}
return feature, openfeature.ProviderResolutionDetail{}, nil
}
func (r *registry) GetByString(name string) (f *Feature, detail openfeature.ProviderResolutionDetail, err error) {
featureName, err := NewName(name)
if err != nil {
detail = openfeature.ProviderResolutionDetail{
ResolutionError: openfeature.NewFlagNotFoundResolutionError(err.Error()),
Reason: openfeature.ErrorReason,
}
return
}
return r.Get(featureName)
}
func (r *registry) List() []*Feature {
features := make([]*Feature, 0, len(r.features))
for _, f := range r.features {
features = append(features, f)
}
return features
}

View File

@@ -0,0 +1,20 @@
package featuretypes
import "github.com/SigNoz/signoz/pkg/valuer"
// A concrete type for a feature flag stage
type Stage struct{ valuer.String }
var (
// Used when the feature is experimental
StageExperimental = Stage{valuer.NewString("experimental")}
// Used when the feature works and in preview stage but is not ready for production
StagePreview = Stage{valuer.NewString("preview")}
// Used when the feature is stable and ready for production
StageStable = Stage{valuer.NewString("stable")}
// Used when the feature is deprecated and will be removed in the future
StageDeprecated = Stage{valuer.NewString("deprecated")}
)