Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
16d7817
Add FFE evaluation completion hook
leoromanovsky May 22, 2026
eba4c86
Add FFE exposure writer
leoromanovsky May 22, 2026
fc69e9f
Load evaluation-completed hook classes in canonical FFE PHPT
leoromanovsky May 23, 2026
92ef9a3
Load evaluation-completed hook classes in canonical FFE PHPT
leoromanovsky May 23, 2026
49d53ef
docs(ffe): add PR-stack and system diagrams for PR #3909
leoromanovsky May 23, 2026
81c01a7
Migrate FFE exposure transport to libdatadog sidecar
leoromanovsky May 23, 2026
0c4b4b3
Update libdatadog submodule to pick up FFE dispatch fix
leoromanovsky May 23, 2026
64c4267
tooling: place mktemp dirs under OUTPUT_DIR to avoid silent no-op on …
leoromanovsky May 23, 2026
ea04875
docs(ffe): quote diagram titles, switch system to TD, re-render at hi…
leoromanovsky May 23, 2026
f319d47
docs(ffe): quote diagram titles, switch system to TD, re-render at hi…
leoromanovsky May 23, 2026
13aca28
docs(ffe): drop 'Hook seam' wording, use 'Hook layer'
leoromanovsky May 23, 2026
28782fb
ExposureWriter: surface first drop with a one-time warning + TODO
leoromanovsky May 24, 2026
f775277
ExposureWriter: flush-on-full so long-running runtimes don't silently…
leoromanovsky May 24, 2026
8eedb0e
chore(ffe): remove generated stack diagrams
leoromanovsky May 24, 2026
04adf69
chore(ffe): remove generated stack diagrams
leoromanovsky May 24, 2026
e9b713c
Merge branch 'leo.romanovsky/milestone-1-runtime-evaluation' into leo…
leoromanovsky May 26, 2026
f4a1546
Merge branch 'leo.romanovsky/m2-m3-evaluation-completed-base' into le…
leoromanovsky May 26, 2026
ec49560
Merge branch 'leo.romanovsky/milestone-1-runtime-evaluation' into leo…
leoromanovsky May 27, 2026
61f6cc1
Merge branch 'leo.romanovsky/m2-m3-evaluation-completed-base' into le…
leoromanovsky May 27, 2026
76118cb
fix(ffe): tidy native exposure branch base
leoromanovsky May 27, 2026
b28e705
Merge milestone 1 runtime evaluation base
leoromanovsky May 27, 2026
c4bceb1
chore(ffe): update libdatadog sidecar dependency
leoromanovsky May 27, 2026
e82793e
chore(ffe): update libdatadog sidecar dependency
leoromanovsky May 28, 2026
5e38adc
chore(ffe): update libdatadog sidecar dependency
leoromanovsky May 28, 2026
c8cb8f5
chore(ffe): update libdatadog sidecar dependency
leoromanovsky May 28, 2026
a30c3ef
chore(ffe): update libdatadog sidecar dependency
leoromanovsky May 28, 2026
59d0ea1
Fix FFE exposure sidecar activation
leoromanovsky May 28, 2026
59be76d
Update FFE exposure dependency and result ABI
leoromanovsky May 28, 2026
416749d
Bump libdatadog exposure runtime
leoromanovsky May 28, 2026
94de19a
Merge remote-tracking branch 'origin/master' into leo.romanovsky/m2-f…
leoromanovsky Jun 2, 2026
11cc8fa
Remove unreachable FFE exposure cleanup branch
leoromanovsky Jun 2, 2026
643272b
Drop redundant FFE exposure release guards
leoromanovsky Jun 2, 2026
e4d254e
Pass FFE exposures as zend strings
leoromanovsky Jun 2, 2026
31ccc9f
Document FFE exposure CLI flush timing
leoromanovsky Jun 2, 2026
436f80e
Move FFE exposure flush helper to testing namespace
leoromanovsky Jun 2, 2026
4557494
Merge remote-tracking branch 'origin/master' into leo.romanovsky/m2-f…
leoromanovsky Jun 2, 2026
1cb2a0b
Merge remote-tracking branch 'origin/master' into leo.romanovsky/m2-f…
leoromanovsky Jun 2, 2026
203281e
Move FFE exposure buffering into tracer
leoromanovsky Jun 2, 2026
bcf1342
Fully move FFE to tracer/
bwoebi Jun 2, 2026
bdaca4f
Fix compile error
bwoebi Jun 2, 2026
00564ae
Remove FFE evaluation RC polling
leoromanovsky Jun 2, 2026
a802e10
Proper empty string
bwoebi Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions components-rs/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,61 @@ typedef struct ddog_TracerHeaderTags {
bool client_computed_stats;
} ddog_TracerHeaderTags;

