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:
Florian Bacher 2023-12-21 16:20:01 +01:00 committed by GitHub
parent 3385d58973
commit e9728aae83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1489 additions and 129 deletions

View File

@ -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:

View File

@ -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

View File

@ -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=

View File

@ -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()

View File

@ -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

View File

@ -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 {

View File

@ -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{},

View File

@ -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
}

View File

@ -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
}

View File

@ -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())
}
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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)
}

View File

@ -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"
```

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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.

View File

@ -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:

View File

@ -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"
}
}

View File

@ -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