Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3,071 changes: 2,139 additions & 932 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion components-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ libdd-telemetry-ffi = { path = "../libdatadog/libdd-telemetry-ffi", default-feat
datadog-live-debugger = { path = "../libdatadog/datadog-live-debugger" }
datadog-live-debugger-ffi = { path = "../libdatadog/datadog-live-debugger-ffi", default-features = false }
datadog-ipc = { path = "../libdatadog/datadog-ipc" }
datadog-remote-config = { path = "../libdatadog/datadog-remote-config" }
datadog-remote-config = { path = "../libdatadog/datadog-remote-config", features = ["ffe"] }
datadog-sidecar = { path = "../libdatadog/datadog-sidecar" }
datadog-sidecar-ffi = { path = "../libdatadog/datadog-sidecar-ffi" }
libdd-tinybytes = { path = "../libdatadog/libdd-tinybytes" }
libdd-trace-utils = { path = "../libdatadog/libdd-trace-utils" }
libdd-crashtracker-ffi = { path = "../libdatadog/libdd-crashtracker-ffi", default-features = false, features = ["collector"] }
libdd-library-config-ffi = { path = "../libdatadog/libdd-library-config-ffi", default-features = false }
spawn_worker = { path = "../libdatadog/spawn_worker" }
datadog-ffe = { path = "../libdatadog/datadog-ffe" }
anyhow = { version = "1.0" }
const-str = "0.5.6"
itertools = "0.11.0"
Expand Down
34 changes: 33 additions & 1 deletion components-rs/ddtrace.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,46 @@ uint32_t ddog_get_logs_count(ddog_CharSlice level);

void ddog_init_remote_config(bool live_debugging_enabled,
bool appsec_activation,
bool appsec_config);
bool appsec_config,
bool ffe_enabled);

struct ddog_RemoteConfigState *ddog_init_remote_config_state(const struct ddog_Endpoint *endpoint);

const char *ddog_remote_config_get_path(const struct ddog_RemoteConfigState *remote_config);

bool ddog_process_remote_configs(struct ddog_RemoteConfigState *remote_config);

bool ddog_ffe_load_config(const char *json);

bool ddog_ffe_has_config(void);

bool ddog_ffe_config_changed(void);

struct FfeResult;

struct FfeAttribute {
const char *key;
int32_t value_type; /* 0=string, 1=number, 2=bool */
const char *string_value;
double number_value;
bool bool_value;
};

struct FfeResult *ddog_ffe_evaluate(
const char *flag_key,
int32_t expected_type,
const char *targeting_key,
const struct FfeAttribute *attributes,
size_t attributes_count);

const char *ddog_ffe_result_value(const struct FfeResult *r);
const char *ddog_ffe_result_variant(const struct FfeResult *r);
const char *ddog_ffe_result_allocation_key(const struct FfeResult *r);
int32_t ddog_ffe_result_reason(const struct FfeResult *r);
int32_t ddog_ffe_result_error_code(const struct FfeResult *r);
bool ddog_ffe_result_do_log(const struct FfeResult *r);
void ddog_ffe_free_result(struct FfeResult *r);

bool ddog_type_can_be_instrumented(const struct ddog_RemoteConfigState *remote_config,
ddog_CharSlice typename_);

Expand Down
304 changes: 304 additions & 0 deletions components-rs/ffe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
use datadog_ffe::rules_based::{
self as ffe, AssignmentReason, AssignmentValue, Attribute, Configuration, EvaluationContext,
EvaluationError, ExpectedFlagType, Str, UniversalFlagConfig,
};
use std::collections::HashMap;
use std::ffi::{c_char, CStr, CString};
use std::sync::{Arc, Mutex};

/// Holds both the FFE configuration and a "changed" flag atomically behind a
/// single Mutex. This avoids the race where another thread could observe
/// `config` updated but `changed` still false (or vice-versa).
///
/// A `RwLock` would be more appropriate here (many readers via `ddog_ffe_evaluate`,
/// rare writer via `store_config`), but PHP is single-threaded per process so
/// contention is not a practical concern. Keeping a Mutex for simplicity.
struct FfeState {
config: Option<Configuration>,
changed: bool,
}

