Commit 87f1683
Add FFE evaluation metrics (#3911)
* Emit feature_flag.evaluations through libdatadog sidecar OTLP forwarder
Adds the M3 evaluation-metrics layer on top of the hook PR (#3909) as a
sibling of the EVP exposures PR (#3910). Records `feature_flag.evaluations`
for both PHP 7 (DD Client hook) and PHP 8 (OpenFeature SDK hook); both
paths share `EvaluationMetricHook::sharedWriter()` for unified
aggregation. OTLP/protobuf payloads are encoded in PHP via the existing
`OtlpMetricEncoder` and delivered to the user-configured OTLP HTTP
metrics intake through the libdatadog sidecar (`ddog_sidecar_send_ffe_metrics`
FFI added in DataDog/libdatadog#2026).
This branch is force-pushed (user-authorized one-time exception to the
no-force-push rule, 2026-05-23) to restructure history away from being
linearly stacked on the M2 exposures PR (#3910). The PR now stacks
directly on the hook PR (#3909) as a sibling of the EVP PR.
PHP side:
- Add `Internal/Metric/EvaluationMetricWriter` with bounded series
aggregation, drop accounting, and shutdown flush.
- Add `Internal/Metric/EvaluationMetricHook` (DD Client hook) and
`OtlpMetricEncoder` (PHP 7-safe protobuf encoding).
- Add `Internal/Metric/SidecarOtlpMetricsTransport` that calls
`\DDTrace\send_ffe_metrics()` (FFI declared in #3910). Endpoint
resolution: `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, falling back to
`OTEL_EXPORTER_OTLP_ENDPOINT + /v1/metrics`, default
`http://localhost:4318/v1/metrics`.
- Add `DDTrace\OpenFeature\EvalMetricsHook` implementing
`OpenFeature\interfaces\hooks\Hook` (after + error stages), registered
on `DataDogProvider` via `setHooks()`.
- `DataDogProvider` constructs its internal DD `Client` with
`DefaultEvaluationCompletedHook::createWithoutMetric()` so the
OpenFeature path records the metric via the OpenFeature hook (PR 3911
scope) and NOT via the DD Client hook — preventing double-counting.
PHP 7 path keeps recording via the DD Client hook.
- Add `Internal/CompositeEvaluationCompletedHook` and
`Internal/DefaultEvaluationCompletedHook` (metric-only composite).
This is the merge-conflict point with PR #3910's `[ExposureHook]`
composite — second merge resolves by combining both hooks.
- Update `Client::create()` to call `DefaultEvaluationCompletedHook::create()`.
- Drop the obsolete `testOtlpTransportBuildsHttpProtobufRequest` PHPUnit
test (HTTP construction now lives in libdatadog, covered by
`cargo test -p datadog-sidecar ffe_metrics_flusher`).
- Add `_files_openfeature.php` entry for `EvalMetricsHook.php`.
C/Rust bridge: the `\DDTrace\send_ffe_metrics()` native function, its C
wrapper `ddtrace_sidecar_send_ffe_metrics()`, and the
`ddog_sidecar_send_ffe_metrics` FFI declaration in `components-rs/sidecar.h`
were already added in #3910. This PR's branch picks up those changes
once #3910 merges (or via the same libdatadog submodule pin during
review). For development locally the libdatadog submodule is pinned to
the FFE branch tip (`29762335c`).
Docs:
- Add `docs/php-ffe-stack/{stack,system}-pr3911.{mmd,png}` per the
4-PR documentation convention.
Validation:
- `php vendor/bin/phpunit --config phpunit.xml tests/api/Unit/FeatureFlags`
→ 40 tests, 160 assertions, OK.
- Mermaid PNGs regenerate via `npx @mermaid-js/mermaid-cli`.
`make test_featureflags`, OpenFeature PHPUnit, and ffe-dogfooding
end-to-end validation will run in CI / are validated separately by
FOLLOW-05 Steps 4–5.
* Fix OTLP endpoint resolution for the test-agent / fallback path
The SidecarOtlpMetricsTransport::resolveEndpoint() default ("http://localhost:4318/v1/metrics")
doesn't match the system-tests parametric setup, where the PHP test
client receives DD_AGENT_HOST=<test_agent_container> but no
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT. The previous OtlpHttpMetricTransport
(replaced by this transport) derived the OTLP endpoint from DD_AGENT_HOST
+ port 4318. Restore that fallback so system-tests
test_php_ffe_evaluation_metric finds the test-agent OTLP intake.
Resolution order (now matches the old transport):
1. OTEL_EXPORTER_OTLP_METRICS_ENDPOINT (explicit)
2. OTEL_EXPORTER_OTLP_ENDPOINT + /v1/metrics
3. DD_AGENT_HOST + :4318/v1/metrics
4. localhost:4318/v1/metrics
* Add native send_ffe_exposures + send_ffe_metrics C+Rust bridge
The C/Rust bridge for the new native PHP functions
\DDTrace\send_ffe_exposures() and \DDTrace\send_ffe_metrics() lives on
the M2 EVP exposures PR (#3910) because that PR introduced the bridge
when refactoring the exposure transport. PR #3911 (this PR) needs the
same bridge for its OTLP metrics transport — without it the
SidecarOtlpMetricsTransport silently drops batches because
function_exists('\\DDTrace\\send_ffe_metrics') is false.
Adds the same bridge files here so the M3 branch is independently
compilable. At merge time the two PRs will conflict at the file level on
these bridge files; resolution is deduplication (the bridge is identical
in both PRs by design).
Files added/modified:
- components-rs/sidecar.h: declares ddog_sidecar_send_ffe_exposures and
ddog_sidecar_send_ffe_metrics FFIs.
- components-rs/common.h: declares ddog_ByteSlice typedef for the
metrics payload.
- ext/sidecar.h, ext/sidecar.c: C wrappers
ddtrace_sidecar_send_ffe_exposures() and ddtrace_sidecar_send_ffe_metrics().
- ext/ddtrace.stub.php, ext/ddtrace_arginfo.h, ext/ddtrace.c: declares
the native PHP functions and the PHP_FUNCTION implementations.
* Update libdatadog submodule to pick up FFE dispatch fix
Pulls in libdatadog commit `875ec8f0e` ("fix(sidecar): dispatch FFE
actions before application-entry check"). Without this fix, the
`SidecarOtlpMetricsTransport::send()` call from PHP would silently
no-op for short-lived processes: the sidecar received the
`FfeMetrics` action but dropped it because the `Entry::Occupied`
gate on the application metadata had not yet fired.
This unblocks the parametric system-test
`Test_Feature_Flag_Parametric_Evaluation_Metrics::test_php_ffe_evaluation_metric`
which exercises the full PHP -> sidecar -> OTLP-HTTP-intake path
end-to-end. Local result: 26/27 FFE-scoped parametric tests pass
(remaining failure is the EVP exposure test, which lives on the
M2 PR #3910 branch).
* docs(ffe): quote diagram titles, switch system to TD, re-render at high rez
Three fixes to the per-PR FFE diagrams:
1. **Titles were truncated to "PHP FFE 4-PR stack — current =" with the
PR number missing.** Mermaid uses the YAML frontmatter for the title
block, and YAML treats unquoted `#` as the start of a comment, so
`title: PHP FFE 4-PR stack — current = #3911 (OTLP metrics)` got
parsed as just `PHP FFE 4-PR stack — current =`. Quoting the title
keeps the `#PR-number` portion intact.
2. **System diagrams switched from `flowchart LR` to `flowchart TD`.**
LR forced the PHP-process / host / backend lanes into a single
very-wide row that rendered as an unreadable horizontal strip on PR
pages. TD stacks them vertically and keeps the per-lane subgraphs
readable.
3. **Re-rendered at 2400×2400 with `--scale 3`** (~1800×2000 stack,
~3000×4500 system) instead of the default ~600px width. PR-page
thumbnails render legibly and zoomed-in detail stays sharp.
README's regeneration recipe updated with all three knobs and a
note on why the `title:` quoting matters.
* docs(ffe): drop 'Hook seam' wording, use 'Hook layer'
* DataDogProvider: never lose the metric hook when a user calls setHooks()
`AbstractProvider::setHooks(array $hooks)` REPLACES the hook list, so
registering our `EvalMetricsHook` via `setHooks([$metricsHook])` in the
constructor would silently drop our metric emission as soon as a user
configures their own provider-level hooks.
Override `getHooks()` to always prepend the Datadog metric hook to the
caller-supplied list. The user can register their own provider hooks
freely (`$provider->setHooks($theirHooks)`) and we still record
`feature_flag.evaluations` on every OpenFeature evaluation.
Adds a unit test that constructs a DataDogProvider, calls
`$provider->setHooks([$userHook])`, then asserts
`$provider->getHooks()` returns `[EvalMetricsHook, $userHook]`.
* SidecarOtlpMetricsTransport: drop custom DD_AGENT_HOST OTLP fallback
`resolveEndpoint()` previously fell back to
`http://$DD_AGENT_HOST:4318/v1/metrics` when neither OTel env var was
set, and also peeled off `unix://` prefixes and wrapped IPv6 hosts in
brackets. That extra path was Datadog-specific and undocumented — it
existed to make the parametric system-tests pass without configuring
the standard OTel env, and to be "helpful" for callers who set
DD_AGENT_HOST but not OTEL_EXPORTER_OTLP_METRICS_ENDPOINT.
Strip it. Follow the OpenTelemetry environment-variable spec only:
1. OTEL_EXPORTER_OTLP_METRICS_ENDPOINT — used as-is
2. OTEL_EXPORTER_OTLP_ENDPOINT — append /v1/metrics
3. http://localhost:4318/v1/metrics — OTel spec default
If a Datadog Agent's OTLP intake is the target, the caller sets
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT explicitly.
System-tests parametric metric test still passes: the parametric test
client now injects OTEL_EXPORTER_OTLP_METRICS_ENDPOINT pointing at the
test-agent OTLP listener (companion change in
DataDog/system-tests:leo.romanovsky/pr-g-php-ffe-scaffold). FFE-scoped
parametric: 26/27 (only the expected exposure_event failure remains —
M3 branch has no exposure code path).
* EvaluationMetricWriter: flush-on-full so long-running runtimes don't silently drop
When the series map reaches `seriesLimit` (default 1000 unique
attribute-sets), the writer was returning false and incrementing
`dropped` for every new key. In PHP-FPM/Apache that was harmless because
`register_shutdown_function` fires per request and the map empties at
the end of each request. In long-running PHP runtimes — Swoole,
RoadRunner, FrankenPHP/Octane, CLI worker loops — the shutdown function
only fires when the worker process exits, so the series map filled
once and then every new unique attribute-set was silently dropped for
the rest of the worker's lifetime (hours or days).
Flush inline when the cap is hit so the new key fits. Same fix as the
parallel commit on the M2 (PR #3910) ExposureWriter.
Test `testSeriesOverflowDropsNewSeriesButKeepsExistingSeries` renamed
and rewritten to assert the new auto-flush behavior:
`droppedCount() === 0` after three records that would have dropped one
under the old code; transport receives three batches instead of one.
Caught by Codex review on the parallel M2 commit.
* chore(ffe): remove generated stack diagrams
* fix(ffe): tidy evaluation metrics branch base
* chore(ffe): update libdatadog sidecar dependency
* Avoid FFE metric stress argument explosion
* chore(ffe): update libdatadog sidecar dependency
* chore(ffe): update libdatadog sidecar dependency
* chore(ffe): update libdatadog sidecar dependency
* chore(ffe): update libdatadog sidecar dependency
* Fix FFE metric sidecar activation
* Update FFE metrics dependency and result ABI
* Bump libdatadog metric runtime
* Honor Datadog agent config for FFE metrics
* Move FFE metric buffering into tracer
* Remove FFE evaluation RC polling
* test: add unix domain sockets test
* Fix FFE metrics OTLP endpoint handling
* Fix FFE metrics UDS test sidecar setup
* Fix FFE evaluation metrics transport
* Fix FFE metrics UDS header assertion
* Update supported config metadata for OTLP metrics
* Align OTLP endpoint metadata with registry
* fix: start sidecar for FFE evaluation metrics
* Update libdatadog submodule now that PR has merged
* fix: keep FFE config thread-local
* Move ffe_metric_buffer to tracer/
* Simplify configuration for otel metrics, make it first class
Also build the endpoint fully in rust code for simplicity
* Avoid zend_string flip-flopping
* fix: support OTEL metrics endpoint config
* docs: explain PHP FFE metric recording
* Revert "fix: support OTEL metrics endpoint config"
This reverts commit c5934af.
* Fix missing OTEL_
* Fix compilation
Signed-off-by: Bob Weinand <[email protected]>
* Fix otel zai config
Signed-off-by: Bob Weinand <[email protected]>
* Fix tests
---------
Signed-off-by: Bob Weinand <[email protected]>
Co-authored-by: Levi Morrison <[email protected]>
Co-authored-by: Bob Weinand <[email protected]>1 parent 0699758 commit 87f1683
39 files changed
Lines changed: 1183 additions & 78 deletions
File tree
- appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration
- components-rs
- ext
- metadata
- src
- DDTrace/OpenFeature
- api/FeatureFlags/Internal
- Metric
- tests
- OpenFeature
- api/Unit/FeatureFlags
- ext/ffe
- tooling
- tracer
- zend_abstract_interface/config
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 16 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
85 | | - | |
| 85 | + | |
86 | 86 | | |
87 | | - | |
| 87 | + | |
88 | 88 | | |
89 | | - | |
90 | | - | |
| 89 | + | |
| 90 | + | |
91 | 91 | | |
92 | | - | |
| 92 | + | |
93 | 93 | | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
108 | | - | |
| 108 | + | |
109 | 109 | | |
110 | | - | |
| 110 | + | |
111 | 111 | | |
112 | 112 | | |
113 | | - | |
| 113 | + | |
114 | 114 | | |
115 | 115 | | |
116 | | - | |
| 116 | + | |
117 | 117 | | |
118 | 118 | | |
119 | | - | |
| 119 | + | |
120 | 120 | | |
121 | 121 | | |
122 | | - | |
| 122 | + | |
123 | 123 | | |
124 | 124 | | |
125 | | - | |
| 125 | + | |
126 | 126 | | |
127 | 127 | | |
128 | 128 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1231 | 1231 | | |
1232 | 1232 | | |
1233 | 1233 | | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
1234 | 1238 | | |
1235 | 1239 | | |
1236 | 1240 | | |
| |||
1239 | 1243 | | |
1240 | 1244 | | |
1241 | 1245 | | |
1242 | | - | |
| 1246 | + | |
| 1247 | + | |
1243 | 1248 | | |
1244 | 1249 | | |
1245 | 1250 | | |
1246 | 1251 | | |
1247 | | - | |
| 1252 | + | |
1248 | 1253 | | |
1249 | 1254 | | |
1250 | 1255 | | |
1251 | 1256 | | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
| 1275 | + | |
| 1276 | + | |
| 1277 | + | |
| 1278 | + | |
1252 | 1279 | | |
1253 | 1280 | | |
1254 | 1281 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
54 | 58 | | |
55 | 59 | | |
56 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
276 | 276 | | |
277 | 277 | | |
278 | 278 | | |
279 | | - | |
| 279 | + | |
280 | 280 | | |
281 | 281 | | |
282 | 282 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
| 22 | + | |
21 | 23 | | |
22 | 24 | | |
23 | 25 | | |
24 | 26 | | |
25 | 27 | | |
26 | 28 | | |
27 | 29 | | |
| 30 | + | |
| 31 | + | |
28 | 32 | | |
29 | 33 | | |
30 | 34 | | |
| |||
146 | 150 | | |
147 | 151 | | |
148 | 152 | | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
149 | 199 | | |
150 | 200 | | |
151 | 201 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
| 198 | + | |
198 | 199 | | |
199 | 200 | | |
200 | 201 | | |
| |||
314 | 315 | | |
315 | 316 | | |
316 | 317 | | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
317 | 334 | | |
318 | 335 | | |
319 | 336 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
127 | 128 | | |
128 | 129 | | |
129 | 130 | | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
130 | 134 | | |
131 | 135 | | |
132 | 136 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
90 | 93 | | |
91 | 94 | | |
92 | 95 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | | - | |
| 161 | + | |
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
| |||
0 commit comments