feat(openfeature): emit server-side EVP flagevaluation#18613
feat(openfeature): emit server-side EVP flagevaluation#18613leoromanovsky wants to merge 40 commits into
Conversation
…gator - Two-tier aggregation (full → degraded → drop-counted); no ultra-degraded tier - Canonical context key: sorted, type-tagged, length-delimited (NOT a hash) - Caps GLOBAL_CAP=131072 / PER_FLAG_CAP=10000 / DEGRADED_CAP=32768 - Context pruning: ≤256 fields / ≤256 chars per reviewer concern #1 - Non-blocking queue.Queue(4096) enqueue with observable drop counter - EVP POST to /evp_proxy/v2/api/v2/flagevaluations with X-Datadog-EVP-Subdomain - Unit tests: canonical key, two-tier overflow, prune, enqueue, payload shape
…ATION_COUNTS_ENABLED killswitch - FlagEvaluationHook(Hook).finally_after: cheap capture + non-blocking enqueue only - Eval-time from metadata["dd.eval.timestamp_ms"]; fallback to hook-fire time - runtime_default_used from absent/None variant (reviewer concern #5) - Provider stamps dd.eval.timestamp_ms at eval boundary (_resolve_details) - Killswitch DD_FLAGGING_EVALUATION_COUNTS_ENABLED (default on) gates EVP path only - OTel FlagEvalHook (_flageval_metrics.py) byte-for-byte unchanged (PRES-01) - Tests: hook lifecycle, killswitch gating, OTel non-regression, metadata extraction
…OVIDER_ERROR Root cause: initialize() raised ProviderNotReadyError after the 10s RC wait, causing the OpenFeature SDK to dispatch PROVIDER_ERROR and mark the provider as ERROR. In ffe-dogfooding, with no FFE flags configured in the org, RC never delivers FFE_FLAGS config, so every start ended in PROVIDER_ERROR and the container exited. Fix: on timeout, log a warning, set _status = READY, and return normally. The SDK dispatches PROVIDER_READY; evaluations degrade to (default_value, ERROR, PROVIDER_NOT_READY) via _resolve_details() until config eventually arrives. On_configuration_received() remains the authoritative path for making config available, unchanged. Update affected tests to reflect the new READY-on-timeout contract.
…ad of PROVIDER_ERROR" This reverts commit 114216b.
…icorn pre-fork workers Root cause: initialize() was blocking on _config_received.wait(timeout=10s) and raising ProviderNotReadyError on expiry. Under gunicorn (and uWSGI) pre-fork workers, the OpenFeature SDK runs initialize() in a background thread; that thread is killed by fork() in each child process, so every worker's initialize() either misses the RC notification (READY in master fires before worker registers via _register_provider) or times out waiting — causing PROVIDER_ERROR and container exit. Evidence (dogfooding logs): 22:44:31 PROVIDER_READY ← worker 1 took fast path (config in master) 22:44:41 PROVIDER_ERROR ← worker 2 timed out (10 s exactly) Fix: remove the blocking wait entirely. initialize() now returns immediately if no config is available yet, matching the original pre-#16650 contract. The async path via on_configuration_received() remains the authoritative READY transition for both the master process and each forked worker (worker RC subscriber replays FFE_FLAGS from the inherited connector queue, which calls _notify_providers_config_received() once the worker's provider is registered). The fast path (config already available at initialize() time) is preserved — this covers the case where the master received config before forking. Update tests: replace TestProviderInitializationBlocking with TestProviderInitializationAsync verifying the new non-blocking contract. Remove the fast_initialization_timeout conftest fixture (no longer needed since initialize() does not block).
…arks + e2e tests - Read DD_FLAGGING_EVALUATION_COUNTS_ENABLED through OpenFeatureConfig (ddtrace config var system) instead of raw os.environ; register it in supported-configurations.json. - Stamp eval timestamp at the provider resolution boundary so the finally_after hook receives accurate eval-time. - Normalize typing annotations (dict[...] generics) and mypy overrides for the flagevaluation writer/hook. - Add openfeature_flagevaluation microbenchmark suite (hook-enqueue / aggregate / hook-plus-drain, incl. a 2500-flag high-cardinality variant) wired into benchmarks/suitespec.yml. - Add end-to-end provider tests covering hook firing on the real eval path, all exit paths (success/error/runtime-default/disabled), and OTel non-regression.
Strip internal validation-gate markers and review-tracking references from test docstrings and comments; keep the substantive test descriptions intact.
|
Codeowners resolved as |
BenchmarksBenchmark execution time: 2026-07-06 04:56:15 Comparing candidate commit e6c25dc in PR branch Found 0 performance improvements and 5 performance regressions! Performance is the same for 591 metrics, 10 unstable metrics. scenario:iast_aspects-re_expand_aspect
scenario:iast_aspects-re_search_aspect
scenario:iastaspects-add_aspect
scenario:iastaspectsospath-ospathbasename_aspect
scenario:span-start
|
This comment has been minimized.
This comment has been minimized.
…46-evp-flagevaluation-python
Circular import analysis
|
Motivation
Server-side feature-flag evaluations need to be observable in the same backend track as the rest of the cross-SDK rollout so APM can judge rollout smoothness and time-to-land from real evaluation counts, not only app-local behavior or metric-side approximations. This contribution gives Python services backend-visible
flagevaluationtelemetry with bounded cardinality and batching, so customers and internal dogfooding can verify which flags evaluated, for which targeting keys, and how often, while preserving the existingfeature_flag.evaluationsmetric path.Changes
flagevaluationwriter behindDD_FLAGGING_EVALUATION_COUNTS_ENABLED; existing OTelfeature_flag.evaluationsmetrics remain unchanged.finally_afterlogging hook that captures a cheap evaluation snapshot and hands it to a background writer.FlagEvalEVPHook/_flag_eval_evp_hook.pyso it is distinct fromFlagEvalMetricsHook./evp_proxy/v2/api/v2/flagevaluationwithX-Datadog-EVP-Subdomain: event-platform-intake.flagEvaluationsrequest bodies under the shared EVP payload limit before POSTing.targeting_keyandcontext; if the degraded row still cannot fit, the writer drops and logs it.first_evaluationandlast_evaluationrather than flush time.DataDogProvider(initialization_timeout=...)keyword for compatibility, while async initialization no longer uses it.Evaluation Capture And Flush
Payload Limit Handling
flowchart TD Events["Aggregated flagEvaluations"] --> Encode["Encode each event with compact JSON"] Encode --> FitsEvent{"Single event fits\ninside shared EVP limit?"} FitsEvent -- "yes" --> Batch["Add to current payload"] FitsEvent -- "no" --> Degrade["Drop targeting_key and context\nfor that event only"] Degrade --> FitsDegraded{"Degraded event fits?"} FitsDegraded -- "yes" --> Batch FitsDegraded -- "no" --> Drop["Drop and count\npayload_limit"] Batch --> FitsPayload{"Payload still <=\nDEFAULT_EVP_PAYLOAD_SIZE_LIMIT?"} FitsPayload -- "yes" --> Continue["Continue batching"] FitsPayload -- "no" --> Split["Close current payload\nstart next payload"] Split --> Continue Continue --> Post["POST each payload sequentially"]Decisions
flagevaluationbackend route instead of adding a Python-specific transport or schema.flagEvaluationspayloads by encoded byte size usingDEFAULT_EVP_PAYLOAD_SIZE_LIMIT; send split payloads sequentially from the writer thread.targeting_keyand pruned context; degraded buckets retain only lower-cardinality schema dimensions.targeting_keyin the dedicated EVP field and removetargetingKey/targeting_keyfrom context before building context keys or payloads.reasonbecause it is not accepted by the canonical worker schema, and do not populatetargeting_rule.keyuntil Python exposes real targeting-rule metadata.openfeature-sdkas an application/test/lint dependency rather than adding it as a runtime dependency ofddtrace; the dogfooding app dependency floor is being tightened separately in https://github.com/DataDog/ffe-dogfooding/pull/88.Validation Evidence
Dogfooding App
ffe-dogfoodingapp-pythonwas run with localdd-trace-pyartifacts and reachedPROVIDER_READY.openfeature-sdk=0.9.0.ffe-dogfooding-string-flagthrough the Python dogfooding app with 5 evaluations per targeting key over one persistent connection:python-batch-keepalive-20260623T004034Z-alphapython-batch-keepalive-20260623T004034Z-bravopython-batch-keepalive-20260623T004034Z-charlievariant_2with allocationallocation-override-392dd7c149f8.System Tests
Staging End-To-End
flagevaluationrows for the exact targeting keys above.evaluation_count=5:python-batch-keepalive-20260623T004034Z-alphapython-batch-keepalive-20260623T004034Z-bravopython-batch-keepalive-20260623T004034Z-charlieflag.key=ffe-dogfooding-string-flag,variant.key=variant_2, andallocation.key=allocation-override-392dd7c149f8.