lazy_static::lazy_static! {
static ref FFE_STATE: Mutex<FfeState> = Mutex::new(FfeState {
config: None,
changed: false,
});
}

/// Called by remote_config when a new FFE configuration arrives via RC.
pub fn store_config(config: Configuration) {
if let Ok(mut state) = FFE_STATE.lock() {
state.config = Some(config);
state.changed = true;
}
}

/// Called by remote_config when an FFE configuration is removed.
pub fn clear_config() {
if let Ok(mut state) = FFE_STATE.lock() {
state.config = None;
state.changed = true;
}
}

/// Load a UFC JSON config string directly into the FFE engine.
/// Used by tests to load config without Remote Config.
#[no_mangle]
pub extern "C" fn ddog_ffe_load_config(json: *const c_char) -> bool {
if json.is_null() {
return false;
}
let json_str = match unsafe { CStr::from_ptr(json) }.to_str() {
Ok(s) => s,
Err(_) => return false,
};
match UniversalFlagConfig::from_json(json_str.as_bytes().to_vec()) {
Ok(ufc) => {
store_config(Configuration::from_server_response(ufc));
true
}
Err(_) => false,
}
}

/// Check if FFE configuration is loaded.
#[no_mangle]
pub extern "C" fn ddog_ffe_has_config() -> bool {
FFE_STATE.lock().map(|s| s.config.is_some()).unwrap_or(false)
}

/// Check if FFE config has changed since last check.
/// Resets the changed flag after reading.
#[no_mangle]
pub extern "C" fn ddog_ffe_config_changed() -> bool {
if let Ok(mut state) = FFE_STATE.lock() {
let was_changed = state.changed;
state.changed = false;
was_changed
} else {
false
}
}

/// Opaque handle for FFE evaluation results returned to C/PHP.
pub struct FfeResult {
pub value_json: CString,
pub variant: Option<CString>,
pub allocation_key: Option<CString>,
pub reason: i32,
pub error_code: i32,
pub do_log: bool,
}

/// A single attribute passed from C/PHP for building an EvaluationContext.
#[repr(C)]
pub struct FfeAttribute {
pub key: *const c_char,
/// 0 = string, 1 = number, 2 = bool
pub value_type: i32,
pub string_value: *const c_char,
pub number_value: f64,
pub bool_value: bool,
}

/// Evaluate a feature flag using the stored Configuration.
///
/// Accepts structured attributes from C instead of a JSON blob.
/// `targeting_key` may be null (no targeting key).
/// `attributes` / `attributes_count` describe an array of `FfeAttribute`.
/// Returns null if no config is loaded.
#[no_mangle]
pub extern "C" fn ddog_ffe_evaluate(
flag_key: *const c_char,
expected_type: i32,
targeting_key: *const c_char,
attributes: *const FfeAttribute,
attributes_count: usize,
) -> *mut FfeResult {
let flag_key = match unsafe { CStr::from_ptr(flag_key) }.to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};

let expected_type = match expected_type {
0 => ExpectedFlagType::String,
1 => ExpectedFlagType::Integer,
2 => ExpectedFlagType::Float,
3 => ExpectedFlagType::Boolean,
4 => ExpectedFlagType::Object,
_ => return std::ptr::null_mut(),
};

// Build targeting key
let tk = if targeting_key.is_null() {
None
} else {
match unsafe { CStr::from_ptr(targeting_key) }.to_str() {
Ok(s) if !s.is_empty() => Some(Str::from(s)),
_ => None,
}
};

// Build attributes map from the C array
let mut attrs = HashMap::new();
if !attributes.is_null() && attributes_count > 0 {
let slice = unsafe { std::slice::from_raw_parts(attributes, attributes_count) };
for attr in slice {
if attr.key.is_null() {
continue;
}
let key = match unsafe { CStr::from_ptr(attr.key) }.to_str() {
Ok(s) => s,
Err(_) => continue,
};
let value = match attr.value_type {
0 => {
// string
if attr.string_value.is_null() {
continue;
}
match unsafe { CStr::from_ptr(attr.string_value) }.to_str() {
Ok(s) => Attribute::from(s),
Err(_) => continue,
}
}
1 => {
// number
Attribute::from(attr.number_value)
}
2 => {
// bool
Attribute::from(attr.bool_value)
}
_ => continue,
};
attrs.insert(Str::from(key), value);
}
}