typedef struct ddog_FfeTelemetryContext {
ddog_CharSlice service;
ddog_CharSlice env;
ddog_CharSlice version;
} ddog_FfeTelemetryContext;

typedef struct ddog_FfeExposure {
uint64_t timestamp_ms;
ddog_CharSlice flag_key;
ddog_CharSlice subject_id;
/**
* UTF-8 JSON object. Empty, invalid, or non-object JSON is serialized as
* an empty subject attribute object.
*/
ddog_CharSlice subject_attributes_json;
ddog_CharSlice allocation_key;
ddog_CharSlice variant;
} ddog_FfeExposure;

typedef struct ddog_Slice_FfeExposure {
/**
* Should be non-null and suitably aligned for the underlying type. It is
* allowed but not recommended for the pointer to be null when the len is
* zero.
*/
const struct ddog_FfeExposure *ptr;
/**
* The number of elements (not bytes) that `.ptr` points to. Must be less
* than or equal to [isize::MAX].
*/
uintptr_t len;
} ddog_Slice_FfeExposure;

typedef struct ddog_FfeEvaluationMetric {
ddog_CharSlice flag_key;
ddog_CharSlice variant;
ddog_CharSlice reason;
ddog_CharSlice error_type;
ddog_CharSlice allocation_key;
} ddog_FfeEvaluationMetric;

typedef struct ddog_Slice_FfeEvaluationMetric {
/**
* Should be non-null and suitably aligned for the underlying type. It is
* allowed but not recommended for the pointer to be null when the len is
* zero.
*/
const struct ddog_FfeEvaluationMetric *ptr;
/**
* The number of elements (not bytes) that `.ptr` points to. Must be less
* than or equal to [isize::MAX].
*/
uintptr_t len;
} ddog_Slice_FfeEvaluationMetric;

/**
* Holds the raw parts of a Rust Vec; it should only be created from Rust,
* never from C.
Expand Down
33 changes: 33 additions & 0 deletions components-rs/sidecar.h
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,39 @@ ddog_MaybeError ddog_sidecar_send_debugger_datum(struct ddog_SidecarTransport **
ddog_QueueId queue_id,
struct ddog_DebuggerPayload *payload);

/**
* Send structured FFE exposure events to the sidecar. The sidecar owns
* deduplication, JSON serialization, and Agent EVP delivery. This function is
* caller-driven; shared libdatadog evaluator calls do not log unless an SDK
* explicitly sends this action.
*
* # Safety
* `context` and every element in `exposures` must contain valid UTF-8
* `CharSlice` values. Empty `exposures` is a no-op.
*/
ddog_MaybeError ddog_sidecar_send_ffe_exposure_batch(struct ddog_SidecarTransport **transport,
const struct ddog_InstanceId *instance_id,
const ddog_QueueId *queue_id,
const struct ddog_FfeTelemetryContext *context,
struct ddog_Slice_FfeExposure exposures);

/**
* Send structured FFE evaluation metric events to the sidecar. The sidecar
* owns aggregation, OTLP/protobuf serialization, and OTLP HTTP delivery. This
* function is caller-driven so SDKs with existing host-language hooks can
* safely coexist until they explicitly migrate.
*
* # Safety
* `endpoint`, `context`, and every element in `metrics` must contain valid
* UTF-8 `CharSlice` values. Empty `endpoint` or `metrics` is a no-op.
*/
ddog_MaybeError ddog_sidecar_send_ffe_evaluation_metrics(struct ddog_SidecarTransport **transport,
const struct ddog_InstanceId *instance_id,
const ddog_QueueId *queue_id,
ddog_CharSlice endpoint,
const struct ddog_FfeTelemetryContext *context,
struct ddog_Slice_FfeEvaluationMetric metrics);

