From 0c4b674aba7f2ec126eda1483f6dceb8e9d0b9b5 Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 25 Aug 2026 16:06:13 +0300 Subject: [PATCH 1/2] Guard eager OpenFeature init and add detailed /ffe/evaluate for Go agentless configuration contract --- utils/build/docker/golang/parametric/ffe.go | 152 +++++++++++++++++-- utils/build/docker/golang/parametric/main.go | 39 ++++- 2 files changed, 175 insertions(+), 16 deletions(-) diff --git a/utils/build/docker/golang/parametric/ffe.go b/utils/build/docker/golang/parametric/ffe.go index 14ac1aa24ff..755636c85dd 100644 --- a/utils/build/docker/golang/parametric/ffe.go +++ b/utils/build/docker/golang/parametric/ffe.go @@ -3,12 +3,40 @@ package main import ( "encoding/json" "net/http" + "sync" + ddof "github.com/DataDog/dd-trace-go/v2/openfeature" of "github.com/open-feature/go-sdk/openfeature" ) -func (s *apmClientServer) ffeStart(http.ResponseWriter, *http.Request) { - return +var ffeStartOnce sync.Once + +func (s *apmClientServer) ffeStart(writer http.ResponseWriter, request *http.Request) { + var startErr error + ffeStartOnce.Do(func() { + provider, err := ddof.NewDatadogProvider(ddof.ProviderConfig{}) + if err != nil { + startErr = err + return + } + + if err := of.SetProvider(provider); err != nil { + startErr = err + return + } + + s.ddProvider = provider + s.ofClient = of.NewClient("system-tests-weblog-client") + }) + + if startErr != nil { + writer.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(writer).Encode(map[string]string{"error": startErr.Error()}) + return + } + + writer.WriteHeader(http.StatusOK) + _ = json.NewEncoder(writer).Encode(map[string]any{}) } func (s *apmClientServer) ffeEval(writer http.ResponseWriter, request *http.Request) { @@ -24,21 +52,127 @@ func (s *apmClientServer) ffeEval(writer http.ResponseWriter, request *http.Requ return } - ctx := of.NewEvaluationContext(body.TargetingKey, body.Attributes) + if s.ofClient == nil { + writer.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(writer).Encode(map[string]string{"error": "FFE provider not initialized"}) + return + } - if initer, ok := s.ddProvider.(of.StateHandler); ok { - initer.Init(ctx) + switch body.VariationType { + case "BOOLEAN", "STRING", "INTEGER", "NUMERIC", "JSON": + default: + http.Error(writer, "unknown variation type: "+body.VariationType, http.StatusBadRequest) + return } - val := s.ofClient.Object(request.Context(), body.Flag, body.DefaultValue, ctx) + ctx := of.NewEvaluationContext(body.TargetingKey, body.Attributes) - writer.WriteHeader(http.StatusOK) + value := body.DefaultValue + reason := string(of.DefaultReason) + var errorCode string + + evalCtx := request.Context() + func() { + defer func() { + if r := recover(); r != nil { + value = body.DefaultValue + reason = "ERROR" + } + }() + + switch body.VariationType { + case "BOOLEAN": + defaultValue, _ := body.DefaultValue.(bool) + details, err := s.ofClient.BooleanValueDetails(evalCtx, body.Flag, defaultValue, ctx) + if err != nil { + value = body.DefaultValue + reason = "ERROR" + return + } + value = details.Value + reason = string(details.Reason) + errorCode = string(details.ErrorCode) + case "STRING": + defaultValue, _ := body.DefaultValue.(string) + details, err := s.ofClient.StringValueDetails(evalCtx, body.Flag, defaultValue, ctx) + if err != nil { + value = body.DefaultValue + reason = "ERROR" + return + } + value = details.Value + reason = string(details.Reason) + errorCode = string(details.ErrorCode) + case "INTEGER": + defaultValue, _ := toInt64(body.DefaultValue) + details, err := s.ofClient.IntValueDetails(evalCtx, body.Flag, defaultValue, ctx) + if err != nil { + value = body.DefaultValue + reason = "ERROR" + return + } + value = details.Value + reason = string(details.Reason) + errorCode = string(details.ErrorCode) + case "NUMERIC": + defaultValue, _ := toFloat64(body.DefaultValue) + details, err := s.ofClient.FloatValueDetails(evalCtx, body.Flag, defaultValue, ctx) + if err != nil { + value = body.DefaultValue + reason = "ERROR" + return + } + value = details.Value + reason = string(details.Reason) + errorCode = string(details.ErrorCode) + case "JSON": + details, err := s.ofClient.ObjectValueDetails(evalCtx, body.Flag, body.DefaultValue, ctx) + if err != nil { + value = body.DefaultValue + reason = "ERROR" + return + } + value = details.Value + reason = string(details.Reason) + errorCode = string(details.ErrorCode) + } + }() + + writer.WriteHeader(http.StatusOK) response := struct { - Value any `json:"value"` - }{val} + Value any `json:"value"` + Reason string `json:"reason"` + ErrorCode string `json:"errorCode"` + }{value, reason, errorCode} if err := json.NewEncoder(writer).Encode(response); err != nil { http.Error(writer, "failed to encode response: "+err.Error(), http.StatusInternalServerError) } } + +func toInt64(v any) (int64, bool) { + switch n := v.(type) { + case int64: + return n, true + case int: + return int64(n), true + case float64: + return int64(n), true + default: + return 0, false + } +} + +func toFloat64(v any) (float64, bool) { + switch n := v.(type) { + case float64: + return n, true + case int: + return float64(n), true + case int64: + return float64(n), true + default: + return 0, false + } +} diff --git a/utils/build/docker/golang/parametric/main.go b/utils/build/docker/golang/parametric/main.go index 509c3fd172c..9c68360570a 100644 --- a/utils/build/docker/golang/parametric/main.go +++ b/utils/build/docker/golang/parametric/main.go @@ -58,19 +58,44 @@ func newServer() *apmClientServer { instruments: make(map[string]interface{}), } - s.ddProvider, err = ddof.NewDatadogProvider(ddof.ProviderConfig{}) - if err != nil { - log.Fatalf("failed to create Datadog OpenFeature provider: %v", err) - } + // The configuration-source contract requires lazy activation: no configuration + // delivery may happen before the provider is accessed through /ffe/start. When any + // Feature Flagging configuration variable is set, skip this eager initialization and + // leave provider setup to /ffe/start. Tests that predate that contract keep the + // original eager behavior. + if !ffeConfigurationEnvVarsSet() { + s.ddProvider, err = ddof.NewDatadogProvider(ddof.ProviderConfig{}) + if err != nil { + log.Fatalf("failed to create Datadog OpenFeature provider: %v", err) + } - if err := of.SetProvider(s.ddProvider); err != nil { - log.Fatalf("failed to set Datadog OpenFeature provider and wait for initialization: %v", err) + if err := of.SetProvider(s.ddProvider); err != nil { + log.Fatalf("failed to set Datadog OpenFeature provider and wait for initialization: %v", err) + } + + s.ofClient = of.NewClient("system-tests-weblog-client") } - s.ofClient = of.NewClient("system-tests-weblog-client") return s } +var ffeConfigurationEnvVars = []string{ + "DD_FEATURE_FLAGS_ENABLED", + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE", + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL", + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS", + "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", +} + +func ffeConfigurationEnvVarsSet() bool { + for _, name := range ffeConfigurationEnvVars { + if _, ok := os.LookupEnv(name); ok { + return true + } + } + return false +} + func main() { flag.String("Darg1", "", "Argument 1") flag.Parse() From 3d5729fb27b404e5e905bf683f6082890a234fd5 Mon Sep 17 00:00:00 2001 From: Pavel Date: Fri, 11 Sep 2026 18:01:37 +0300 Subject: [PATCH 2/2] fix(golang): block /ffe/start on provider init and stop filtering RC polls by verb --- tests/parametric/test_ffe/test_configuration_sources.py | 5 ++++- utils/build/docker/golang/parametric/ffe.go | 5 ++++- utils/build/docker/golang/parametric/main.go | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/parametric/test_ffe/test_configuration_sources.py b/tests/parametric/test_ffe/test_configuration_sources.py index 195edddd1ad..688f9fbdae9 100644 --- a/tests/parametric/test_ffe/test_configuration_sources.py +++ b/tests/parametric/test_ffe/test_configuration_sources.py @@ -151,8 +151,11 @@ def _assert_no_mock_requests(mock_ffe_agentless_backend: MockFFEAgentlessBackend def _remote_config_products(test_agent: TestAgentAPI) -> set[str]: + # Not post_only: golang polls /v0.7/config with GET, every other library with + # POST. The verb says nothing about which products the body advertises, and + # the capability assertion next to this one is already unfiltered. products: set[str] = set() - for request in test_agent.rc_requests(post_only=True): + for request in test_agent.rc_requests(): client = request["body"].get("client", {}) products.update(client.get("products", [])) return products diff --git a/utils/build/docker/golang/parametric/ffe.go b/utils/build/docker/golang/parametric/ffe.go index 755636c85dd..d17c115ff02 100644 --- a/utils/build/docker/golang/parametric/ffe.go +++ b/utils/build/docker/golang/parametric/ffe.go @@ -20,7 +20,10 @@ func (s *apmClientServer) ffeStart(writer http.ResponseWriter, request *http.Req return } - if err := of.SetProvider(provider); err != nil { + // AndWait: plain SetProvider returns before Init, so /ffe/start would + // answer 200 with no configuration and the next evaluation gets the + // default. Other SDKs block on initialize inside set_provider. + if err := of.SetProviderAndWait(provider); err != nil { startErr = err return } diff --git a/utils/build/docker/golang/parametric/main.go b/utils/build/docker/golang/parametric/main.go index 9c68360570a..040e58288b3 100644 --- a/utils/build/docker/golang/parametric/main.go +++ b/utils/build/docker/golang/parametric/main.go @@ -69,8 +69,12 @@ func newServer() *apmClientServer { log.Fatalf("failed to create Datadog OpenFeature provider: %v", err) } + // Async on purpose, unlike /ffe/start: this runs for every parametric + // test that sets no Feature Flagging variable, and waiting would add the + // 10s DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS to each + // container start. if err := of.SetProvider(s.ddProvider); err != nil { - log.Fatalf("failed to set Datadog OpenFeature provider and wait for initialization: %v", err) + log.Fatalf("failed to set Datadog OpenFeature provider: %v", err) } s.ofClient = of.NewClient("system-tests-weblog-client")