let context = EvaluationContext::new(tk, Arc::new(attrs));

let state = match FFE_STATE.lock() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
};

let assignment = ffe::get_assignment(
state.config.as_ref(),
flag_key,
&context,
expected_type,
ffe::now(),
);

let result = match assignment {
Ok(a) => FfeResult {
value_json: CString::new(assignment_value_to_json(&a.value)).unwrap_or_default(),
variant: Some(CString::new(a.variation_key.as_str()).unwrap_or_default()),
allocation_key: Some(CString::new(a.allocation_key.as_str()).unwrap_or_default()),
reason: match a.reason {
AssignmentReason::Static => 0,
AssignmentReason::TargetingMatch => 2,
AssignmentReason::Split => 3,
},
error_code: 0,
do_log: a.do_log,
},
Err(err) => {
let (error_code, reason) = match &err {
EvaluationError::TypeMismatch { .. } => (1, 5),
EvaluationError::ConfigurationParseError => (2, 5),
EvaluationError::ConfigurationMissing => (6, 5),
EvaluationError::FlagUnrecognizedOrDisabled => (3, 1),
EvaluationError::FlagDisabled => (0, 4),
EvaluationError::DefaultAllocationNull => (0, 1),
_ => (7, 5),
};
FfeResult {
value_json: CString::new("null").unwrap_or_default(),
variant: None,
allocation_key: None,
reason,
error_code,
do_log: false,
}
}
};

Box::into_raw(Box::new(result))
}

#[no_mangle]
pub extern "C" fn ddog_ffe_result_value(r: *const FfeResult) -> *const c_char {
if r.is_null() {
return std::ptr::null();
}
unsafe { &*r }.value_json.as_ptr()
}

#[no_mangle]
pub extern "C" fn ddog_ffe_result_variant(r: *const FfeResult) -> *const c_char {
if r.is_null() {
return std::ptr::null();
}
unsafe { &*r }
.variant
.as_ref()
.map(|s| s.as_ptr())
.unwrap_or(std::ptr::null())
}

#[no_mangle]
pub extern "C" fn ddog_ffe_result_allocation_key(r: *const FfeResult) -> *const c_char {
if r.is_null() {
return std::ptr::null();
}
unsafe { &*r }
.allocation_key
.as_ref()
.map(|s| s.as_ptr())
.unwrap_or(std::ptr::null())
}

#[no_mangle]
pub extern "C" fn ddog_ffe_result_reason(r: *const FfeResult) -> i32 {
if r.is_null() {
return -1;
}
unsafe { &*r }.reason
}

#[no_mangle]
pub extern "C" fn ddog_ffe_result_error_code(r: *const FfeResult) -> i32 {
if r.is_null() {
return -1;
}
unsafe { &*r }.error_code
}

#[no_mangle]
pub extern "C" fn ddog_ffe_result_do_log(r: *const FfeResult) -> bool {
if r.is_null() {
return false;
}
unsafe { &*r }.do_log
}

#[no_mangle]
pub unsafe extern "C" fn ddog_ffe_free_result(r: *mut FfeResult) {
if !r.is_null() {
drop(Box::from_raw(r));
}
}

fn assignment_value_to_json(value: &AssignmentValue) -> String {
match value {
AssignmentValue::String(s) => serde_json::to_string(s.as_str()).unwrap_or_default(),
AssignmentValue::Integer(i) => i.to_string(),
AssignmentValue::Float(f) => serde_json::Number::from_f64(*f)
.map(|n| n.to_string())
.unwrap_or_else(|| f.to_string()),
AssignmentValue::Boolean(b) => b.to_string(),
AssignmentValue::Json { raw, .. } => raw.get().to_string(),
}
}
1 change: 1 addition & 0 deletions components-rs/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

pub mod log;
pub mod remote_config;
pub mod ffe;
pub mod sidecar;
pub mod telemetry;
pub mod bytes;
Expand Down
Loading
Loading