ddog_MaybeError ddog_sidecar_send_debugger_diagnostics(struct ddog_SidecarTransport **transport,
const struct ddog_InstanceId *instance_id,
ddog_QueueId queue_id,
Expand Down
49 changes: 49 additions & 0 deletions ext/ddtrace.c
Original file line number Diff line number Diff line change
Expand Up @@ -1932,6 +1932,8 @@ static PHP_RSHUTDOWN_FUNCTION(ddtrace) {
ddtrace_rshutdown_remote_config();
}

ddtrace_ffe_flush_exposures();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is known and intended that for long-running CLI scripts, exposures will only be flushed at the request end, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is intentional for this PR. The exposure buffer is request-local native memory and is flushed at RSHUTDOWN. For PHP-FPM, it flushes once per request; for CLI, the script lifetime is the request lifetime.

The tradeoff is intentional: evaluation stays I/O-free, and the sidecar receives/deduplicates exposures only at flush time. The caveat is long-running CLI scripts: exposures are delayed until shutdown, and the current in-process buffer is capped at 1000 entries. If we decide long-running CLI needs better behavior, I would handle that as a follow-up with an explicit periodic/threshold flush path rather than adding sidecar I/O to evaluation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intuitive approach would be a (fixed size) ring buffer via shared memory (a few pages of shm , which the sidecar polls (e.g. 1x/sec). This would also naturally limit the amount of exposures per second, which the sidecar has to process (hard limit on overhead esssentially).


if (!ddtrace_disable) {
ddtrace_autoload_rshutdown();

Expand Down Expand Up @@ -2769,6 +2771,12 @@ PHP_FUNCTION(DDTrace_Internal_handle_fork) {
dd_internal_handle_fork();
}

PHP_FUNCTION(DDTrace_Internal_flush_ffe_exposures) {
ZEND_PARSE_PARAMETERS_NONE();

RETURN_BOOL(ddtrace_ffe_flush_exposures());
}

PHP_FUNCTION(DDTrace_dogstatsd_count) {
zend_string *metric;
zend_long value;
Expand Down Expand Up @@ -2975,6 +2983,16 @@ PHP_FUNCTION(DDTrace_Testing_ffe_load_config) {
RETURN_BOOL(ddog_ffe_load_config(dd_zend_string_to_CharSlice(json)));
}

static zend_string *ddtrace_ffe_attributes_json(zval *attrs_zv) {
smart_str buf = {0};
zai_json_encode(&buf, attrs_zv, 0);
if (!buf.s) {
return zend_string_init("{}", sizeof("{}") - 1, 0);
}
smart_str_0(&buf);
return smart_str_extract(&buf);
}

static void ddtrace_ffe_update_property(zval *object, const char *name, size_t name_len, zval *value) {
zend_string *property_name = zend_string_init(name, name_len, 0);
zend_update_property_ex(ddtrace_ce_ffe_result, Z_OBJ_P(object), property_name, value);
Expand Down Expand Up @@ -3017,6 +3035,22 @@ static void ddtrace_ffe_update_empty_array_property(zval *object, const char *na
zval_ptr_dtor(&property_value);
}

static void ddtrace_ffe_refresh_remote_config(void) {
if (!DDTRACE_G(remote_config_state)) {
return;
}

if (DDTRACE_G(reread_remote_configuration)) {
DDTRACE_G(reread_remote_configuration) = 0;
ddog_process_remote_configs(DDTRACE_G(remote_config_state));
return;
}

if (!ddog_ffe_has_config()) {
ddtrace_check_for_new_config_now();
}
}

PHP_FUNCTION(DDTrace_ffe_evaluate) {
zend_string *flag_key;
zend_long type_id_zl;
Expand All @@ -3041,6 +3075,7 @@ PHP_FUNCTION(DDTrace_ffe_evaluate) {
ZEND_PARSE_PARAMETERS_END();

type_id = (int32_t) type_id_zl;
ddtrace_ffe_refresh_remote_config();
attributes = Z_ARRVAL_P(attrs_zv);
attrs_count = zend_hash_num_elements(attributes);

Expand Down Expand Up @@ -3117,6 +3152,20 @@ PHP_FUNCTION(DDTrace_ffe_evaluate) {
RETURN_NULL();
}

if (result.do_log && result.allocation_key && result.variant) {
zend_string *subject_attributes_json = ddtrace_ffe_attributes_json(attrs_zv);
ddtrace_ffe_record_exposure(
ZSTR_VAL(flag_key),
ZSTR_LEN(flag_key),
targeting_key ? ZSTR_VAL(targeting_key) : NULL,
targeting_key ? ZSTR_LEN(targeting_key) : 0,
subject_attributes_json,
ZSTR_VAL(result.allocation_key),
ZSTR_VAL(result.variant)
);
zend_string_release(subject_attributes_json);
}

object_init_ex(return_value, ddtrace_ce_ffe_result);
ddtrace_ffe_update_nullable_string_property(return_value, ZEND_STRL("valueJson"), result.value_json);
ddtrace_ffe_update_nullable_string_property(return_value, ZEND_STRL("variant"), result.variant);
Expand Down
3 changes: 3 additions & 0 deletions ext/ddtrace.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ ZEND_BEGIN_MODULE_GLOBALS(ddtrace)
ddog_SidecarTransport *sidecar;
ddog_QueueId sidecar_queue_id;
MUTEX_T sidecar_universal_service_tags_mutex;
void *ffe_exposure_buffer;
size_t ffe_exposure_buffer_len;
size_t ffe_exposure_buffer_cap;
ddog_AgentRemoteConfigReader *agent_config_reader;
ddog_RemoteConfigState *remote_config_state;
bool remote_config_writing; // true while RC WRITE mode INI update is in progress
Expand Down
7 changes: 7 additions & 0 deletions ext/ddtrace.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,13 @@ function add_span_flag(\DDTrace\SpanData $span, int $flag): void {}
*/
function handle_fork(): void {}

/**
* Flushes native FFE exposure batches for integration tests.
*
* @internal
*/
function flush_ffe_exposures(): bool {}
Comment thread
leoromanovsky marked this conversation as resolved.
Outdated

}

namespace datadog\appsec\v2 {
Expand Down
4 changes: 4 additions & 0 deletions ext/ddtrace_arginfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ ZEND_END_ARG_INFO()

#define arginfo_DDTrace_Internal_handle_fork arginfo_DDTrace_flush

#define arginfo_DDTrace_Internal_flush_ffe_exposures arginfo_DDTrace_are_endpoints_collected

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_datadog_appsec_v2_track_user_login_success, 0, 1, IS_VOID, 0)
ZEND_ARG_TYPE_INFO(0, login, IS_STRING, 0)
ZEND_ARG_TYPE_MASK(0, user, MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_NULL, "null")
Expand Down Expand Up @@ -428,6 +430,7 @@ ZEND_FUNCTION(DDTrace_Testing_emit_asm_event);
ZEND_FUNCTION(DDTrace_Testing_normalize_tag_value);
ZEND_FUNCTION(DDTrace_Internal_add_span_flag);
ZEND_FUNCTION(DDTrace_Internal_handle_fork);
ZEND_FUNCTION(DDTrace_Internal_flush_ffe_exposures);
ZEND_FUNCTION(datadog_appsec_v2_track_user_login_success);
ZEND_FUNCTION(datadog_appsec_v2_track_user_login_failure);
ZEND_FUNCTION(dd_trace_env_config);
Expand Down Expand Up @@ -527,6 +530,7 @@ static const zend_function_entry ext_functions[] = {
ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Testing", "normalize_tag_value"), zif_DDTrace_Testing_normalize_tag_value, arginfo_DDTrace_Testing_normalize_tag_value, 0, NULL, NULL)
ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "add_span_flag"), zif_DDTrace_Internal_add_span_flag, arginfo_DDTrace_Internal_add_span_flag, 0, NULL, NULL)
ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "handle_fork"), zif_DDTrace_Internal_handle_fork, arginfo_DDTrace_Internal_handle_fork, 0, NULL, NULL)
ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "flush_ffe_exposures"), zif_DDTrace_Internal_flush_ffe_exposures, arginfo_DDTrace_Internal_flush_ffe_exposures, 0, NULL, NULL)
ZEND_RAW_FENTRY(ZEND_NS_NAME("datadog\\appsec\\v2", "track_user_login_success"), zif_datadog_appsec_v2_track_user_login_success, arginfo_datadog_appsec_v2_track_user_login_success, 0, NULL, NULL)
ZEND_RAW_FENTRY(ZEND_NS_NAME("datadog\\appsec\\v2", "track_user_login_failure"), zif_datadog_appsec_v2_track_user_login_failure, arginfo_datadog_appsec_v2_track_user_login_failure, 0, NULL, NULL)
ZEND_FE(dd_trace_env_config, arginfo_dd_trace_env_config)
Expand Down
Loading
Loading