feat(openfeature): emit server-side EVP flagevaluation#11639
feat(openfeature): emit server-side EVP flagevaluation#11639leoromanovsky wants to merge 27 commits into
Conversation
…EVP writer - Tests for identical-event aggregation (count 2, first<=last min/max) - Test for type-tagged canonical key distinguishing int vs string - Tests for global/degraded cap overflow and drop-counted overflow - Test for absent variant -> runtimeDefaultUsed - Tests for 256-field/256-char context pruning - Tests for flush posting to 'flagevaluations' with required JSON fields
…nWriterImpl two-tier EVP writer - FlagEvalEvent (bootstrap): lightweight data record for hook->writer channel - FlagEvaluationWriter (bootstrap): interface with enqueue/start/close - FlagEvaluationWriterImpl (lib): two-tier aggregation with frozen contract constants - GLOBAL_CAP=131072 / PER_FLAG_CAP=10000 / DEGRADED_CAP=32768 - Canonical context key: sorted, type-tagged, length-delimited (no hash; reviewer concern #3) - Context pruning: <=256 fields, string values <=256 chars (reviewer concern #1) - Min/max first/last eval time (reviewer concern #4) - Absent variant -> runtimeDefaultUsed (reviewer concern #5) - Drop-counted overflow (reviewer concern #8) - Posts to 'flagevaluations' via BackendApiFactory(EVENT_PLATFORM) - Flush interval 10s (differs from exposure 1s) - Add FEATURE_FLAG_EVALUATION_PROCESSOR thread to AgentThreadFactory - Register/deregister writer with FeatureFlaggingGateway - FlagEvaluationWriterImplTest: all 10 unit tests GREEN
- Test: enqueue called with flagKey, variant, reason (lowercased), allocationKey - Test: evalTimeMs from dd.eval.timestamp_ms metadata (reviewer concern #4) - Test: evalTimeMs fallback to hook-fire time when metadata absent - Test: null value -> null variant (runtime default; reviewer concern #5) - Test: only enqueue called, no inline aggregation (reviewer concern #7) - Test: writer=null is no-op (killswitch off) - Test: targetingKey extracted from evaluation context
- FlagEvalEVPHook: Hook<T> with finallyAfter() doing cheap capture only
- Reads allocationKey from metadata.getString('allocationKey')
- Reads eval-time from metadata.getLong('dd.eval.timestamp_ms'), fallback to hook-fire time
- Null value -> null variant (runtime default; reviewer concern #5)
- Lowercases reason string
- Resolves writer lazily from FeatureFlaggingGateway (test: injected directly)
- No aggregation on hook thread (reviewer concern #7)
- DDEvaluator: stamp dd.eval.timestamp_ms in flag metadata at resolution point
- Provider.getProviderHooks(): returns [OTel FlagEvalHook, EVP FlagEvalEVPHook]
- OTel path preserved byte-for-byte (PRES-01 non-regression)
- FeatureFlaggingSystem: create + start FlagEvaluationWriterImpl behind killswitch
- DD_FLAGGING_EVALUATION_COUNTS_ENABLED=false disables EVP path only
- Default: enabled (EVP path on)
- ProviderTest: updated to expect 2 hooks (OTel + EVP)
- FlagEvalEVPHookTest: all 8 unit tests GREEN
- Apply project-required code style (google-java-format) to all new files in this plan before submitting
This comment has been minimized.
This comment has been minimized.
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
… pruning, shutdown drain, and schema validation
- variant sourced from details.getVariant() (not the evaluated value)
- error object captured from evaluation details, schema {"message":...}
- killswitch DD_FLAGGING_EVALUATION_COUNTS_ENABLED via the tracer config system
- deterministic context pruning (sort before cut), stored pruned attrs
- observable queue-overflow drop counter; close() drains and final-flushes
- JMH hot-path benchmark; structural validation against vendored worker schema
… evaluation writer The flag evaluation serializer's lastTicks and globalFullCount fields are confined to the single serializer thread but are written from more than one method, which SpotBugs flags as AT_NONATOMIC_64BIT_PRIMITIVE and AT_STALE_THREAD_WRITE_OF_PRIMITIVE. Annotate both fields with @SuppressFBWarnings and a thread-confinement justification, matching the existing convention used across the tracer.
…uations-cross-sdk # Conflicts: # products/feature-flagging/feature-flagging-lib/gradle.lockfile
…uations-cross-sdk # Conflicts: # products/feature-flagging/feature-flagging-lib/gradle.lockfile
…uations-cross-sdk
|
Hi! 👋 Thanks for your pull request! 🎉 To help us review it, please make sure to:
If you need help, please check our contributing guidelines. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3d4244f8ae
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| final FlagEvaluationWriter w = | ||
| injectedWriter != null ? injectedWriter : FeatureFlaggingGateway.getFlagEvalWriter(); |
There was a problem hiding this comment.
Guard EVP hook lookup against missing bootstrap
When dd-openfeature is used with an older or absent tracer bootstrap on the classpath, resolving the newly added FlagEvaluationWriter/FeatureFlaggingGateway.getFlagEvalWriter() path can throw a LinkageError/NoSuchMethodError here. Because this lookup happens before the try block and the catch only handles Exception, the optional EVP hook can break flag evaluation instead of becoming the intended no-op; this needs the same kind of LinkageError guard used around the OTel hook.
Useful? React with 👍 / 👎.
| String evpProxyEndpoint = featuresDiscovery.getEvpProxyEndpoint(); | ||
| if (preferredEvpProxyEndpoint != null | ||
| && featuresDiscovery.supportsEvpProxyEndpoint(preferredEvpProxyEndpoint)) { | ||
| evpProxyEndpoint = preferredEvpProxyEndpoint; |
There was a problem hiding this comment.
Do not fall back to v4 for v2-only flagevaluation
When the flag-evaluation writer requests V2_EVP_PROXY_ENDPOINT, Agents that advertise only /evp_proxy/v4/ leave evpProxyEndpoint at the discovered v4 value because this condition simply skips the preference. The writer then posts to /evp_proxy/v4/api/v2/flagevaluation, despite this signal being documented in the new writer as using /evp_proxy/v2/api/v2/flagevaluation; those environments will send the new telemetry to the wrong Agent route instead of disabling it when v2 is unavailable.
Useful? React with 👍 / 👎.
Motivation
Customers need consistent server-side feature-flag evaluation visibility across supported runtimes so rollout behavior can be correlated with application behavior in APM and Event Platform. This Java contribution adds that server-side
flagevaluationsignal for Java OpenFeature evaluations while preserving the existing OTelfeature_flag.evaluationspath and the existing exposure telemetry path.Changes
flagevaluationwriter path behindDD_FLAGGING_EVALUATION_COUNTS_ENABLED./evp_proxy/v2/api/v2/flagevaluation.contextandflagEvaluations.targeting_keyas the dedicated event field and removes duplicatetargetingKeyfromcontext.evaluationfor flagevaluation events.timestamp, while preserving evaluation-entry time forfirst_evaluationandlast_evaluation.Decisions
FlagEvaluationsRequestper flush, with a top-levelcontextobject and aflagEvaluationsarray, instead of sending one HTTP request per evaluation.reasonis not a hidden aggregate key because it is not a worker field.targeting_keyas the dedicated event field and keep it out ofcontext.evaluationso the same identity is not encoded twice.timestamp, matching the backend distinction between when evaluations happened and when the SDK sent the batch.Validation Evidence
Dogfooding App
ffe-dogfoodingapp-javawas rebuilt with local Java artifacts and reachedPROVIDER_READY.ffe-dogfooding-string-flagthrough the Java dogfooding app 15 times total: 5 evaluations for each public-safe targeting key:java-batch-evp-20260622T233009Z-alphajava-batch-evp-20260622T233009Z-bravojava-batch-evp-20260622T233009Z-charlievariant_2and allocationallocation-override-392dd7c149f8.System Tests
Staging End-To-End
flagevaluationrows for the exact targeting keys above, proving SDK aggregation/batching instead of one backend row per evaluation.flag.key=ffe-dogfooding-string-flag,variant.key=variant_2,allocation.key=allocation-override-392dd7c149f8, andevaluation_count=5.timestamp(1782171009940), while each row preserved distinctfirst_evaluationandlast_evaluationbounds for the five evaluations in that aggregate.