Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2674769
feat(sidecar): forward FFE exposures to agent EVP proxy
leoromanovsky Apr 23, 2026
1d8e284
chore(sidecar): keep FFE flusher comments ASCII
leoromanovsky May 22, 2026
3ac1d75
fix(sidecar): use current HTTP capability API for FFE
leoromanovsky May 22, 2026
6ac16b3
fix(sidecar): avoid test-only HTTP client import warning
leoromanovsky May 22, 2026
a1afaae
chore(sidecar): add FFE flusher codeowners
leoromanovsky May 22, 2026
a649b7f
test(ffe): skip EVP mock server tests under miri
leoromanovsky May 22, 2026
2976233
feat(sidecar): forward FFE evaluation metrics to OTLP HTTP intake
leoromanovsky May 23, 2026
875ec8f
fix(sidecar): dispatch FFE actions before application-entry check
leoromanovsky May 23, 2026
e7beb1c
chore(sidecar): rename ffe_flusher → ffe_exposures_flusher
leoromanovsky May 23, 2026
6650cc2
chore(sidecar): rustfmt after ffe_flusher → ffe_exposures_flusher rename
leoromanovsky May 23, 2026
2389cab
docs(sidecar): add FFE forwarder system diagram for PR 2026
leoromanovsky May 23, 2026
97c57e5
test(sidecar): cover FFE dispatch before app registration
leoromanovsky May 24, 2026
a16e7cc
chore(ffe): remove generated sidecar docs
leoromanovsky May 24, 2026
aa12a2d
fix(sidecar): reuse FFE HTTP client and enforce timeouts
leoromanovsky May 27, 2026
11916e3
fix(sidecar): satisfy FFE flusher clippy
leoromanovsky May 27, 2026
717cf7b
fix(sidecar): address FFE forwarding review
leoromanovsky May 27, 2026
45174a1
feat(sidecar): handle structured FFE telemetry
leoromanovsky May 27, 2026
c8577c0
Merge remote-tracking branch 'origin/main' into leo.romanovsky/ffe-si…
leoromanovsky May 28, 2026
a7726a9
fix(profiling): avoid newer integer helper in profiler
leoromanovsky May 28, 2026
f8a8ea9
fix(profiling): allow modulo compatibility lint
leoromanovsky May 28, 2026
6ed96c2
fix(obfuscation): avoid const String bytes for older toolchains
leoromanovsky May 28, 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
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,5 @@ tools/sidecar_mockgen/ @DataDog/libdatadog-php
libdd-data-pipeline/src/otlp/ @DataDog/apm-sdk-capabilities-rust
libdd-data-pipeline/tests/test_trace_exporter_otlp_export.rs @DataDog/apm-sdk-capabilities-rust
libdd-trace-utils/src/otlp_encoder/ @DataDog/apm-sdk-capabilities-rust
datadog-sidecar/src/service/ffe_exposures_flusher.rs @DataDog/libdatadog-php @DataDog/libdatadog-apm @DataDog/feature-flagging-and-experimentation-sdk
datadog-sidecar/src/service/ffe_metrics_flusher.rs @DataDog/libdatadog-php @DataDog/libdatadog-apm @DataDog/feature-flagging-and-experimentation-sdk
2 changes: 2 additions & 0 deletions Cargo.lock

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

170 changes: 168 additions & 2 deletions datadog-sidecar-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@ use datadog_sidecar::service::agent_info::AgentInfoReader;
use datadog_sidecar::service::telemetry::InternalTelemetryAction;
use datadog_sidecar::service::{
blocking::{self, SidecarTransport},
DynamicInstrumentationConfigState, InstanceId, QueueId, RuntimeMetadata,
DynamicInstrumentationConfigState, FfeEvaluationMetric as SidecarFfeEvaluationMetric,
FfeExposure as SidecarFfeExposure, FfeExposureBatch as SidecarFfeExposureBatch,
FfeTelemetryContext as SidecarFfeTelemetryContext, InstanceId, QueueId, RuntimeMetadata,
SerializedTracerHeaderTags, SessionConfig, SidecarAction, SidecarFlushOptions,
};
use datadog_sidecar::service::{get_telemetry_action_sender, InternalTelemetryActions};
use datadog_sidecar::shm_remote_config::{path_for_remote_config, RemoteConfigReader};
use libc::c_char;
use libdd_common::tag::Tag;
use libdd_common::Endpoint;
use libdd_common_ffi::slice::{AsBytes, CharSlice};
use libdd_common_ffi::slice::{AsBytes, CharSlice, Slice};
use libdd_common_ffi::{self as ffi, MaybeError};
#[cfg(windows)]
use libdd_crashtracker_ffi::Metadata;
Expand Down Expand Up @@ -1116,6 +1118,170 @@ pub unsafe extern "C" fn ddog_sidecar_send_debugger_datum(
ddog_sidecar_send_debugger_data(transport, instance_id, queue_id, vec![*payload])
}

#[repr(C)]
pub struct FfeTelemetryContext<'a> {
pub service: CharSlice<'a>,
pub env: CharSlice<'a>,
pub version: CharSlice<'a>,
}

