feat: support new flagd.evaluation and flagd.sync schemas (#1083)
Closes #1029 This PR introduces support for the newly introduced evaluation and sync schemas. Supporting both the old and new schemas involves some duplication, but I tried to keep it as minimal as possible. I'm of course open for suggestions for any ideas on how to make this simpler :) See reasoning for new naming [here](https://github.com/open-feature/flagd/issues/948). --------- Signed-off-by: Florian Bacher <florian.bacher@dynatrace.com> Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
This commit is contained in:
parent
3385d58973
commit
e9728aae83
|
|
@ -96,7 +96,7 @@ Experiment with flagd in your browser using [the Killercoda tutorial](https://ki
|
|||
Retrieve a `String` value:
|
||||
|
||||
```sh
|
||||
curl -X POST "http://localhost:8013/schema.v1.Service/ResolveString" \
|
||||
curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveString" \
|
||||
-d '{"flagKey":"myStringFlag","context":{}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ Experiment with flagd in your browser using [the Killercoda tutorial](https://ki
|
|||
|
||||
```sh
|
||||
set json={"flagKey":"myStringFlag","context":{}}
|
||||
curl -i -X POST -H "Content-Type: application/json" -d %json:"=\"% "localhost:8013/schema.v1.Service/ResolveString"
|
||||
curl -i -X POST -H "Content-Type: application/json" -d %json:"=\"% "localhost:8013/flagd.evaluation.v1.Service/ResolveString"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ go 1.20
|
|||
|
||||
require (
|
||||
buf.build/gen/go/open-feature/flagd/connectrpc/go v1.12.0-20231031123731-ac2ec0f39838.1
|
||||
buf.build/gen/go/open-feature/flagd/grpc/go v1.3.0-20230710190440-2333a9579c1a.1
|
||||
buf.build/gen/go/open-feature/flagd/grpc/go v1.3.0-20231031123731-ac2ec0f39838.2
|
||||
buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.31.0-20231031123731-ac2ec0f39838.2
|
||||
connectrpc.com/connect v1.13.0
|
||||
connectrpc.com/otelconnect v0.6.0
|
||||
|
|
|
|||
21
core/go.sum
21
core/go.sum
|
|
@ -1,11 +1,11 @@
|
|||
buf.build/gen/go/grpc-ecosystem/grpc-gateway/grpc/go v1.3.0-20220906183531-bc28b723cd77.1/go.mod h1:9Ec7rvBnjfZvU/TnWjtcSGgiLQ4B+U3B+6SnZgVTA7A=
|
||||
buf.build/gen/go/grpc-ecosystem/grpc-gateway/grpc/go v1.3.0-20220906183531-bc28b723cd77.2/go.mod h1:9Ec7rvBnjfZvU/TnWjtcSGgiLQ4B+U3B+6SnZgVTA7A=
|
||||
buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.28.1-20220906183531-bc28b723cd77.4/go.mod h1:92ejKVTiuvnKoAtRlpJpIxKfloI935DDqhs0NCRx+KM=
|
||||
buf.build/gen/go/grpc-ecosystem/grpc-gateway/protocolbuffers/go v1.31.0-20220906183531-bc28b723cd77.2/go.mod h1:/j/LOrpev/FdyGhdj/sOc0peUf2KR0y4nMmLp4t1g14=
|
||||
buf.build/gen/go/open-feature/flagd/connectrpc/go v1.12.0-20231031123731-ac2ec0f39838.1 h1:wgTgPwRPfD+xXJW6bD+Hcn9KhyPTewy3uOOnpYbeA0c=
|
||||
buf.build/gen/go/open-feature/flagd/connectrpc/go v1.12.0-20231031123731-ac2ec0f39838.1/go.mod h1:l+36EM5Mg5mkmpPNCaIdAt4hvbwYRJKcOe/8ZP/383M=
|
||||
buf.build/gen/go/open-feature/flagd/grpc/go v1.3.0-20230710190440-2333a9579c1a.1 h1:P20N6hN+bx4U9Iccb0dkmvHO+H2lUwdm6QDI57o5U8s=
|
||||
buf.build/gen/go/open-feature/flagd/grpc/go v1.3.0-20230710190440-2333a9579c1a.1/go.mod h1:+lhRQ8QpGLbYqHVf4S9cNpKwytWTyXmcmOoeBPqXm94=
|
||||
buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.28.1-20230710190440-2333a9579c1a.4/go.mod h1:+Bnrjo56uVn/aBcLWchTveR8UeCj+KSJN4fE0xSmBNc=
|
||||
buf.build/gen/go/open-feature/flagd/grpc/go v1.3.0-20231031123731-ac2ec0f39838.2 h1:DCww6WQNaepShZVh/jDVpIfCHQy5QwrpKl8iYAZeaV8=
|
||||
buf.build/gen/go/open-feature/flagd/grpc/go v1.3.0-20231031123731-ac2ec0f39838.2/go.mod h1:NmrKm2OIzFV3sUPs9cWMCmbYeCM3xVEzt4YzFgY5HO4=
|
||||
buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.28.1-20231031123731-ac2ec0f39838.4/go.mod h1:+Bnrjo56uVn/aBcLWchTveR8UeCj+KSJN4fE0xSmBNc=
|
||||
buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.31.0-20231031123731-ac2ec0f39838.2 h1:oYhz5yXOku2FUOFil3hlKp3phfLBinKyUMHkml267kI=
|
||||
buf.build/gen/go/open-feature/flagd/protocolbuffers/go v1.31.0-20231031123731-ac2ec0f39838.2/go.mod h1:QXsT/9pJTFDRE9VnNkVgkfJFAAEVwkTp7/f5JBjyw2Y=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
|
|
@ -438,8 +438,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.3.2 h1:srg/h16pzyuS0/+P2HOt2zdDPDnzaFZtsHtfTugRPVc=
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.3.2/go.mod h1:9oE8z9G+0OMxOoLHF3fhek3KuqD5CBqM0B6XFL08MSg=
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.4.0 h1:TN++nRmEMA5UHzKl8MJ1kbF5SSzWtKHE0PZ6ITbJeH4=
|
||||
github.com/diegoholiveira/jsonlogic/v3 v3.4.0/go.mod h1:9oE8z9G+0OMxOoLHF3fhek3KuqD5CBqM0B6XFL08MSg=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
|
|
@ -868,8 +866,6 @@ golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri
|
|||
golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
|
||||
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
|
||||
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
|
||||
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
|
||||
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -1108,7 +1104,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
|||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
|
|
@ -1224,13 +1219,9 @@ google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZV
|
|||
google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
||||
google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE=
|
||||
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
|
||||
google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA+oRzP9k7cSwJlvDFiROO72uwD6i0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
|
|
@ -1271,8 +1262,6 @@ google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCD
|
|||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/grpc v1.60.0 h1:6FQAR0kM31P6MRdeluor2w2gPaS4SVNrD/DNTxrQ15k=
|
||||
google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
evaluationV1 "buf.build/gen/go/open-feature/flagd/connectrpc/go/flagd/evaluation/v1/evaluationv1connect"
|
||||
schemaConnectV1 "buf.build/gen/go/open-feature/flagd/connectrpc/go/schema/v1/schemav1connect"
|
||||
"github.com/open-feature/flagd/core/pkg/evaluator"
|
||||
"github.com/open-feature/flagd/core/pkg/logger"
|
||||
|
|
@ -31,7 +32,27 @@ import (
|
|||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
const ErrorPrefix = "FlagdError:"
|
||||
const (
|
||||
ErrorPrefix = "FlagdError:"
|
||||
|
||||
flagdSchemaPrefix = "/flagd"
|
||||
)
|
||||
|
||||
// bufSwitchHandler combines the handlers of the old and new evaluation schema and combines them into one
|
||||
// this way we support both the new and the (deprecated) old schemas until only the new schema is supported
|
||||
// NOTE: this will not be required anymore when it is time to work on https://github.com/open-feature/flagd/issues/1088
|
||||
type bufSwitchHandler struct {
|
||||
old http.Handler
|
||||
new http.Handler
|
||||
}
|
||||
|
||||
func (b bufSwitchHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
if strings.HasPrefix(request.URL.Path, flagdSchemaPrefix) {
|
||||
b.new.ServeHTTP(writer, request)
|
||||
} else {
|
||||
b.old.ServeHTTP(writer, request)
|
||||
}
|
||||
}
|
||||
|
||||
type ConnectService struct {
|
||||
logger *logger.Logger
|
||||
|
|
@ -107,10 +128,11 @@ func (s *ConnectService) Notify(n service.Notification) {
|
|||
s.eventingConfiguration.emitToAll(n)
|
||||
}
|
||||
|
||||
// nolint: funlen
|
||||
func (s *ConnectService) setupServer(svcConf service.Configuration) (net.Listener, error) {
|
||||
var lis net.Listener
|
||||
var err error
|
||||
mux := http.NewServeMux()
|
||||
|
||||
if svcConf.SocketPath != "" {
|
||||
lis, err = net.Listen("unix", svcConf.SocketPath)
|
||||
} else {
|
||||
|
|
@ -120,7 +142,10 @@ func (s *ConnectService) setupServer(svcConf service.Configuration) (net.Listene
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating listener for flag evaluation service: %w", err)
|
||||
}
|
||||
fes := NewFlagEvaluationService(
|
||||
|
||||
// register handler for old flag evaluation schema
|
||||
// can be removed as a part of https://github.com/open-feature/flagd/issues/1088
|
||||
fes := NewOldFlagEvaluationService(
|
||||
s.logger.WithFields(zap.String("component", "flagservice")),
|
||||
s.eval,
|
||||
s.eventingConfiguration,
|
||||
|
|
@ -133,12 +158,27 @@ func (s *ConnectService) setupServer(svcConf service.Configuration) (net.Listene
|
|||
protojson.UnmarshalOptions{DiscardUnknown: true},
|
||||
)
|
||||
|
||||
mux.Handle(schemaConnectV1.NewServiceHandler(fes, append(svcConf.Options, marshalOpts)...))
|
||||
_, oldHandler := schemaConnectV1.NewServiceHandler(fes, append(svcConf.Options, marshalOpts)...)
|
||||
|
||||
// register handler for new flag evaluation schema
|
||||
|
||||
newFes := NewFlagEvaluationService(s.logger.WithFields(zap.String("component", "flagd.evaluation.v1")),
|
||||
s.eval,
|
||||
s.eventingConfiguration,
|
||||
s.metrics,
|
||||
)
|
||||
|
||||
_, newHandler := evaluationV1.NewServiceHandler(newFes, svcConf.Options...)
|
||||
|
||||
bs := bufSwitchHandler{
|
||||
old: oldHandler,
|
||||
new: newHandler,
|
||||
}
|
||||
|
||||
s.serverMtx.Lock()
|
||||
s.server = &http.Server{
|
||||
ReadHeaderTimeout: time.Second,
|
||||
Handler: mux,
|
||||
Handler: bs,
|
||||
}
|
||||
s.serverMtx.Unlock()
|
||||
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ func TestAddMiddleware(t *testing.T) {
|
|||
}()
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/schema.v1.Service/ResolveAll", port))
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/flagd.evaluation.v1.Service/ResolveAll", port))
|
||||
// with the default http handler we should get a method not allowed (405) when attempting a GET request
|
||||
return err == nil && resp.StatusCode == http.StatusMethodNotAllowed
|
||||
}, 3*time.Second, 100*time.Millisecond)
|
||||
|
|
@ -162,7 +162,7 @@ func TestAddMiddleware(t *testing.T) {
|
|||
svc.AddMiddleware(mwMock)
|
||||
|
||||
// with the injected middleware, the GET method should work
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/schema.v1.Service/ResolveAll", port))
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/flagd.evaluation.v1.Service/ResolveAll", port))
|
||||
|
||||
require.Nil(t, err)
|
||||
// verify that the status we return in the mocked middleware
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ import (
|
|||
type resolverSignature[T constraints] func(context context.Context, reqID, flagKey string, ctx map[string]any) (
|
||||
T, string, string, map[string]interface{}, error)
|
||||
|
||||
type FlagEvaluationService struct {
|
||||
// OldFlagEvaluationService implements the methods required for the soon-to-be deprecated flag evaluation schema
|
||||
// this can be removed as a part of https://github.com/open-feature/flagd/issues/1088
|
||||
type OldFlagEvaluationService struct {
|
||||
logger *logger.Logger
|
||||
eval evaluator.IEvaluator
|
||||
metrics *telemetry.MetricsRecorder
|
||||
|
|
@ -32,11 +34,11 @@ type FlagEvaluationService struct {
|
|||
flagEvalTracer trace.Tracer
|
||||
}
|
||||
|
||||
// NewFlagEvaluationService creates a FlagEvaluationService with provided parameters
|
||||
func NewFlagEvaluationService(log *logger.Logger,
|
||||
// NewOldFlagEvaluationService creates a OldFlagEvaluationService with provided parameters
|
||||
func NewOldFlagEvaluationService(log *logger.Logger,
|
||||
eval evaluator.IEvaluator, eventingCfg *eventingConfiguration, metricsRecorder *telemetry.MetricsRecorder,
|
||||
) *FlagEvaluationService {
|
||||
return &FlagEvaluationService{
|
||||
) *OldFlagEvaluationService {
|
||||
return &OldFlagEvaluationService{
|
||||
logger: log,
|
||||
eval: eval,
|
||||
metrics: metricsRecorder,
|
||||
|
|
@ -45,7 +47,8 @@ func NewFlagEvaluationService(log *logger.Logger,
|
|||
}
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveAll(
|
||||
// nolint:dupl
|
||||
func (s *OldFlagEvaluationService) ResolveAll(
|
||||
ctx context.Context,
|
||||
req *connect.Request[schemaV1.ResolveAllRequest],
|
||||
) (*connect.Response[schemaV1.ResolveAllResponse], error) {
|
||||
|
|
@ -108,7 +111,7 @@ func (s *FlagEvaluationService) ResolveAll(
|
|||
return connect.NewResponse(res), nil
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) EventStream(
|
||||
func (s *OldFlagEvaluationService) EventStream(
|
||||
ctx context.Context,
|
||||
req *connect.Request[schemaV1.EventStreamRequest],
|
||||
stream *connect.ServerStream[schemaV1.EventStreamResponse],
|
||||
|
|
@ -147,7 +150,7 @@ func (s *FlagEvaluationService) EventStream(
|
|||
}
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveBoolean(
|
||||
func (s *OldFlagEvaluationService) ResolveBoolean(
|
||||
ctx context.Context,
|
||||
req *connect.Request[schemaV1.ResolveBooleanRequest],
|
||||
) (*connect.Response[schemaV1.ResolveBooleanResponse], error) {
|
||||
|
|
@ -160,7 +163,7 @@ func (s *FlagEvaluationService) ResolveBoolean(
|
|||
s.eval.ResolveBooleanValue,
|
||||
req.Msg.GetFlagKey(),
|
||||
req.Msg.GetContext(),
|
||||
&booleanResponse{res},
|
||||
&booleanResponse{schemaV1Resp: res},
|
||||
s.metrics,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -171,7 +174,7 @@ func (s *FlagEvaluationService) ResolveBoolean(
|
|||
return res, err
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveString(
|
||||
func (s *OldFlagEvaluationService) ResolveString(
|
||||
ctx context.Context,
|
||||
req *connect.Request[schemaV1.ResolveStringRequest],
|
||||
) (*connect.Response[schemaV1.ResolveStringResponse], error) {
|
||||
|
|
@ -185,7 +188,7 @@ func (s *FlagEvaluationService) ResolveString(
|
|||
s.eval.ResolveStringValue,
|
||||
req.Msg.GetFlagKey(),
|
||||
req.Msg.GetContext(),
|
||||
&stringResponse{res},
|
||||
&stringResponse{schemaV1Resp: res},
|
||||
s.metrics,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -196,7 +199,7 @@ func (s *FlagEvaluationService) ResolveString(
|
|||
return res, err
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveInt(
|
||||
func (s *OldFlagEvaluationService) ResolveInt(
|
||||
ctx context.Context,
|
||||
req *connect.Request[schemaV1.ResolveIntRequest],
|
||||
) (*connect.Response[schemaV1.ResolveIntResponse], error) {
|
||||
|
|
@ -210,7 +213,7 @@ func (s *FlagEvaluationService) ResolveInt(
|
|||
s.eval.ResolveIntValue,
|
||||
req.Msg.GetFlagKey(),
|
||||
req.Msg.GetContext(),
|
||||
&intResponse{res},
|
||||
&intResponse{schemaV1Resp: res},
|
||||
s.metrics,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -221,7 +224,7 @@ func (s *FlagEvaluationService) ResolveInt(
|
|||
return res, err
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveFloat(
|
||||
func (s *OldFlagEvaluationService) ResolveFloat(
|
||||
ctx context.Context,
|
||||
req *connect.Request[schemaV1.ResolveFloatRequest],
|
||||
) (*connect.Response[schemaV1.ResolveFloatResponse], error) {
|
||||
|
|
@ -235,7 +238,7 @@ func (s *FlagEvaluationService) ResolveFloat(
|
|||
s.eval.ResolveFloatValue,
|
||||
req.Msg.GetFlagKey(),
|
||||
req.Msg.GetContext(),
|
||||
&floatResponse{res},
|
||||
&floatResponse{schemaV1Resp: res},
|
||||
s.metrics,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
@ -246,7 +249,7 @@ func (s *FlagEvaluationService) ResolveFloat(
|
|||
return res, err
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveObject(
|
||||
func (s *OldFlagEvaluationService) ResolveObject(
|
||||
ctx context.Context,
|
||||
req *connect.Request[schemaV1.ResolveObjectRequest],
|
||||
) (*connect.Response[schemaV1.ResolveObjectResponse], error) {
|
||||
|
|
@ -260,7 +263,7 @@ func (s *FlagEvaluationService) ResolveObject(
|
|||
s.eval.ResolveObjectValue,
|
||||
req.Msg.GetFlagKey(),
|
||||
req.Msg.GetContext(),
|
||||
&objectResponse{res},
|
||||
&objectResponse{schemaV1Resp: res},
|
||||
s.metrics,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ func TestConnectService_ResolveAll(t *testing.T) {
|
|||
tt.evalRes,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
@ -228,7 +228,7 @@ func TestFlag_Evaluation_ResolveBoolean(t *testing.T) {
|
|||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
@ -283,7 +283,7 @@ func BenchmarkFlag_Evaluation_ResolveBoolean(b *testing.B) {
|
|||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
@ -381,7 +381,7 @@ func TestFlag_Evaluation_ResolveString(t *testing.T) {
|
|||
tt.wantErr,
|
||||
)
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
@ -436,7 +436,7 @@ func BenchmarkFlag_Evaluation_ResolveString(b *testing.B) {
|
|||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
@ -533,7 +533,7 @@ func TestFlag_Evaluation_ResolveFloat(t *testing.T) {
|
|||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
@ -588,7 +588,7 @@ func BenchmarkFlag_Evaluation_ResolveFloat(b *testing.B) {
|
|||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
@ -685,7 +685,7 @@ func TestFlag_Evaluation_ResolveInt(t *testing.T) {
|
|||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
@ -740,7 +740,7 @@ func BenchmarkFlag_Evaluation_ResolveInt(b *testing.B) {
|
|||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
@ -840,7 +840,7 @@ func TestFlag_Evaluation_ResolveObject(t *testing.T) {
|
|||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
@ -903,7 +903,7 @@ func BenchmarkFlag_Evaluation_ResolveObject(b *testing.B) {
|
|||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
s := NewOldFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package service
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
evalV1 "buf.build/gen/go/open-feature/flagd/protocolbuffers/go/flagd/evaluation/v1"
|
||||
schemaV1 "buf.build/gen/go/open-feature/flagd/protocolbuffers/go/schema/v1"
|
||||
"connectrpc.com/connect"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
|
@ -17,98 +18,145 @@ type constraints interface {
|
|||
}
|
||||
|
||||
type booleanResponse struct {
|
||||
*connect.Response[schemaV1.ResolveBooleanResponse]
|
||||
schemaV1Resp *connect.Response[schemaV1.ResolveBooleanResponse]
|
||||
evalV1Resp *connect.Response[evalV1.ResolveBooleanResponse]
|
||||
}
|
||||
|
||||
func (r *booleanResponse) SetResult(value bool, variant, reason string, metadata map[string]interface{}) error {
|
||||
r.Msg.Value = value
|
||||
r.Msg.Variant = variant
|
||||
r.Msg.Reason = reason
|
||||
|
||||
newStruct, err := structpb.NewStruct(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure to wrap metadata %w", err)
|
||||
}
|
||||
|
||||
r.Msg.Metadata = newStruct
|
||||
if r.schemaV1Resp != nil {
|
||||
r.schemaV1Resp.Msg.Value = value
|
||||
r.schemaV1Resp.Msg.Variant = variant
|
||||
r.schemaV1Resp.Msg.Reason = reason
|
||||
r.schemaV1Resp.Msg.Metadata = newStruct
|
||||
}
|
||||
if r.evalV1Resp != nil {
|
||||
r.evalV1Resp.Msg.Value = value
|
||||
r.evalV1Resp.Msg.Variant = variant
|
||||
r.evalV1Resp.Msg.Reason = reason
|
||||
r.evalV1Resp.Msg.Metadata = newStruct
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type stringResponse struct {
|
||||
*connect.Response[schemaV1.ResolveStringResponse]
|
||||
schemaV1Resp *connect.Response[schemaV1.ResolveStringResponse]
|
||||
evalV1Resp *connect.Response[evalV1.ResolveStringResponse]
|
||||
}
|
||||
|
||||
func (r *stringResponse) SetResult(value string, variant, reason string, metadata map[string]interface{}) error {
|
||||
r.Msg.Value = value
|
||||
r.Msg.Variant = variant
|
||||
r.Msg.Reason = reason
|
||||
|
||||
newStruct, err := structpb.NewStruct(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure to wrap metadata %w", err)
|
||||
}
|
||||
|
||||
r.Msg.Metadata = newStruct
|
||||
if r.schemaV1Resp != nil {
|
||||
r.schemaV1Resp.Msg.Value = value
|
||||
r.schemaV1Resp.Msg.Variant = variant
|
||||
r.schemaV1Resp.Msg.Reason = reason
|
||||
r.schemaV1Resp.Msg.Metadata = newStruct
|
||||
}
|
||||
if r.evalV1Resp != nil {
|
||||
r.evalV1Resp.Msg.Value = value
|
||||
r.evalV1Resp.Msg.Variant = variant
|
||||
r.evalV1Resp.Msg.Reason = reason
|
||||
r.evalV1Resp.Msg.Metadata = newStruct
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type floatResponse struct {
|
||||
*connect.Response[schemaV1.ResolveFloatResponse]
|
||||
schemaV1Resp *connect.Response[schemaV1.ResolveFloatResponse]
|
||||
evalV1Resp *connect.Response[evalV1.ResolveFloatResponse]
|
||||
}
|
||||
|
||||
func (r *floatResponse) SetResult(value float64, variant, reason string, metadata map[string]interface{}) error {
|
||||
r.Msg.Value = value
|
||||
r.Msg.Variant = variant
|
||||
r.Msg.Reason = reason
|
||||
|
||||
newStruct, err := structpb.NewStruct(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure to wrap metadata %w", err)
|
||||
}
|
||||
|
||||
r.Msg.Metadata = newStruct
|
||||
if r.schemaV1Resp != nil {
|
||||
r.schemaV1Resp.Msg.Value = value
|
||||
r.schemaV1Resp.Msg.Variant = variant
|
||||
r.schemaV1Resp.Msg.Reason = reason
|
||||
r.schemaV1Resp.Msg.Metadata = newStruct
|
||||
}
|
||||
if r.evalV1Resp != nil {
|
||||
r.evalV1Resp.Msg.Value = value
|
||||
r.evalV1Resp.Msg.Variant = variant
|
||||
r.evalV1Resp.Msg.Reason = reason
|
||||
r.evalV1Resp.Msg.Metadata = newStruct
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type intResponse struct {
|
||||
*connect.Response[schemaV1.ResolveIntResponse]
|
||||
schemaV1Resp *connect.Response[schemaV1.ResolveIntResponse]
|
||||
evalV1Resp *connect.Response[evalV1.ResolveIntResponse]
|
||||
}
|
||||
|
||||
func (r *intResponse) SetResult(value int64, variant, reason string, metadata map[string]interface{}) error {
|
||||
r.Msg.Value = value
|
||||
r.Msg.Variant = variant
|
||||
r.Msg.Reason = reason
|
||||
|
||||
newStruct, err := structpb.NewStruct(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure to wrap metadata %w", err)
|
||||
}
|
||||
|
||||
r.Msg.Metadata = newStruct
|
||||
if r.schemaV1Resp != nil {
|
||||
r.schemaV1Resp.Msg.Value = value
|
||||
r.schemaV1Resp.Msg.Variant = variant
|
||||
r.schemaV1Resp.Msg.Reason = reason
|
||||
r.schemaV1Resp.Msg.Metadata = newStruct
|
||||
}
|
||||
if r.evalV1Resp != nil {
|
||||
r.evalV1Resp.Msg.Value = value
|
||||
r.evalV1Resp.Msg.Variant = variant
|
||||
r.evalV1Resp.Msg.Reason = reason
|
||||
r.evalV1Resp.Msg.Metadata = newStruct
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type objectResponse struct {
|
||||
*connect.Response[schemaV1.ResolveObjectResponse]
|
||||
schemaV1Resp *connect.Response[schemaV1.ResolveObjectResponse]
|
||||
evalV1Resp *connect.Response[evalV1.ResolveObjectResponse]
|
||||
}
|
||||
|
||||
func (r *objectResponse) SetResult(value map[string]any, variant, reason string,
|
||||
metadata map[string]interface{},
|
||||
) error {
|
||||
r.Msg.Reason = reason
|
||||
newStruct, err := structpb.NewStruct(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure to wrap metadata %w", err)
|
||||
}
|
||||
if r.schemaV1Resp != nil {
|
||||
r.schemaV1Resp.Msg.Reason = reason
|
||||
val, err := structpb.NewStruct(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("struct response construction: %w", err)
|
||||
}
|
||||
|
||||
r.Msg.Value = val
|
||||
r.Msg.Variant = variant
|
||||
|
||||
newStruct, err := structpb.NewStruct(metadata)
|
||||
r.schemaV1Resp.Msg.Value = val
|
||||
r.schemaV1Resp.Msg.Variant = variant
|
||||
r.schemaV1Resp.Msg.Metadata = newStruct
|
||||
}
|
||||
if r.evalV1Resp != nil {
|
||||
r.evalV1Resp.Msg.Reason = reason
|
||||
val, err := structpb.NewStruct(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure to wrap metadata %w", err)
|
||||
return fmt.Errorf("struct response construction: %w", err)
|
||||
}
|
||||
|
||||
r.Msg.Metadata = newStruct
|
||||
r.evalV1Resp.Msg.Value = val
|
||||
r.evalV1Resp.Msg.Variant = variant
|
||||
r.evalV1Resp.Msg.Metadata = newStruct
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
evalV1 "buf.build/gen/go/open-feature/flagd/protocolbuffers/go/flagd/evaluation/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/open-feature/flagd/core/pkg/evaluator"
|
||||
"github.com/open-feature/flagd/core/pkg/logger"
|
||||
"github.com/open-feature/flagd/core/pkg/service"
|
||||
"github.com/open-feature/flagd/core/pkg/telemetry"
|
||||
"github.com/rs/xid"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
type FlagEvaluationService struct {
|
||||
logger *logger.Logger
|
||||
eval evaluator.IEvaluator
|
||||
metrics *telemetry.MetricsRecorder
|
||||
eventingConfiguration *eventingConfiguration
|
||||
flagEvalTracer trace.Tracer
|
||||
}
|
||||
|
||||
// NewFlagEvaluationService creates a FlagEvaluationService with provided parameters
|
||||
func NewFlagEvaluationService(log *logger.Logger,
|
||||
eval evaluator.IEvaluator,
|
||||
eventingCfg *eventingConfiguration,
|
||||
metricsRecorder *telemetry.MetricsRecorder,
|
||||
) *FlagEvaluationService {
|
||||
return &FlagEvaluationService{
|
||||
logger: log,
|
||||
eval: eval,
|
||||
metrics: metricsRecorder,
|
||||
eventingConfiguration: eventingCfg,
|
||||
flagEvalTracer: otel.Tracer("flagd.evaluation.v1"),
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:dupl,funlen
|
||||
func (s *FlagEvaluationService) ResolveAll(
|
||||
ctx context.Context,
|
||||
req *connect.Request[evalV1.ResolveAllRequest],
|
||||
) (*connect.Response[evalV1.ResolveAllResponse], error) {
|
||||
reqID := xid.New().String()
|
||||
defer s.logger.ClearFields(reqID)
|
||||
|
||||
sCtx, span := s.flagEvalTracer.Start(ctx, "resolveAll", trace.WithSpanKind(trace.SpanKindServer))
|
||||
defer span.End()
|
||||
|
||||
res := &evalV1.ResolveAllResponse{
|
||||
Flags: make(map[string]*evalV1.AnyFlag),
|
||||
}
|
||||
|
||||
evalCtx := map[string]any{}
|
||||
if e := req.Msg.GetContext(); e != nil {
|
||||
evalCtx = e.AsMap()
|
||||
}
|
||||
|
||||
values := s.eval.ResolveAllValues(sCtx, reqID, evalCtx)
|
||||
span.SetAttributes(attribute.Int("feature_flag.count", len(values)))
|
||||
for _, value := range values {
|
||||
// register the impression and reason for each flag evaluated
|
||||
s.metrics.RecordEvaluation(sCtx, value.Error, value.Reason, value.Variant, value.FlagKey)
|
||||
switch v := value.Value.(type) {
|
||||
case bool:
|
||||
res.Flags[value.FlagKey] = &evalV1.AnyFlag{
|
||||
Reason: value.Reason,
|
||||
Variant: value.Variant,
|
||||
Value: &evalV1.AnyFlag_BoolValue{
|
||||
BoolValue: v,
|
||||
},
|
||||
}
|
||||
case string:
|
||||
res.Flags[value.FlagKey] = &evalV1.AnyFlag{
|
||||
Reason: value.Reason,
|
||||
Variant: value.Variant,
|
||||
Value: &evalV1.AnyFlag_StringValue{
|
||||
StringValue: v,
|
||||
},
|
||||
}
|
||||
case float64:
|
||||
res.Flags[value.FlagKey] = &evalV1.AnyFlag{
|
||||
Reason: value.Reason,
|
||||
Variant: value.Variant,
|
||||
Value: &evalV1.AnyFlag_DoubleValue{
|
||||
DoubleValue: v,
|
||||
},
|
||||
}
|
||||
case map[string]any:
|
||||
val, err := structpb.NewStruct(v)
|
||||
if err != nil {
|
||||
s.logger.ErrorWithID(reqID, fmt.Sprintf("struct response construction: %v", err))
|
||||
continue
|
||||
}
|
||||
res.Flags[value.FlagKey] = &evalV1.AnyFlag{
|
||||
Reason: value.Reason,
|
||||
Variant: value.Variant,
|
||||
Value: &evalV1.AnyFlag_ObjectValue{
|
||||
ObjectValue: val,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return connect.NewResponse(res), nil
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) EventStream(
|
||||
ctx context.Context,
|
||||
req *connect.Request[evalV1.EventStreamRequest],
|
||||
stream *connect.ServerStream[evalV1.EventStreamResponse],
|
||||
) error {
|
||||
requestNotificationChan := make(chan service.Notification, 1)
|
||||
s.eventingConfiguration.subscribe(req, requestNotificationChan)
|
||||
defer s.eventingConfiguration.unSubscribe(req)
|
||||
|
||||
requestNotificationChan <- service.Notification{
|
||||
Type: service.ProviderReady,
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-time.After(20 * time.Second):
|
||||
err := stream.Send(&evalV1.EventStreamResponse{
|
||||
Type: string(service.KeepAlive),
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error(err.Error())
|
||||
}
|
||||
case notification := <-requestNotificationChan:
|
||||
d, err := structpb.NewStruct(notification.Data)
|
||||
if err != nil {
|
||||
s.logger.Error(err.Error())
|
||||
}
|
||||
err = stream.Send(&evalV1.EventStreamResponse{
|
||||
Type: string(notification.Type),
|
||||
Data: d,
|
||||
})
|
||||
if err != nil {
|
||||
s.logger.Error(err.Error())
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveBoolean(
|
||||
ctx context.Context,
|
||||
req *connect.Request[evalV1.ResolveBooleanRequest],
|
||||
) (*connect.Response[evalV1.ResolveBooleanResponse], error) {
|
||||
sCtx, span := s.flagEvalTracer.Start(ctx, "resolveBoolean", trace.WithSpanKind(trace.SpanKindServer))
|
||||
defer span.End()
|
||||
res := connect.NewResponse(&evalV1.ResolveBooleanResponse{})
|
||||
err := resolve[bool](
|
||||
sCtx,
|
||||
s.logger,
|
||||
s.eval.ResolveBooleanValue,
|
||||
req.Msg.GetFlagKey(),
|
||||
req.Msg.GetContext(),
|
||||
&booleanResponse{evalV1Resp: res},
|
||||
s.metrics,
|
||||
)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, fmt.Sprintf("error evaluating flag with key %s", req.Msg.GetFlagKey()))
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveString(
|
||||
ctx context.Context,
|
||||
req *connect.Request[evalV1.ResolveStringRequest],
|
||||
) (*connect.Response[evalV1.ResolveStringResponse], error) {
|
||||
sCtx, span := s.flagEvalTracer.Start(ctx, "resolveString", trace.WithSpanKind(trace.SpanKindServer))
|
||||
defer span.End()
|
||||
|
||||
res := connect.NewResponse(&evalV1.ResolveStringResponse{})
|
||||
err := resolve[string](
|
||||
sCtx,
|
||||
s.logger,
|
||||
s.eval.ResolveStringValue,
|
||||
req.Msg.GetFlagKey(),
|
||||
req.Msg.GetContext(),
|
||||
&stringResponse{evalV1Resp: res},
|
||||
s.metrics,
|
||||
)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, fmt.Sprintf("error evaluating flag with key %s", req.Msg.GetFlagKey()))
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveInt(
|
||||
ctx context.Context,
|
||||
req *connect.Request[evalV1.ResolveIntRequest],
|
||||
) (*connect.Response[evalV1.ResolveIntResponse], error) {
|
||||
sCtx, span := s.flagEvalTracer.Start(ctx, "resolveInt", trace.WithSpanKind(trace.SpanKindServer))
|
||||
defer span.End()
|
||||
|
||||
res := connect.NewResponse(&evalV1.ResolveIntResponse{})
|
||||
err := resolve[int64](
|
||||
sCtx,
|
||||
s.logger,
|
||||
s.eval.ResolveIntValue,
|
||||
req.Msg.GetFlagKey(),
|
||||
req.Msg.GetContext(),
|
||||
&intResponse{evalV1Resp: res},
|
||||
s.metrics,
|
||||
)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, fmt.Sprintf("error evaluating flag with key %s", req.Msg.GetFlagKey()))
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveFloat(
|
||||
ctx context.Context,
|
||||
req *connect.Request[evalV1.ResolveFloatRequest],
|
||||
) (*connect.Response[evalV1.ResolveFloatResponse], error) {
|
||||
sCtx, span := s.flagEvalTracer.Start(ctx, "resolveFloat", trace.WithSpanKind(trace.SpanKindServer))
|
||||
defer span.End()
|
||||
|
||||
res := connect.NewResponse(&evalV1.ResolveFloatResponse{})
|
||||
err := resolve[float64](
|
||||
sCtx,
|
||||
s.logger,
|
||||
s.eval.ResolveFloatValue,
|
||||
req.Msg.GetFlagKey(),
|
||||
req.Msg.GetContext(),
|
||||
&floatResponse{evalV1Resp: res},
|
||||
s.metrics,
|
||||
)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, fmt.Sprintf("error evaluating flag with key %s", req.Msg.GetFlagKey()))
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *FlagEvaluationService) ResolveObject(
|
||||
ctx context.Context,
|
||||
req *connect.Request[evalV1.ResolveObjectRequest],
|
||||
) (*connect.Response[evalV1.ResolveObjectResponse], error) {
|
||||
sCtx, span := s.flagEvalTracer.Start(ctx, "resolveObject", trace.WithSpanKind(trace.SpanKindServer))
|
||||
defer span.End()
|
||||
|
||||
res := connect.NewResponse(&evalV1.ResolveObjectResponse{})
|
||||
err := resolve[map[string]any](
|
||||
sCtx,
|
||||
s.logger,
|
||||
s.eval.ResolveObjectValue,
|
||||
req.Msg.GetFlagKey(),
|
||||
req.Msg.GetContext(),
|
||||
&objectResponse{evalV1Resp: res},
|
||||
s.metrics,
|
||||
)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, fmt.Sprintf("error evaluating flag with key %s", req.Msg.GetFlagKey()))
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
|
@ -0,0 +1,946 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
evalV1 "buf.build/gen/go/open-feature/flagd/protocolbuffers/go/flagd/evaluation/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/open-feature/flagd/core/pkg/evaluator"
|
||||
mock "github.com/open-feature/flagd/core/pkg/evaluator/mock"
|
||||
"github.com/open-feature/flagd/core/pkg/logger"
|
||||
"github.com/open-feature/flagd/core/pkg/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
func TestConnectServiceV2_ResolveAll(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
req *evalV1.ResolveAllRequest
|
||||
evalRes []evaluator.AnyValue
|
||||
wantErr error
|
||||
wantRes *evalV1.ResolveAllResponse
|
||||
}{
|
||||
"happy-path": {
|
||||
req: &evalV1.ResolveAllRequest{},
|
||||
evalRes: []evaluator.AnyValue{
|
||||
{
|
||||
Value: true,
|
||||
Variant: "bool-true",
|
||||
Reason: "true",
|
||||
FlagKey: "bool",
|
||||
},
|
||||
{
|
||||
Value: float64(12.12),
|
||||
Variant: "float",
|
||||
Reason: "float",
|
||||
FlagKey: "float",
|
||||
},
|
||||
{
|
||||
Value: "hello",
|
||||
Variant: "string",
|
||||
Reason: "string",
|
||||
FlagKey: "string",
|
||||
},
|
||||
{
|
||||
Value: "hello",
|
||||
Variant: "object",
|
||||
Reason: "string",
|
||||
FlagKey: "object",
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
wantRes: &evalV1.ResolveAllResponse{
|
||||
Flags: map[string]*evalV1.AnyFlag{
|
||||
"bool": {
|
||||
Value: &evalV1.AnyFlag_BoolValue{
|
||||
BoolValue: true,
|
||||
},
|
||||
Reason: "STATIC",
|
||||
},
|
||||
"float": {
|
||||
Value: &evalV1.AnyFlag_DoubleValue{
|
||||
DoubleValue: float64(12.12),
|
||||
},
|
||||
Reason: "STATIC",
|
||||
},
|
||||
"string": {
|
||||
Value: &evalV1.AnyFlag_StringValue{
|
||||
StringValue: "hello",
|
||||
},
|
||||
Reason: "STATIC",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ctrl := gomock.NewController(t)
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveAllValues(gomock.Any(), gomock.Any(), gomock.Any()).Return(
|
||||
tt.evalRes,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
got, err := s.ResolveAll(context.Background(), connect.NewRequest(tt.req))
|
||||
if err != nil && !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("ConnectService.ResolveAll() error = %v, wantErr %v", err.Error(), tt.wantErr.Error())
|
||||
return
|
||||
}
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(t, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(t, len(data.ScopeMetrics), 1)
|
||||
for _, flag := range tt.evalRes {
|
||||
switch v := flag.Value.(type) {
|
||||
case bool:
|
||||
val := got.Msg.Flags[flag.FlagKey].Value.(*evalV1.AnyFlag_BoolValue)
|
||||
require.Equal(t, v, val.BoolValue)
|
||||
case string:
|
||||
val := got.Msg.Flags[flag.FlagKey].Value.(*evalV1.AnyFlag_StringValue)
|
||||
require.Equal(t, v, val.StringValue)
|
||||
case float64:
|
||||
val := got.Msg.Flags[flag.FlagKey].Value.(*evalV1.AnyFlag_DoubleValue)
|
||||
require.Equal(t, v, val.DoubleValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type resolveBooleanArgsV2 struct {
|
||||
evalFields resolveBooleanEvalFieldsV2
|
||||
functionArgs resolveBooleanFunctionArgsV2
|
||||
want *evalV1.ResolveBooleanResponse
|
||||
wantErr error
|
||||
mCount int
|
||||
}
|
||||
type resolveBooleanFunctionArgsV2 struct {
|
||||
ctx context.Context
|
||||
req *evalV1.ResolveBooleanRequest
|
||||
}
|
||||
type resolveBooleanEvalFieldsV2 struct {
|
||||
result bool
|
||||
evalCommons
|
||||
}
|
||||
|
||||
func TestFlag_EvaluationV2_ResolveBoolean(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
tests := map[string]resolveBooleanArgsV2{
|
||||
"happy path": {
|
||||
mCount: 1,
|
||||
evalFields: resolveBooleanEvalFieldsV2{
|
||||
result: true,
|
||||
evalCommons: happyCommon,
|
||||
},
|
||||
functionArgs: resolveBooleanFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveBooleanRequest{
|
||||
FlagKey: "bool",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveBooleanResponse{
|
||||
Value: true,
|
||||
Reason: model.DefaultReason,
|
||||
Variant: "on",
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
"eval returns error": {
|
||||
mCount: 1,
|
||||
evalFields: resolveBooleanEvalFieldsV2{
|
||||
result: true,
|
||||
evalCommons: sadCommon,
|
||||
},
|
||||
functionArgs: resolveBooleanFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveBooleanRequest{
|
||||
FlagKey: "bool",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveBooleanResponse{
|
||||
Value: true,
|
||||
Variant: ":(",
|
||||
Reason: model.ErrorReason,
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: errors.New("eval interface error"),
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveBooleanValue(gomock.Any(), gomock.Any(), tt.functionArgs.req.FlagKey, gomock.Any()).Return(
|
||||
tt.evalFields.result,
|
||||
tt.evalFields.variant,
|
||||
tt.evalFields.reason,
|
||||
tt.evalFields.metadata,
|
||||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
got, err := s.ResolveBoolean(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
|
||||
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("Flag_Evaluation.ResolveBoolean() error = %v, wantErr %v", err.Error(), tt.wantErr.Error())
|
||||
return
|
||||
}
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(t, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(t, len(data.ScopeMetrics), tt.mCount)
|
||||
require.Equal(t, tt.want, got.Msg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFlag_EvaluationV2_ResolveBoolean(b *testing.B) {
|
||||
ctrl := gomock.NewController(b)
|
||||
tests := map[string]resolveBooleanArgsV2{
|
||||
"happy path": {
|
||||
evalFields: resolveBooleanEvalFieldsV2{
|
||||
result: true,
|
||||
evalCommons: happyCommon,
|
||||
},
|
||||
functionArgs: resolveBooleanFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveBooleanRequest{
|
||||
FlagKey: "bool",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveBooleanResponse{
|
||||
Value: true,
|
||||
Reason: model.DefaultReason,
|
||||
Variant: "on",
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveBooleanValue(gomock.Any(), gomock.Any(), tt.functionArgs.req.FlagKey, gomock.Any()).Return(
|
||||
tt.evalFields.result,
|
||||
tt.evalFields.variant,
|
||||
tt.evalFields.reason,
|
||||
tt.evalFields.metadata,
|
||||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got, err := s.ResolveBoolean(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
|
||||
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||
b.Errorf("Flag_Evaluation.ResolveBoolean() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.Equal(b, tt.want, got.Msg)
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(b, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(b, len(data.ScopeMetrics), 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type resolveStringArgsV2 struct {
|
||||
evalFields resolveStringEvalFieldsV2
|
||||
functionArgs resolveStringFunctionArgsV2
|
||||
want *evalV1.ResolveStringResponse
|
||||
wantErr error
|
||||
mCount int
|
||||
}
|
||||
type resolveStringFunctionArgsV2 struct {
|
||||
ctx context.Context
|
||||
req *evalV1.ResolveStringRequest
|
||||
}
|
||||
type resolveStringEvalFieldsV2 struct {
|
||||
result string
|
||||
evalCommons
|
||||
}
|
||||
|
||||
func TestFlag_EvaluationV2_ResolveString(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
tests := map[string]resolveStringArgsV2{
|
||||
"happy path": {
|
||||
mCount: 1,
|
||||
evalFields: resolveStringEvalFieldsV2{
|
||||
result: "true",
|
||||
evalCommons: happyCommon,
|
||||
},
|
||||
functionArgs: resolveStringFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveStringRequest{
|
||||
FlagKey: "string",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveStringResponse{
|
||||
Value: "true",
|
||||
Reason: model.DefaultReason,
|
||||
Variant: "on",
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
"eval returns error": {
|
||||
mCount: 1,
|
||||
evalFields: resolveStringEvalFieldsV2{
|
||||
result: "true",
|
||||
evalCommons: sadCommon,
|
||||
},
|
||||
functionArgs: resolveStringFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveStringRequest{
|
||||
FlagKey: "string",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveStringResponse{
|
||||
Value: "true",
|
||||
Variant: ":(",
|
||||
Reason: model.ErrorReason,
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: errors.New("eval interface error"),
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveStringValue(
|
||||
gomock.Any(), gomock.Any(), tt.functionArgs.req.FlagKey, gomock.Any()).Return(
|
||||
tt.evalFields.result,
|
||||
tt.evalFields.variant,
|
||||
tt.evalFields.reason,
|
||||
tt.evalFields.metadata,
|
||||
tt.wantErr,
|
||||
)
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
got, err := s.ResolveString(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
|
||||
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("Flag_Evaluation.ResolveString() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(t, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(t, len(data.ScopeMetrics), tt.mCount)
|
||||
require.Equal(t, tt.want, got.Msg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFlag_EvaluationV2_ResolveString(b *testing.B) {
|
||||
ctrl := gomock.NewController(b)
|
||||
tests := map[string]resolveStringArgsV2{
|
||||
"happy path": {
|
||||
evalFields: resolveStringEvalFieldsV2{
|
||||
result: "true",
|
||||
evalCommons: happyCommon,
|
||||
},
|
||||
functionArgs: resolveStringFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveStringRequest{
|
||||
FlagKey: "string",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveStringResponse{
|
||||
Value: "true",
|
||||
Reason: model.DefaultReason,
|
||||
Variant: "on",
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveStringValue(gomock.Any(), gomock.Any(), tt.functionArgs.req.FlagKey, gomock.Any()).Return(
|
||||
tt.evalFields.result,
|
||||
tt.evalFields.variant,
|
||||
tt.evalFields.reason,
|
||||
tt.evalFields.metadata,
|
||||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got, err := s.ResolveString(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
|
||||
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||
b.Errorf("Flag_Evaluation.ResolveString() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.Equal(b, tt.want, got.Msg)
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(b, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(b, len(data.ScopeMetrics), 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type resolveFloatArgsV2 struct {
|
||||
evalFields resolveFloatEvalFieldsV2
|
||||
functionArgs resolveFloatFunctionArgsV2
|
||||
want *evalV1.ResolveFloatResponse
|
||||
wantErr error
|
||||
mCount int
|
||||
}
|
||||
type resolveFloatFunctionArgsV2 struct {
|
||||
ctx context.Context
|
||||
req *evalV1.ResolveFloatRequest
|
||||
}
|
||||
type resolveFloatEvalFieldsV2 struct {
|
||||
result float64
|
||||
evalCommons
|
||||
}
|
||||
|
||||
func TestFlag_EvaluationV2_ResolveFloat(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
tests := map[string]resolveFloatArgsV2{
|
||||
"happy path": {
|
||||
mCount: 1,
|
||||
evalFields: resolveFloatEvalFieldsV2{
|
||||
result: 12,
|
||||
evalCommons: happyCommon,
|
||||
},
|
||||
functionArgs: resolveFloatFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveFloatRequest{
|
||||
FlagKey: "float",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveFloatResponse{
|
||||
Value: 12,
|
||||
Reason: model.DefaultReason,
|
||||
Variant: "on",
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
"eval returns error": {
|
||||
mCount: 1,
|
||||
evalFields: resolveFloatEvalFieldsV2{
|
||||
result: 12,
|
||||
evalCommons: sadCommon,
|
||||
},
|
||||
functionArgs: resolveFloatFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveFloatRequest{
|
||||
FlagKey: "float",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveFloatResponse{
|
||||
Value: 12,
|
||||
Variant: ":(",
|
||||
Reason: model.ErrorReason,
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: errors.New("eval interface error"),
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveFloatValue(gomock.Any(), gomock.Any(), tt.functionArgs.req.FlagKey, gomock.Any()).Return(
|
||||
tt.evalFields.result,
|
||||
tt.evalFields.variant,
|
||||
tt.evalFields.reason,
|
||||
tt.evalFields.metadata,
|
||||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
got, err := s.ResolveFloat(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
|
||||
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("Flag_Evaluation.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.Equal(t, tt.want, got.Msg)
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(t, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(t, len(data.ScopeMetrics), tt.mCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFlag_EvaluationV2_ResolveFloat(b *testing.B) {
|
||||
ctrl := gomock.NewController(b)
|
||||
tests := map[string]resolveFloatArgsV2{
|
||||
"happy path": {
|
||||
evalFields: resolveFloatEvalFieldsV2{
|
||||
result: 12,
|
||||
evalCommons: happyCommon,
|
||||
},
|
||||
functionArgs: resolveFloatFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveFloatRequest{
|
||||
FlagKey: "float",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveFloatResponse{
|
||||
Value: 12,
|
||||
Reason: model.DefaultReason,
|
||||
Variant: "on",
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveFloatValue(gomock.Any(), gomock.Any(), tt.functionArgs.req.FlagKey, gomock.Any()).Return(
|
||||
tt.evalFields.result,
|
||||
tt.evalFields.variant,
|
||||
tt.evalFields.reason,
|
||||
tt.evalFields.metadata,
|
||||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got, err := s.ResolveFloat(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
|
||||
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||
b.Errorf("Flag_Evaluation.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.Equal(b, tt.want, got.Msg)
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(b, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(b, len(data.ScopeMetrics), 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type resolveIntArgsV2 struct {
|
||||
evalFields resolveIntEvalFieldsV2
|
||||
functionArgs resolveIntFunctionArgsV2
|
||||
want *evalV1.ResolveIntResponse
|
||||
wantErr error
|
||||
mCount int
|
||||
}
|
||||
type resolveIntFunctionArgsV2 struct {
|
||||
ctx context.Context
|
||||
req *evalV1.ResolveIntRequest
|
||||
}
|
||||
type resolveIntEvalFieldsV2 struct {
|
||||
result int64
|
||||
evalCommons
|
||||
}
|
||||
|
||||
func TestFlag_EvaluationV2_ResolveInt(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
tests := map[string]resolveIntArgsV2{
|
||||
"happy path": {
|
||||
mCount: 1,
|
||||
evalFields: resolveIntEvalFieldsV2{
|
||||
result: 12,
|
||||
evalCommons: happyCommon,
|
||||
},
|
||||
functionArgs: resolveIntFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveIntRequest{
|
||||
FlagKey: "int",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveIntResponse{
|
||||
Value: 12,
|
||||
Reason: model.DefaultReason,
|
||||
Variant: "on",
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
"eval returns error": {
|
||||
mCount: 1,
|
||||
evalFields: resolveIntEvalFieldsV2{
|
||||
result: 12,
|
||||
evalCommons: sadCommon,
|
||||
},
|
||||
functionArgs: resolveIntFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveIntRequest{
|
||||
FlagKey: "int",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveIntResponse{
|
||||
Value: 12,
|
||||
Variant: ":(",
|
||||
Reason: model.ErrorReason,
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: errors.New("eval interface error"),
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveIntValue(gomock.Any(), gomock.Any(), tt.functionArgs.req.FlagKey, gomock.Any()).Return(
|
||||
tt.evalFields.result,
|
||||
tt.evalFields.variant,
|
||||
tt.evalFields.reason,
|
||||
tt.evalFields.metadata,
|
||||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
got, err := s.ResolveInt(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
|
||||
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("Flag_Evaluation.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.Equal(t, tt.want, got.Msg)
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(t, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(t, len(data.ScopeMetrics), tt.mCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFlag_EvaluationV2_ResolveInt(b *testing.B) {
|
||||
ctrl := gomock.NewController(b)
|
||||
tests := map[string]resolveIntArgsV2{
|
||||
"happy path": {
|
||||
evalFields: resolveIntEvalFieldsV2{
|
||||
result: 12,
|
||||
evalCommons: happyCommon,
|
||||
},
|
||||
functionArgs: resolveIntFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveIntRequest{
|
||||
FlagKey: "int",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveIntResponse{
|
||||
Value: 12,
|
||||
Reason: model.DefaultReason,
|
||||
Variant: "on",
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveIntValue(gomock.Any(), gomock.Any(), tt.functionArgs.req.FlagKey, gomock.Any()).Return(
|
||||
tt.evalFields.result,
|
||||
tt.evalFields.variant,
|
||||
tt.evalFields.reason,
|
||||
tt.evalFields.metadata,
|
||||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
b.Run(name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got, err := s.ResolveInt(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
|
||||
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||
b.Errorf("Flag_Evaluation.ResolveNumber() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.Equal(b, tt.want, got.Msg)
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(b, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(b, len(data.ScopeMetrics), 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type resolveObjectArgsV2 struct {
|
||||
evalFields resolveObjectEvalFieldsV2
|
||||
functionArgs resolveObjectFunctionArgsV2
|
||||
want *evalV1.ResolveObjectResponse
|
||||
wantErr error
|
||||
mCount int
|
||||
}
|
||||
type resolveObjectFunctionArgsV2 struct {
|
||||
ctx context.Context
|
||||
req *evalV1.ResolveObjectRequest
|
||||
}
|
||||
type resolveObjectEvalFieldsV2 struct {
|
||||
result map[string]interface{}
|
||||
evalCommons
|
||||
}
|
||||
|
||||
func TestFlag_EvaluationV2_ResolveObject(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
tests := map[string]resolveObjectArgsV2{
|
||||
"happy path": {
|
||||
mCount: 1,
|
||||
evalFields: resolveObjectEvalFieldsV2{
|
||||
result: map[string]interface{}{
|
||||
"food": "bars",
|
||||
},
|
||||
evalCommons: happyCommon,
|
||||
},
|
||||
functionArgs: resolveObjectFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveObjectRequest{
|
||||
FlagKey: "object",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveObjectResponse{
|
||||
Value: nil,
|
||||
Reason: model.DefaultReason,
|
||||
Variant: "on",
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
"eval returns error": {
|
||||
mCount: 1,
|
||||
evalFields: resolveObjectEvalFieldsV2{
|
||||
result: map[string]interface{}{
|
||||
"food": "bars",
|
||||
},
|
||||
evalCommons: sadCommon,
|
||||
},
|
||||
functionArgs: resolveObjectFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveObjectRequest{
|
||||
FlagKey: "object",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveObjectResponse{
|
||||
Variant: ":(",
|
||||
Reason: model.ErrorReason,
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: errors.New("eval interface error"),
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveObjectValue(gomock.Any(), gomock.Any(), tt.functionArgs.req.FlagKey, gomock.Any()).Return(
|
||||
tt.evalFields.result,
|
||||
tt.evalFields.variant,
|
||||
tt.evalFields.reason,
|
||||
tt.evalFields.metadata,
|
||||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
|
||||
outParsed, err := structpb.NewStruct(tt.evalFields.result)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tt.want.Value = outParsed
|
||||
got, err := s.ResolveObject(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
|
||||
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("Flag_Evaluation.ResolveObject() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.Equal(t, tt.want, got.Msg)
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(t, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(t, len(data.ScopeMetrics), tt.mCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFlag_EvaluationV2_ResolveObject(b *testing.B) {
|
||||
ctrl := gomock.NewController(b)
|
||||
tests := map[string]resolveObjectArgsV2{
|
||||
"happy path": {
|
||||
evalFields: resolveObjectEvalFieldsV2{
|
||||
result: map[string]interface{}{
|
||||
"food": "bars",
|
||||
},
|
||||
evalCommons: happyCommon,
|
||||
},
|
||||
functionArgs: resolveObjectFunctionArgsV2{
|
||||
context.Background(),
|
||||
&evalV1.ResolveObjectRequest{
|
||||
FlagKey: "object",
|
||||
Context: &structpb.Struct{},
|
||||
},
|
||||
},
|
||||
want: &evalV1.ResolveObjectResponse{
|
||||
Value: nil,
|
||||
Reason: model.DefaultReason,
|
||||
Variant: "on",
|
||||
Metadata: responseStruct,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
eval := mock.NewMockIEvaluator(ctrl)
|
||||
eval.EXPECT().ResolveObjectValue(gomock.Any(), gomock.Any(), tt.functionArgs.req.FlagKey, gomock.Any()).Return(
|
||||
tt.evalFields.result,
|
||||
tt.evalFields.variant,
|
||||
tt.evalFields.reason,
|
||||
tt.evalFields.metadata,
|
||||
tt.wantErr,
|
||||
).AnyTimes()
|
||||
metrics, exp := getMetricReader()
|
||||
s := NewFlagEvaluationService(
|
||||
logger.NewLogger(nil, false),
|
||||
eval,
|
||||
&eventingConfiguration{},
|
||||
metrics,
|
||||
)
|
||||
if name != "eval returns error" {
|
||||
outParsed, err := structpb.NewStruct(tt.evalFields.result)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
tt.want.Value = outParsed
|
||||
}
|
||||
b.Run(name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got, err := s.ResolveObject(tt.functionArgs.ctx, connect.NewRequest(tt.functionArgs.req))
|
||||
if (err != nil) && !errors.Is(err, tt.wantErr) {
|
||||
b.Errorf("Flag_Evaluation.ResolveObject() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.Equal(b, tt.want, got.Msg)
|
||||
var data metricdata.ResourceMetrics
|
||||
err = exp.Collect(context.TODO(), &data)
|
||||
require.Nil(b, err)
|
||||
// the impression metric is registered
|
||||
require.Equal(b, len(data.ScopeMetrics), 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFlag_EvaluationV2_ErrorCodes test validate error mapping from known errors to connect.Code and avoid accidental
|
||||
// changes. This is essential as SDK implementations rely on connect. Code to differentiate GRPC errors vs Flag errors.
|
||||
// For any change in error codes, we must change respective SDK.
|
||||
func TestFlag_EvaluationV2_ErrorCodes(t *testing.T) {
|
||||
tests := []struct {
|
||||
err error
|
||||
code connect.Code
|
||||
}{
|
||||
{
|
||||
err: errors.New(model.FlagNotFoundErrorCode),
|
||||
code: connect.CodeNotFound,
|
||||
},
|
||||
{
|
||||
err: errors.New(model.TypeMismatchErrorCode),
|
||||
code: connect.CodeInvalidArgument,
|
||||
},
|
||||
{
|
||||
err: errors.New(model.ParseErrorCode),
|
||||
code: connect.CodeDataLoss,
|
||||
},
|
||||
{
|
||||
err: errors.New(model.FlagDisabledErrorCode),
|
||||
code: connect.CodeNotFound,
|
||||
},
|
||||
{
|
||||
err: errors.New(model.GeneralErrorCode),
|
||||
code: connect.CodeUnknown,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := errFormat(test.err)
|
||||
|
||||
var connectErr *connect.Error
|
||||
ok := errors.As(err, &connectErr)
|
||||
|
||||
if !ok {
|
||||
t.Error("formatted error is not of type connect.Error")
|
||||
}
|
||||
|
||||
if connectErr.Code() != test.code {
|
||||
t.Errorf("expected code %s, but got code %s for model error %s", test.code, connectErr.Code(),
|
||||
test.err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"buf.build/gen/go/open-feature/flagd/grpc/go/flagd/sync/v1/syncv1grpc"
|
||||
rpc "buf.build/gen/go/open-feature/flagd/grpc/go/sync/v1/syncv1grpc"
|
||||
syncv12 "buf.build/gen/go/open-feature/flagd/protocolbuffers/go/flagd/sync/v1"
|
||||
syncv1 "buf.build/gen/go/open-feature/flagd/protocolbuffers/go/sync/v1"
|
||||
"github.com/open-feature/flagd/core/pkg/logger"
|
||||
"github.com/open-feature/flagd/core/pkg/subscriptions"
|
||||
|
|
@ -12,12 +14,59 @@ import (
|
|||
)
|
||||
|
||||
type handler struct {
|
||||
syncv1grpc.UnimplementedFlagSyncServiceServer
|
||||
syncStore subscriptions.Manager
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func (nh *handler) SyncFlags(
|
||||
request *syncv12.SyncFlagsRequest,
|
||||
server syncv1grpc.FlagSyncService_SyncFlagsServer,
|
||||
) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
errChan := make(chan error)
|
||||
dataSync := make(chan sync.DataSync)
|
||||
nh.syncStore.RegisterSubscription(ctx, request.GetSelector(), request, dataSync, errChan)
|
||||
for {
|
||||
select {
|
||||
case e := <-errChan:
|
||||
return e
|
||||
case d := <-dataSync:
|
||||
if err := server.Send(&syncv12.SyncFlagsResponse{
|
||||
FlagConfiguration: d.FlagData,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error sending configuration change event: %w", err)
|
||||
}
|
||||
case <-server.Context().Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (nh *handler) FetchAllFlags(
|
||||
ctx context.Context,
|
||||
request *syncv12.FetchAllFlagsRequest,
|
||||
) (*syncv12.FetchAllFlagsResponse, error) {
|
||||
data, err := nh.syncStore.FetchAllFlags(ctx, request, request.GetSelector())
|
||||
if err != nil {
|
||||
return &syncv12.FetchAllFlagsResponse{}, fmt.Errorf("error fetching all flags from sync store: %w", err)
|
||||
}
|
||||
|
||||
return &syncv12.FetchAllFlagsResponse{
|
||||
FlagConfiguration: data.FlagData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// oldHandler is the implementation of the old sync schema.
|
||||
// this will not be required anymore when it is time to work on https://github.com/open-feature/flagd/issues/1088
|
||||
type oldHandler struct {
|
||||
rpc.UnimplementedFlagSyncServiceServer
|
||||
syncStore subscriptions.Manager
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
func (l *handler) FetchAllFlags(ctx context.Context, req *syncv1.FetchAllFlagsRequest) (
|
||||
func (l *oldHandler) FetchAllFlags(ctx context.Context, req *syncv1.FetchAllFlagsRequest) (
|
||||
*syncv1.FetchAllFlagsResponse,
|
||||
error,
|
||||
) {
|
||||
|
|
@ -31,7 +80,7 @@ func (l *handler) FetchAllFlags(ctx context.Context, req *syncv1.FetchAllFlagsRe
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (l *handler) SyncFlags(
|
||||
func (l *oldHandler) SyncFlags(
|
||||
req *syncv1.SyncFlagsRequest,
|
||||
stream rpc.FlagSyncService_SyncFlagsServer,
|
||||
) error {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
syncv1 "buf.build/gen/go/open-feature/flagd/grpc/go/flagd/sync/v1/syncv1grpc"
|
||||
rpc "buf.build/gen/go/open-feature/flagd/grpc/go/sync/v1/syncv1grpc"
|
||||
"github.com/open-feature/flagd/core/pkg/logger"
|
||||
iservice "github.com/open-feature/flagd/core/pkg/service"
|
||||
|
|
@ -26,6 +27,8 @@ type Server struct {
|
|||
server *http.Server
|
||||
metricsServer *http.Server
|
||||
Logger *logger.Logger
|
||||
// oldHandler will not be required anymore when https://github.com/open-feature/flagd/issues/1088 is being worked on
|
||||
oldHandler *oldHandler
|
||||
handler *handler
|
||||
config iservice.Configuration
|
||||
grpcServer *grpc.Server
|
||||
|
|
@ -33,11 +36,17 @@ type Server struct {
|
|||
}
|
||||
|
||||
func NewServer(logger *logger.Logger, store subscriptions.Manager) *Server {
|
||||
return &Server{
|
||||
handler: &handler{
|
||||
theOldHandler := &oldHandler{
|
||||
logger: logger,
|
||||
syncStore: store,
|
||||
},
|
||||
}
|
||||
theNewHandler := &handler{
|
||||
logger: logger,
|
||||
syncStore: store,
|
||||
}
|
||||
return &Server{
|
||||
oldHandler: theOldHandler,
|
||||
handler: theNewHandler,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
|
@ -92,8 +101,10 @@ func (s *Server) startServer() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("error setting up listener for address %s: %w", address, err)
|
||||
}
|
||||
|
||||
s.grpcServer = grpc.NewServer()
|
||||
rpc.RegisterFlagSyncServiceServer(s.grpcServer, s.handler)
|
||||
rpc.RegisterFlagSyncServiceServer(s.grpcServer, s.oldHandler)
|
||||
syncv1.RegisterFlagSyncServiceServer(s.grpcServer, s.handler)
|
||||
|
||||
if err := s.grpcServer.Serve(
|
||||
lis,
|
||||
|
|
@ -107,8 +118,8 @@ func (s *Server) startServer() error {
|
|||
func (s *Server) startMetricsServer() error {
|
||||
s.Logger.Info(fmt.Sprintf("binding metrics to %d", s.config.ManagementPort))
|
||||
|
||||
grpc := grpc.NewServer()
|
||||
grpc_health_v1.RegisterHealthServer(grpc, health.NewServer())
|
||||
grpcServer := grpc.NewServer()
|
||||
grpc_health_v1.RegisterHealthServer(grpcServer, health.NewServer())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/healthz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -124,9 +135,9 @@ func (s *Server) startMetricsServer() error {
|
|||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
// if this is 'application/grpc' and HTTP2, handle with gRPC, otherwise HTTP.
|
||||
if request.ProtoMajor == 2 && strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpc") {
|
||||
grpc.ServeHTTP(writer, request)
|
||||
// if this is 'application/grpcServer' and HTTP2, handle with gRPC, otherwise HTTP.
|
||||
if request.ProtoMajor == 2 && strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpcServer") {
|
||||
grpcServer.ServeHTTP(writer, request)
|
||||
} else {
|
||||
mux.ServeHTTP(writer, request)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ func (s *Server) captureMetrics() error {
|
|||
provider := metric.NewMeterProvider(metric.WithReader(exporter))
|
||||
meter := provider.Meter(serviceName)
|
||||
|
||||
syncGuage, err := meter.Int64ObservableGauge(
|
||||
syncGauge, err := meter.Int64ObservableGauge(
|
||||
"sync_active_streams",
|
||||
api.WithDescription("number of open sync subscriptions"),
|
||||
)
|
||||
|
|
@ -30,9 +30,9 @@ func (s *Server) captureMetrics() error {
|
|||
}
|
||||
|
||||
_, err = meter.RegisterCallback(func(_ context.Context, o api.Observer) error {
|
||||
o.ObserveInt64(syncGuage, s.handler.syncStore.GetActiveSubscriptionsInt64())
|
||||
o.ObserveInt64(syncGauge, s.handler.syncStore.GetActiveSubscriptionsInt64())
|
||||
return nil
|
||||
}, syncGuage)
|
||||
}, syncGauge)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to register active subscription metric callback: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ docker run \
|
|||
Test it out by running the following cURL command in a separate terminal:
|
||||
|
||||
```shell
|
||||
curl -X POST "http://localhost:8013/schema.v1.Service/ResolveBoolean" \
|
||||
curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
|
||||
-d '{"flagKey":"show-welcome-banner","context":{}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ Open the `demo.flagd.json` file in a text editor and change the `defaultVariant`
|
|||
Save and rerun the following cURL command:
|
||||
|
||||
```shell
|
||||
curl -X POST "http://localhost:8013/schema.v1.Service/ResolveBoolean" \
|
||||
curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
|
||||
-d '{"flagKey":"show-welcome-banner","context":{}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ In this section, we'll talk about a multi-variant feature flag can be used to co
|
|||
Save and rerun the following cURL command:
|
||||
|
||||
```shell
|
||||
curl -X POST "http://localhost:8013/schema.v1.Service/ResolveString" \
|
||||
curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveString" \
|
||||
-d '{"flagKey":"background-color","context":{}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ If there isn't a match, the `defaultVariant` is returned.
|
|||
Let's confirm that customers are still seeing the `red` variant by running the following command:
|
||||
|
||||
```shell
|
||||
curl -X POST "http://localhost:8013/schema.v1.Service/ResolveString" \
|
||||
curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveString" \
|
||||
-d '{"flagKey":"background-color","context":{"company": "stark industries"}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
|
|
@ -190,7 +190,7 @@ Let's confirm that employees of Initech are seeing the updated variant.
|
|||
Run the following cURL command in the terminal:
|
||||
|
||||
```shell
|
||||
curl -X POST "http://localhost:8013/schema.v1.Service/ResolveString" \
|
||||
curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveString" \
|
||||
-d '{"flagKey":"background-color","context":{"company": "initech"}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ will return variant `red` 50% of the time, `blue` 20% of the time & `green` 30%
|
|||
Command:
|
||||
|
||||
```shell
|
||||
curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "foo@bar.com"}}' -H "Content-Type: application/json"
|
||||
curl -X POST "localhost:8013/flagd.evaluation.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "foo@bar.com"}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
@ -105,7 +105,7 @@ Result:
|
|||
Command:
|
||||
|
||||
```shell
|
||||
curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "foo@test.com"}}' -H "Content-Type: application/json"
|
||||
curl -X POST "localhost:8013/flagd.evaluation.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "foo@test.com"}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ will return variant `red`, if the value of the `version` is a semantic version t
|
|||
Command:
|
||||
|
||||
```shell
|
||||
curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"version": "1.0.1"}}' -H "Content-Type: application/json"
|
||||
curl -X POST "localhost:8013/flagd.evaluation.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"version": "1.0.1"}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
@ -74,7 +74,7 @@ Result:
|
|||
Command:
|
||||
|
||||
```shell
|
||||
curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"version": "0.1.0"}}' -H "Content-Type: application/json"
|
||||
curl -X POST "localhost:8013/flagd.evaluation.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"version": "0.1.0"}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ will return variant `red`, if the value of the `email` property starts with `use
|
|||
Command:
|
||||
|
||||
```shell
|
||||
curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "user@faas.com"}}' -H "Content-Type: application/json"
|
||||
curl -X POST "localhost:8013/flagd.evaluation.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "user@faas.com"}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
@ -71,7 +71,7 @@ Result:
|
|||
Command:
|
||||
|
||||
```shell
|
||||
curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "foo@bar.com"}}' -H "Content-Type: application/json"
|
||||
curl -X POST "localhost:8013/flagd.evaluation.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "foo@bar.com"}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
@ -132,7 +132,7 @@ will return variant `red`, if the value of the `email` property ends with `faas.
|
|||
Command:
|
||||
|
||||
```shell
|
||||
curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "user@faas.com"}}' -H "Content-Type: application/json"
|
||||
curl -X POST "localhost:8013/flagd.evaluation.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "user@faas.com"}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
@ -144,7 +144,7 @@ Result:
|
|||
Command:
|
||||
|
||||
```shell
|
||||
curl -X POST "localhost:8013/schema.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "foo@bar.com"}}' -H "Content-Type: application/json"
|
||||
curl -X POST "localhost:8013/flagd.evaluation.v1.Service/ResolveString" -d '{"flagKey":"headerColor","context":{"email": "foo@bar.com"}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@ Example:
|
|||
It is an object containing the possible variations supported by the flag.
|
||||
All the values of the object **must** be the same type (e.g. boolean, numbers, string, JSON).
|
||||
The type used as the variant value will correspond directly affects how the flag is accessed.
|
||||
For example, to use a flag configured with boolean values the `/schema.v1.Service/ResolveBoolean` path should be used.
|
||||
If another path, such as `/schema.v1.Service/ResolveString` is called, a type mismatch occurs and an error is returned.
|
||||
For example, to use a flag configured with boolean values the `/flagd.evaluation.v1.Service/ResolveBoolean` path should be used.
|
||||
If another path, such as `/flagd.evaluation.v1.Service/ResolveString` is called, a type mismatch occurs and an error is returned.
|
||||
|
||||
Example:
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ spec:
|
|||
|
||||
```bash
|
||||
// From within the pod
|
||||
curl --location 'http://localhost:8080/schema.v1.Service/ResolveString' --header 'Content-Type: application/json' --data '{ "flagKey":"foo"}'
|
||||
curl --location 'http://localhost:8080/flagd.evaluation.v1.Service/ResolveString' --header 'Content-Type: application/json' --data '{ "flagKey":"foo"}'
|
||||
```
|
||||
|
||||
In a real application, rather than `curl`, you would probably use the OpenFeature SDK with the `flagd` provider.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Why is my `int` response a `string`?
|
|||
Command:
|
||||
|
||||
```sh
|
||||
curl -X POST "localhost:8013/schema.v1.Service/ResolveInt" -d '{"flagKey":"myIntFlag","context":{}}' -H "Content-Type: application/json"
|
||||
curl -X POST "localhost:8013/flagd.evaluation.v1.Service/ResolveInt" -d '{"flagKey":"myIntFlag","context":{}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
@ -40,7 +40,7 @@ If a number value is required, and none of the provided SDK's can be used, then
|
|||
Command:
|
||||
|
||||
```sh
|
||||
curl -X POST "localhost:8013/schema.v1.Service/ResolveFloat" -d '{"flagKey":"myIntFlag","context":{}}' -H "Content-Type: application/json"
|
||||
curl -X POST "localhost:8013/flagd.evaluation.v1.Service/ResolveFloat" -d '{"flagKey":"myIntFlag","context":{}}' -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ export default function () {
|
|||
export function genUrl(type) {
|
||||
switch (type) {
|
||||
case "boolean":
|
||||
return "http://localhost:8013/schema.v1.Service/ResolveBoolean"
|
||||
return "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean"
|
||||
case "string":
|
||||
return "http://localhost:8013/schema.v1.Service/ResolveString"
|
||||
return "http://localhost:8013/flagd.evaluation.v1.Service/ResolveString"
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ spec:
|
|||
- '-c'
|
||||
- |
|
||||
for i in $(seq 1 3000); do
|
||||
curl -H 'Cache-Control: no-cache, no-store' -X POST flagd-svc.$FLAGD_DEV_NAMESPACE.svc.cluster.local:8013/schema.v1.Service/ResolveString?$RANDOM -d '{"flagKey":"myStringFlag","context":{}}' -H "Content-Type: application/json" > ~/out.txt
|
||||
curl -H 'Cache-Control: no-cache, no-store' -X POST flagd-svc.$FLAGD_DEV_NAMESPACE.svc.cluster.local:8013/flagd.evaluation.v1.Service/ResolveString?$RANDOM -d '{"flagKey":"myStringFlag","context":{}}' -H "Content-Type: application/json" > ~/out.txt
|
||||
if ! grep -q "val1" ~/out.txt
|
||||
then
|
||||
cat ~/out.txt
|
||||
|
|
|
|||
Loading…
Reference in New Issue