mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-29 08:00:59 +00:00
Compare commits
7 Commits
chore/remo
...
feat/intro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
483f6438df | ||
|
|
c4eb785855 | ||
|
|
e6f5b3e840 | ||
|
|
bef71a8aa9 | ||
|
|
67243a648e | ||
|
|
9c5a2aba3d | ||
|
|
ca47e471b2 |
@@ -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
|
||||
|
||||
@@ -1618,6 +1618,51 @@ paths:
|
||||
summary: Update user preference
|
||||
tags:
|
||||
- preferences
|
||||
/api/v2/features:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint returns the supported features and their details
|
||||
operationId: GetFeatures
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/components/schemas/FeaturetypesGettableFeatureWithResolution'
|
||||
type: array
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get features
|
||||
tags:
|
||||
- features
|
||||
/api/v2/orgs/me:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -2065,6 +2110,26 @@ components:
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
FeaturetypesGettableFeatureWithResolution:
|
||||
properties:
|
||||
defaultVariant:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
resolvedValue: {}
|
||||
stage:
|
||||
type: string
|
||||
valueSource:
|
||||
type: string
|
||||
variants:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
PreferencetypesPreference:
|
||||
properties:
|
||||
allowedScopes:
|
||||
|
||||
16
go.mod
16
go.mod
@@ -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
36
go.sum
@@ -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=
|
||||
|
||||
31
pkg/apiserver/signozapiserver/flagger.go
Normal file
31
pkg/apiserver/signozapiserver/flagger.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addFlaggerRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v2/features", handler.New(provider.authZ.ViewAccess(provider.flaggerHandler.GetFeatures), handler.OpenAPIDef{
|
||||
ID: "GetFeatures",
|
||||
Tags: []string{"features"},
|
||||
Summary: "Get features",
|
||||
Description: "This endpoint returns the supported features and their details",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: make([]*featuretypes.GettableFeatureWithResolution, 0),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/apiserver"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
@@ -28,6 +29,7 @@ type provider struct {
|
||||
sessionHandler session.Handler
|
||||
authDomainHandler authdomain.Handler
|
||||
preferenceHandler preference.Handler
|
||||
flaggerHandler flagger.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -38,9 +40,10 @@ func NewFactory(
|
||||
sessionHandler session.Handler,
|
||||
authDomainHandler authdomain.Handler,
|
||||
preferenceHandler preference.Handler,
|
||||
flaggerHandler flagger.Handler,
|
||||
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
|
||||
return newProvider(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler)
|
||||
return newProvider(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler, flaggerHandler)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,6 +58,7 @@ func newProvider(
|
||||
sessionHandler session.Handler,
|
||||
authDomainHandler authdomain.Handler,
|
||||
preferenceHandler preference.Handler,
|
||||
flaggerHandler flagger.Handler,
|
||||
) (apiserver.APIServer, error) {
|
||||
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
|
||||
router := mux.NewRouter().UseEncodedPath()
|
||||
@@ -68,6 +72,7 @@ func newProvider(
|
||||
sessionHandler: sessionHandler,
|
||||
authDomainHandler: authDomainHandler,
|
||||
preferenceHandler: preferenceHandler,
|
||||
flaggerHandler: flaggerHandler,
|
||||
}
|
||||
|
||||
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
|
||||
@@ -104,6 +109,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addFlaggerRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
29
pkg/flagger/config.go
Normal file
29
pkg/flagger/config.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package flagger
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/factory"
|
||||
|
||||
type Config struct {
|
||||
// Features are the features and there overrides which come directly from the config file.
|
||||
Config ConfigFeatures `mapstructure:"config"`
|
||||
}
|
||||
|
||||
type ConfigFeatures struct {
|
||||
EnableInterpolation bool `mapstructure:"enable_interpolation"`
|
||||
}
|
||||
|
||||
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: ConfigFeatures{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
266
pkg/flagger/configflagger/configflagger.go
Normal file
266
pkg/flagger/configflagger/configflagger.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package configflagger
|
||||
|
||||
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
|
||||
// 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.FlaggerProvider, flagger.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("config"), func(ctx context.Context, ps factory.ProviderSettings, c flagger.Config) (flagger.FlaggerProvider, error) {
|
||||
return New(ctx, ps, c, defaultRegistry)
|
||||
})
|
||||
}
|
||||
|
||||
func New(ctx context.Context, ps factory.ProviderSettings, c flagger.Config, defaultRegistry featuretypes.Registry) (flagger.FlaggerProvider, error) {
|
||||
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/pkg/flagger/configflagger")
|
||||
|
||||
featureVariants := make(map[featuretypes.Name]*featuretypes.FeatureVariant)
|
||||
|
||||
// handle each supported feature individually
|
||||
featureValues := c.Config
|
||||
|
||||
// For feature: `enable_interpolation`
|
||||
{
|
||||
f, _, err := defaultRegistry.GetByString("enable_interpolation")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if featureValues.EnableInterpolation != f.Variants[f.DefaultVariant].Value {
|
||||
v, err := defaultRegistry.GetVariantByNameAndValue(f.Name.String(), featureValues.EnableInterpolation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
featureVariants[f.Name] = v
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
result := make([]*featuretypes.GettableFeature, 0, len(p.featureVariants))
|
||||
|
||||
for featureName, variant := range p.featureVariants {
|
||||
feature, _, err := p.defaultRegistry.Get(featureName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, &featuretypes.GettableFeature{
|
||||
Name: feature.Name.String(),
|
||||
Kind: feature.Kind.StringValue(),
|
||||
Stage: feature.Stage.StringValue(),
|
||||
Description: feature.Description,
|
||||
Value: variant.Value,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
285
pkg/flagger/flagger.go
Normal file
285
pkg/flagger/flagger.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package flagger
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/open-feature/go-sdk/openfeature"
|
||||
)
|
||||
|
||||
// Any feature flag provider has to implement this interface.
|
||||
type FlaggerProvider interface {
|
||||
openfeature.FeatureProvider
|
||||
|
||||
// List returns all the feature flags
|
||||
List(ctx context.Context) ([]*featuretypes.GettableFeature, error)
|
||||
}
|
||||
|
||||
// This is the consumer facing interface for the Flagger service.
|
||||
type Flagger interface {
|
||||
Boolean(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (bool, error)
|
||||
String(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (string, error)
|
||||
Float(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (float64, error)
|
||||
Int(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (int64, error)
|
||||
Object(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (any, 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]FlaggerProvider
|
||||
clients map[string]*openfeature.Client
|
||||
}
|
||||
|
||||
func New(ctx context.Context, ps factory.ProviderSettings, config Config, defaultRegistry featuretypes.Registry, factories ...factory.ProviderFactory[FlaggerProvider, Config]) (Flagger, error) {
|
||||
|
||||
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/pkg/flagger")
|
||||
|
||||
providers := make(map[string]FlaggerProvider)
|
||||
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, 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, nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) String(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (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, nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Float(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (float64, 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, nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Int(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (int64, 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, nil
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) Object(ctx context.Context, flag string, evalCtx featuretypes.FlaggerEvaluationContext) (any, 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
|
||||
}
|
||||
|
||||
// ! for object we do not compare with the default value for now, we will figure this out better in future coming releases
|
||||
// if value != defaultValue {
|
||||
// return value, nil
|
||||
// }
|
||||
return value, nil
|
||||
}
|
||||
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (f *flagger) List(ctx context.Context, evalCtx featuretypes.FlaggerEvaluationContext) ([]*featuretypes.GettableFeatureWithResolution, error) {
|
||||
// get all the feature from the default registry
|
||||
allFeatures := f.defaultRegistry.List()
|
||||
|
||||
// make a map of name of feature -> the dict we want to create from all features
|
||||
featureMap := make(map[string]*featuretypes.GettableFeatureWithResolution, len(allFeatures))
|
||||
|
||||
for _, feature := range allFeatures {
|
||||
variants := make(map[string]any, len(feature.Variants))
|
||||
for name, value := range feature.Variants {
|
||||
variants[name.String()] = value.Value
|
||||
}
|
||||
|
||||
featureMap[feature.Name.String()] = &featuretypes.GettableFeatureWithResolution{
|
||||
Name: feature.Name.String(),
|
||||
Kind: feature.Kind.StringValue(),
|
||||
Stage: feature.Stage.StringValue(),
|
||||
Description: feature.Description,
|
||||
DefaultVariant: feature.DefaultVariant.String(),
|
||||
Variants: variants,
|
||||
ResolvedValue: feature.Variants[feature.DefaultVariant].Value,
|
||||
ValueSource: "default",
|
||||
}
|
||||
}
|
||||
|
||||
// now call each provider and fix the value in feature map
|
||||
for _, provider := range f.providers {
|
||||
pFeatures, err := provider.List(ctx)
|
||||
if err != nil {
|
||||
f.settings.Logger().WarnContext(ctx, "failed to get features from provider", "error", err, "provider", provider.Metadata().Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// merge
|
||||
for _, pFeature := range pFeatures {
|
||||
if existing, ok := featureMap[pFeature.Name]; ok {
|
||||
existing.ResolvedValue = pFeature.Value
|
||||
existing.ValueSource = provider.Metadata().Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]*featuretypes.GettableFeatureWithResolution, 0, len(allFeatures))
|
||||
|
||||
for _, f := range featureMap {
|
||||
result = append(result, f)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
44
pkg/flagger/handler.go
Normal file
44
pkg/flagger/handler.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package flagger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
GetFeatures(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
flagger Flagger
|
||||
providerSettings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewHandler(flagger Flagger, providerSettings factory.ProviderSettings) Handler {
|
||||
return &handler{
|
||||
flagger: flagger,
|
||||
providerSettings: providerSettings,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) GetFeatures(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create evaluation context (could get orgID from claims if needed)
|
||||
evalCtx := featuretypes.NewFlaggerEvaluationContext(valuer.GenerateUUID())
|
||||
|
||||
features, err := h.flagger.List(ctx, evalCtx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, features)
|
||||
}
|
||||
38
pkg/flagger/registry.go
Normal file
38
pkg/flagger/registry.go
Normal file
@@ -0,0 +1,38 @@
|
||||
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: getBooleanVariants(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return registry
|
||||
}
|
||||
|
||||
func getBooleanVariants() map[featuretypes.Name]featuretypes.FeatureVariant {
|
||||
return map[featuretypes.Name]featuretypes.FeatureVariant{
|
||||
featuretypes.MustNewName("disabled"): {
|
||||
Variant: featuretypes.MustNewName("disabled"),
|
||||
Value: false,
|
||||
},
|
||||
featuretypes.MustNewName("enabled"): {
|
||||
Variant: featuretypes.MustNewName("enabled"),
|
||||
Value: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,7 @@ package signoz
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
flaggerPkg "github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/modules/apdex"
|
||||
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
|
||||
@@ -34,9 +35,10 @@ type Handlers struct {
|
||||
SpanPercentile spanpercentile.Handler
|
||||
Services services.Handler
|
||||
MetricsExplorer metricsexplorer.Handler
|
||||
FlaggerHandler flaggerPkg.Handler
|
||||
}
|
||||
|
||||
func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, querier querier.Querier, licensing licensing.Licensing) Handlers {
|
||||
func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, querier querier.Querier, licensing licensing.Licensing, flagger flaggerPkg.Flagger) Handlers {
|
||||
return Handlers{
|
||||
SavedView: implsavedview.NewHandler(modules.SavedView),
|
||||
Apdex: implapdex.NewHandler(modules.Apdex),
|
||||
@@ -47,5 +49,6 @@ func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, que
|
||||
Services: implservices.NewHandler(modules.Services),
|
||||
MetricsExplorer: implmetricsexplorer.NewHandler(modules.MetricsExplorer),
|
||||
SpanPercentile: implspanpercentile.NewHandler(modules.SpanPercentile),
|
||||
FlaggerHandler: flaggerPkg.NewHandler(flagger, providerSettings),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestNewHandlers(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{})
|
||||
|
||||
handlers := NewHandlers(modules, providerSettings, nil, nil)
|
||||
handlers := NewHandlers(modules, providerSettings, nil, nil, nil)
|
||||
|
||||
reflectVal := reflect.ValueOf(handlers)
|
||||
for i := 0; i < reflectVal.NumField(); i++ {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/apiserver"
|
||||
"github.com/SigNoz/signoz/pkg/apiserver/signozapiserver"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||
@@ -36,6 +37,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ session.Handler }{},
|
||||
struct{ authdomain.Handler }{},
|
||||
struct{ preference.Handler }{},
|
||||
struct{ flagger.Handler }{},
|
||||
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -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"
|
||||
@@ -221,7 +224,7 @@ func NewQuerierProviderFactories(telemetryStore telemetrystore.TelemetryStore, p
|
||||
)
|
||||
}
|
||||
|
||||
func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.AuthZ, modules Modules, handlers Handlers) factory.NamedMap[factory.ProviderFactory[apiserver.APIServer, apiserver.Config]] {
|
||||
func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.AuthZ, modules Modules, handlers Handlers, flaggerService flagger.Flagger) factory.NamedMap[factory.ProviderFactory[apiserver.APIServer, apiserver.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
signozapiserver.NewFactory(
|
||||
orgGetter,
|
||||
@@ -231,6 +234,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
implsession.NewHandler(modules.Session),
|
||||
implauthdomain.NewHandler(modules.AuthDomain),
|
||||
implpreference.NewHandler(modules.Preference),
|
||||
handlers.FlaggerHandler,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -242,3 +246,9 @@ func NewTokenizerProviderFactories(cache cache.Cache, sqlstore sqlstore.SQLStore
|
||||
jwttokenizer.NewFactory(cache, tokenStore),
|
||||
)
|
||||
}
|
||||
|
||||
func NewFlaggerProviderFactories(defaultRegistry featuretypes.Registry) factory.NamedMap[factory.ProviderFactory[flagger.FlaggerProvider, flagger.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
configflagger.NewFactory(defaultRegistry),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ func TestNewProviderFactories(t *testing.T) {
|
||||
nil,
|
||||
Modules{},
|
||||
Handlers{},
|
||||
nil,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
@@ -345,18 +347,32 @@ func New(
|
||||
telemetrymetadata.AttributesMetadataLocalTableName,
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Initialize all modules
|
||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config)
|
||||
|
||||
// Initialize all handlers for the modules
|
||||
handlers := NewHandlers(modules, providerSettings, querier, licensing)
|
||||
handlers := NewHandlers(modules, providerSettings, querier, licensing, flagger)
|
||||
|
||||
// Initialize the API server
|
||||
apiserver, err := factory.NewProviderFromNamedMap(
|
||||
ctx,
|
||||
providerSettings,
|
||||
config.APIServer,
|
||||
NewAPIServerProviderFactories(orgGetter, authz, modules, handlers),
|
||||
NewAPIServerProviderFactories(orgGetter, authz, modules, handlers, flagger),
|
||||
"signoz",
|
||||
)
|
||||
if err != nil {
|
||||
@@ -423,5 +439,6 @@ func New(
|
||||
Modules: modules,
|
||||
Handlers: handlers,
|
||||
QueryParser: queryParser,
|
||||
Flagger: flagger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
23
pkg/types/featuretypes/context.go
Normal file
23
pkg/types/featuretypes/context.go
Normal 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
|
||||
}
|
||||
148
pkg/types/featuretypes/feature.go
Normal file
148
pkg/types/featuretypes/feature.go
Normal file
@@ -0,0 +1,148 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
type GettableFeature struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Stage string `json:"stage"`
|
||||
Description string `json:"description"`
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
type GettableFeatureWithVariants 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"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
14
pkg/types/featuretypes/kind.go
Normal file
14
pkg/types/featuretypes/kind.go
Normal 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")}
|
||||
)
|
||||
37
pkg/types/featuretypes/name.go
Normal file
37
pkg/types/featuretypes/name.go
Normal 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
|
||||
}
|
||||
147
pkg/types/featuretypes/registry.go
Normal file
147
pkg/types/featuretypes/registry.go
Normal file
@@ -0,0 +1,147 @@
|
||||
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
|
||||
|
||||
// Returns the variant by feature name and value for the given feature
|
||||
GetVariantByNameAndValue(name string, value any) (*FeatureVariant, error)
|
||||
}
|
||||
|
||||
// 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 := ®istry{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
|
||||
}
|
||||
|
||||
func (r *registry) GetVariantByNameAndValue(name string, value any) (*FeatureVariant, error) {
|
||||
f, _, err := r.GetByString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, variant := range f.Variants {
|
||||
if variant.Value == value {
|
||||
return &variant, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.Newf(errors.TypeNotFound, ErrCodeFeatureVariantNotFound, "no variant found with value %v for feature %s in variants %v", value, name, f.Variants)
|
||||
}
|
||||
20
pkg/types/featuretypes/stage.go
Normal file
20
pkg/types/featuretypes/stage.go
Normal 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")}
|
||||
)
|
||||
Reference in New Issue
Block a user