#[repr(C)]
pub struct FfeExposure<'a> {
pub timestamp_ms: u64,
pub flag_key: CharSlice<'a>,
pub subject_id: CharSlice<'a>,
/// UTF-8 JSON object. Empty, invalid, or non-object JSON is serialized as
/// an empty subject attribute object.
pub subject_attributes_json: CharSlice<'a>,
pub allocation_key: CharSlice<'a>,
pub variant: CharSlice<'a>,
}

#[repr(C)]
pub struct FfeEvaluationMetric<'a> {
pub flag_key: CharSlice<'a>,
pub variant: CharSlice<'a>,
pub reason: CharSlice<'a>,
pub error_type: CharSlice<'a>,
pub allocation_key: CharSlice<'a>,
}

/// 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.
#[no_mangle]
#[allow(clippy::missing_safety_doc)]
pub unsafe extern "C" fn ddog_sidecar_send_ffe_exposure_batch(
transport: &mut Box<SidecarTransport>,
instance_id: &InstanceId,
queue_id: &QueueId,
context: &FfeTelemetryContext<'_>,
exposures: Slice<FfeExposure<'_>>,
) -> MaybeError {
if exposures.is_empty() {
return MaybeError::None;
}

let context = try_c!(ffe_context_from_ffi(context));
let exposures = try_c!(exposures
.try_as_slice()
.map_err(|e| format!("Invalid exposure slice: {e}"))
.and_then(|exposures| exposures
.iter()
.map(ffe_exposure_from_ffi)
.collect::<Result<Vec<_>, _>>()));

if exposures.is_empty() {
return MaybeError::None;
}

try_c!(blocking::enqueue_actions(
Comment thread
leoromanovsky marked this conversation as resolved.
transport,
instance_id,
queue_id,
vec![SidecarAction::FfeExposureBatch(SidecarFfeExposureBatch {
context,
exposures,
})],
));
MaybeError::None
}

/// 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.
#[no_mangle]
#[allow(clippy::missing_safety_doc)]
pub unsafe extern "C" fn ddog_sidecar_send_ffe_evaluation_metrics(
transport: &mut Box<SidecarTransport>,
instance_id: &InstanceId,
queue_id: &QueueId,
endpoint: CharSlice,
context: &FfeTelemetryContext<'_>,
metrics: Slice<FfeEvaluationMetric<'_>>,
) -> MaybeError {
if endpoint.is_empty() || metrics.is_empty() {
return MaybeError::None;
}

let endpoint = try_c!(char_slice_to_string(endpoint));
let context = try_c!(ffe_context_from_ffi(context));
let metrics = try_c!(metrics
.try_as_slice()
.map_err(|e| format!("Invalid metric slice: {e}"))
.and_then(|metrics| metrics
.iter()
.map(ffe_metric_from_ffi)
.collect::<Result<Vec<_>, _>>()));

if metrics.is_empty() {
return MaybeError::None;
}

try_c!(blocking::enqueue_actions(
transport,
instance_id,
queue_id,
vec![SidecarAction::FfeEvaluationMetrics {
endpoint,
context,
metrics,
}],
));
MaybeError::None
}

fn ffe_context_from_ffi(
context: &FfeTelemetryContext<'_>,
) -> Result<SidecarFfeTelemetryContext, String> {
Ok(SidecarFfeTelemetryContext {
service: char_slice_to_string(context.service)?,
env: char_slice_to_string(context.env)?,
version: char_slice_to_string(context.version)?,
})
}

fn ffe_exposure_from_ffi(exposure: &FfeExposure<'_>) -> Result<SidecarFfeExposure, String> {
Ok(SidecarFfeExposure {
timestamp_ms: exposure.timestamp_ms,
flag_key: char_slice_to_string(exposure.flag_key)?,
subject_id: char_slice_to_string(exposure.subject_id)?,
subject_attributes_json: char_slice_to_string(exposure.subject_attributes_json)?,
allocation_key: char_slice_to_string(exposure.allocation_key)?,
variant: char_slice_to_string(exposure.variant)?,
})
}

fn ffe_metric_from_ffi(
metric: &FfeEvaluationMetric<'_>,
) -> Result<SidecarFfeEvaluationMetric, String> {
Ok(SidecarFfeEvaluationMetric {
flag_key: char_slice_to_string(metric.flag_key)?,
variant: char_slice_to_string(metric.variant)?,
reason: char_slice_to_string(metric.reason)?,
error_type: optional_string(metric.error_type)?,
allocation_key: optional_string(metric.allocation_key)?,
})
}

fn optional_string(slice: CharSlice) -> Result<Option<String>, String> {
if slice.is_empty() {
Ok(None)
} else {
char_slice_to_string(slice).map(Some)
}
}

#[no_mangle]
#[allow(clippy::missing_safety_doc)]
#[allow(improper_ctypes_definitions)] // DebuggerPayload is just a pointer, we hide its internals
Expand Down
2 changes: 2 additions & 0 deletions datadog-sidecar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ serde = { version = "1.0", features = ["derive", "rc"] }
serde_with = "3.6.0"
bincode = { version = "1.3.3" }
serde_json = "1.0"
lru = "0.16.3"
prost = "0.14.1"
base64 = "0.22.1"
spawn_worker = { path = "../spawn_worker" }
zwohash = "0.1.2"
Expand Down
Loading
Loading