diff --git a/Cargo.lock b/Cargo.lock index 7a0fe64ab9e..89c9eb4388b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1388,6 +1388,7 @@ dependencies = [ "nix 0.29.0", "prctl", "priority-queue", + "prost", "rand 0.8.5", "rmp-serde", "sendfd", @@ -2870,7 +2871,7 @@ dependencies = [ [[package]] name = "libdd-data-pipeline" -version = "4.0.0" +version = "5.0.0" dependencies = [ "anyhow", "arc-swap", @@ -2934,7 +2935,7 @@ dependencies = [ [[package]] name = "libdd-library-config" -version = "1.1.0" +version = "2.0.0" dependencies = [ "anyhow", "libc 0.2.177", @@ -3127,7 +3128,7 @@ dependencies = [ [[package]] name = "libdd-trace-obfuscation" -version = "3.0.0" +version = "3.1.0" dependencies = [ "anyhow", "criterion", @@ -3158,7 +3159,7 @@ dependencies = [ [[package]] name = "libdd-trace-stats" -version = "3.0.0" +version = "4.0.0" dependencies = [ "anyhow", "arc-swap", @@ -3185,7 +3186,7 @@ dependencies = [ [[package]] name = "libdd-trace-utils" -version = "4.0.0" +version = "5.0.0" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/components-rs/common.h b/components-rs/common.h index a39a3bd0fbd..3a43d29ef69 100644 --- a/components-rs/common.h +++ b/components-rs/common.h @@ -276,19 +276,16 @@ typedef struct _zend_string _zend_string; #define ddog_MultiTargetFetcher_DEFAULT_CLIENTS_LIMIT 100 -typedef enum ddog_Log { - DDOG_LOG_ERROR = 1, - DDOG_LOG_WARN = 2, - DDOG_LOG_INFO = 3, - DDOG_LOG_DEBUG = 4, - DDOG_LOG_TRACE = 5, - DDOG_LOG_DEPRECATED = (3 | ddog_LOG_ONCE), - DDOG_LOG_STARTUP = (3 | (2 << 4)), - DDOG_LOG_STARTUP_WARN = (1 | (2 << 4)), - DDOG_LOG_SPAN = (4 | (3 << 4)), - DDOG_LOG_SPAN_TRACE = (5 | (3 << 4)), - DDOG_LOG_HOOK_TRACE = (5 | (4 << 4)), -} ddog_Log; +typedef enum ddog_ConfigurationOrigin { + DDOG_CONFIGURATION_ORIGIN_ENV_VAR, + DDOG_CONFIGURATION_ORIGIN_CODE, + DDOG_CONFIGURATION_ORIGIN_DD_CONFIG, + DDOG_CONFIGURATION_ORIGIN_REMOTE_CONFIG, + DDOG_CONFIGURATION_ORIGIN_DEFAULT, + DDOG_CONFIGURATION_ORIGIN_LOCAL_STABLE_CONFIG, + DDOG_CONFIGURATION_ORIGIN_FLEET_STABLE_CONFIG, + DDOG_CONFIGURATION_ORIGIN_CALCULATED, +} ddog_ConfigurationOrigin; typedef enum ddog_DynamicConfigUpdateMode { DDOG_DYNAMIC_CONFIG_UPDATE_MODE_READ, @@ -297,37 +294,30 @@ typedef enum ddog_DynamicConfigUpdateMode { DDOG_DYNAMIC_CONFIG_UPDATE_MODE_RESTORE, } ddog_DynamicConfigUpdateMode; -typedef enum ddog_InBodyLocation { - DDOG_IN_BODY_LOCATION_NONE, - DDOG_IN_BODY_LOCATION_START, - DDOG_IN_BODY_LOCATION_END, -} ddog_InBodyLocation; - typedef enum ddog_EvaluateAt { DDOG_EVALUATE_AT_ENTRY, DDOG_EVALUATE_AT_EXIT, } ddog_EvaluateAt; -typedef enum ddog_MetricKind { - DDOG_METRIC_KIND_COUNT, - DDOG_METRIC_KIND_GAUGE, - DDOG_METRIC_KIND_HISTOGRAM, - DDOG_METRIC_KIND_DISTRIBUTION, -} ddog_MetricKind; - -typedef enum ddog_SpanProbeTarget { - DDOG_SPAN_PROBE_TARGET_ACTIVE, - DDOG_SPAN_PROBE_TARGET_ROOT, -} ddog_SpanProbeTarget; +typedef enum ddog_InBodyLocation { + DDOG_IN_BODY_LOCATION_NONE, + DDOG_IN_BODY_LOCATION_START, + DDOG_IN_BODY_LOCATION_END, +} ddog_InBodyLocation; -typedef enum ddog_ProbeStatus { - DDOG_PROBE_STATUS_RECEIVED, - DDOG_PROBE_STATUS_INSTALLED, - DDOG_PROBE_STATUS_EMITTING, - DDOG_PROBE_STATUS_ERROR, - DDOG_PROBE_STATUS_BLOCKED, - DDOG_PROBE_STATUS_WARNING, -} ddog_ProbeStatus; +typedef enum ddog_Log { + DDOG_LOG_ERROR = 1, + DDOG_LOG_WARN = 2, + DDOG_LOG_INFO = 3, + DDOG_LOG_DEBUG = 4, + DDOG_LOG_TRACE = 5, + DDOG_LOG_DEPRECATED = (3 | ddog_LOG_ONCE), + DDOG_LOG_STARTUP = (3 | (2 << 4)), + DDOG_LOG_STARTUP_WARN = (1 | (2 << 4)), + DDOG_LOG_SPAN = (4 | (3 << 4)), + DDOG_LOG_SPAN_TRACE = (5 | (3 << 4)), + DDOG_LOG_HOOK_TRACE = (5 | (4 << 4)), +} ddog_Log; typedef enum ddog_Method { DDOG_METHOD_GET = 0, @@ -342,22 +332,12 @@ typedef enum ddog_Method { DDOG_METHOD_OTHER = 9, } ddog_Method; -typedef enum ddog_ConfigurationOrigin { - DDOG_CONFIGURATION_ORIGIN_ENV_VAR, - DDOG_CONFIGURATION_ORIGIN_CODE, - DDOG_CONFIGURATION_ORIGIN_DD_CONFIG, - DDOG_CONFIGURATION_ORIGIN_REMOTE_CONFIG, - DDOG_CONFIGURATION_ORIGIN_DEFAULT, - DDOG_CONFIGURATION_ORIGIN_LOCAL_STABLE_CONFIG, - DDOG_CONFIGURATION_ORIGIN_FLEET_STABLE_CONFIG, - DDOG_CONFIGURATION_ORIGIN_CALCULATED, -} ddog_ConfigurationOrigin; - -typedef enum ddog_MetricType { - DDOG_METRIC_TYPE_GAUGE, - DDOG_METRIC_TYPE_COUNT, - DDOG_METRIC_TYPE_DISTRIBUTION, -} ddog_MetricType; +typedef enum ddog_MetricKind { + DDOG_METRIC_KIND_COUNT, + DDOG_METRIC_KIND_GAUGE, + DDOG_METRIC_KIND_HISTOGRAM, + DDOG_METRIC_KIND_DISTRIBUTION, +} ddog_MetricKind; typedef enum ddog_MetricNamespace { DDOG_METRIC_NAMESPACE_TRACERS, @@ -373,17 +353,20 @@ typedef enum ddog_MetricNamespace { DDOG_METRIC_NAMESPACE_SIDECAR, } ddog_MetricNamespace; -typedef enum ddog_RemoteConfigProduct { - DDOG_REMOTE_CONFIG_PRODUCT_AGENT_CONFIG, - DDOG_REMOTE_CONFIG_PRODUCT_AGENT_TASK, - DDOG_REMOTE_CONFIG_PRODUCT_APM_TRACING, - DDOG_REMOTE_CONFIG_PRODUCT_ASM, - DDOG_REMOTE_CONFIG_PRODUCT_ASM_DATA, - DDOG_REMOTE_CONFIG_PRODUCT_ASM_DD, - DDOG_REMOTE_CONFIG_PRODUCT_ASM_FEATURES, - DDOG_REMOTE_CONFIG_PRODUCT_FFE_FLAGS, - DDOG_REMOTE_CONFIG_PRODUCT_LIVE_DEBUGGER, -} ddog_RemoteConfigProduct; +typedef enum ddog_MetricType { + DDOG_METRIC_TYPE_GAUGE, + DDOG_METRIC_TYPE_COUNT, + DDOG_METRIC_TYPE_DISTRIBUTION, +} ddog_MetricType; + +typedef enum ddog_ProbeStatus { + DDOG_PROBE_STATUS_RECEIVED, + DDOG_PROBE_STATUS_INSTALLED, + DDOG_PROBE_STATUS_EMITTING, + DDOG_PROBE_STATUS_ERROR, + DDOG_PROBE_STATUS_BLOCKED, + DDOG_PROBE_STATUS_WARNING, +} ddog_ProbeStatus; typedef enum ddog_RemoteConfigCapabilities { DDOG_REMOTE_CONFIG_CAPABILITIES_ASM_ACTIVATION = 1, @@ -433,14 +416,29 @@ typedef enum ddog_RemoteConfigCapabilities { DDOG_REMOTE_CONFIG_CAPABILITIES_FFE_FLAG_CONFIGURATION_RULES = 46, } ddog_RemoteConfigCapabilities; +typedef enum ddog_RemoteConfigProduct { + DDOG_REMOTE_CONFIG_PRODUCT_AGENT_CONFIG, + DDOG_REMOTE_CONFIG_PRODUCT_AGENT_TASK, + DDOG_REMOTE_CONFIG_PRODUCT_APM_TRACING, + DDOG_REMOTE_CONFIG_PRODUCT_ASM, + DDOG_REMOTE_CONFIG_PRODUCT_ASM_DATA, + DDOG_REMOTE_CONFIG_PRODUCT_ASM_DD, + DDOG_REMOTE_CONFIG_PRODUCT_ASM_FEATURES, + DDOG_REMOTE_CONFIG_PRODUCT_FFE_FLAGS, + DDOG_REMOTE_CONFIG_PRODUCT_LIVE_DEBUGGER, +} ddog_RemoteConfigProduct; + +typedef enum ddog_SpanProbeTarget { + DDOG_SPAN_PROBE_TARGET_ACTIVE, + DDOG_SPAN_PROBE_TARGET_ROOT, +} ddog_SpanProbeTarget; + typedef struct ddog_AgentInfoReader ddog_AgentInfoReader; typedef struct ddog_DebuggerPayload ddog_DebuggerPayload; typedef struct ddog_DslString ddog_DslString; -typedef struct ddog_FfeResult ddog_FfeResult; - typedef struct ddog_HashMap_ShmCacheKey__ShmCache ddog_HashMap_ShmCacheKey__ShmCache; /** @@ -480,6 +478,24 @@ typedef struct ddog_SidecarTransport ddog_SidecarTransport; */ typedef struct ddog_SpanConcentrator ddog_SpanConcentrator; +typedef struct ddog_FfeResult { + struct _zend_string *value_json; + struct _zend_string *variant; + struct _zend_string *allocation_key; + int32_t reason; + int32_t error_code; + bool do_log; + bool valid; +} ddog_FfeResult; + +typedef struct ddog_FfeAttribute { + ddog_CharSlice key; + int32_t value_type; + ddog_CharSlice string_value; + double number_value; + bool bool_value; +} ddog_FfeAttribute; + /** * Flags selecting which Remote Config products/capabilities to subscribe to. * @@ -510,16 +526,6 @@ typedef struct ddog_Tag { typedef struct _zend_string *ddog_OwnedZendString; -struct ddog_FfeResult { - ddog_OwnedZendString value_json; - ddog_OwnedZendString variant; - ddog_OwnedZendString allocation_key; - int32_t reason; - int32_t error_code; - bool do_log; - bool valid; -}; - typedef struct _zend_string *(*ddog_DynamicConfigUpdate)(ddog_CharSlice config, ddog_OwnedZendString value, enum ddog_DynamicConfigUpdateMode mode); @@ -704,14 +710,6 @@ typedef struct ddog_Vec_DebuggerPayload { */ typedef uint64_t ddog_QueueId; -typedef struct ddog_FfeAttribute { - ddog_CharSlice key; - int32_t value_type; - ddog_CharSlice string_value; - double number_value; - bool bool_value; -} ddog_FfeAttribute; - /** * A (key, value) pair for peer-service tags, borrowed from PHP/concentrator memory. */ @@ -945,18 +943,18 @@ typedef struct ddog_DebuggerValue ddog_DebuggerValue; #define ddog_EVALUATOR_RESULT_REDACTED (const void*)-2 -typedef enum ddog_FieldType { - DDOG_FIELD_TYPE_STATIC, - DDOG_FIELD_TYPE_ARG, - DDOG_FIELD_TYPE_LOCAL, -} ddog_FieldType; - typedef enum ddog_DebuggerType { DDOG_DEBUGGER_TYPE_DIAGNOSTICS, DDOG_DEBUGGER_TYPE_SNAPSHOTS, DDOG_DEBUGGER_TYPE_LOGS, } ddog_DebuggerType; +typedef enum ddog_FieldType { + DDOG_FIELD_TYPE_STATIC, + DDOG_FIELD_TYPE_ARG, + DDOG_FIELD_TYPE_LOCAL, +} ddog_FieldType; + typedef struct ddog_Entry ddog_Entry; typedef struct ddog_HashMap_CowStr__Value ddog_HashMap_CowStr__Value; @@ -1085,6 +1083,16 @@ typedef struct ddog_OwnedCharSlice { void (*free)(ddog_CharSlice); } ddog_OwnedCharSlice; +typedef enum ddog_LogLevel { + DDOG_LOG_LEVEL_ERROR, + DDOG_LOG_LEVEL_WARN, + DDOG_LOG_LEVEL_DEBUG, +} ddog_LogLevel; + +typedef enum ddog_TelemetryWorkerBuilderBoolProperty { + DDOG_TELEMETRY_WORKER_BUILDER_BOOL_PROPERTY_CONFIG_TELEMETRY_DEBUG_LOGGING_ENABLED, +} ddog_TelemetryWorkerBuilderBoolProperty; + typedef enum ddog_TelemetryWorkerBuilderEndpointProperty { DDOG_TELEMETRY_WORKER_BUILDER_ENDPOINT_PROPERTY_CONFIG_ENDPOINT, } ddog_TelemetryWorkerBuilderEndpointProperty; @@ -1106,16 +1114,6 @@ typedef enum ddog_TelemetryWorkerBuilderStrProperty { DDOG_TELEMETRY_WORKER_BUILDER_STR_PROPERTY_ROOT_SESSION_ID, } ddog_TelemetryWorkerBuilderStrProperty; -typedef enum ddog_TelemetryWorkerBuilderBoolProperty { - DDOG_TELEMETRY_WORKER_BUILDER_BOOL_PROPERTY_CONFIG_TELEMETRY_DEBUG_LOGGING_ENABLED, -} ddog_TelemetryWorkerBuilderBoolProperty; - -typedef enum ddog_LogLevel { - DDOG_LOG_LEVEL_ERROR, - DDOG_LOG_LEVEL_WARN, - DDOG_LOG_LEVEL_DEBUG, -} ddog_LogLevel; - typedef struct ddog_TelemetryWorkerBuilder ddog_TelemetryWorkerBuilder; /** @@ -1223,6 +1221,34 @@ 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_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. @@ -1257,25 +1283,37 @@ typedef struct ddog_SenderParameters { ddog_CharSlice url; } ddog_SenderParameters; +typedef enum ddog_crasht_BuildIdType { + DDOG_CRASHT_BUILD_ID_TYPE_GNU, + DDOG_CRASHT_BUILD_ID_TYPE_GO, + DDOG_CRASHT_BUILD_ID_TYPE_PDB, + DDOG_CRASHT_BUILD_ID_TYPE_SHA1, +} ddog_crasht_BuildIdType; + /** - * Stacktrace collection occurs in the context of a crashing process. - * If the stack is sufficiently corruputed, it is possible (but unlikely), - * for stack trace collection itself to crash. - * We recommend fully enabling stacktrace collection, but having an environment - * variable to allow downgrading the collector. + * Result type for runtime callback registration */ -typedef enum ddog_crasht_StacktraceCollection { - DDOG_CRASHT_STACKTRACE_COLLECTION_DISABLED, - DDOG_CRASHT_STACKTRACE_COLLECTION_WITHOUT_SYMBOLS, - /** - * This option uses `backtrace::resolve_frame_unsynchronized()` to gather symbol information - * and also unwind inlined functions. Enabling this feature will not only provide symbolic - * details, but may also yield additional or less stack frames compared to other - * configurations. - */ - DDOG_CRASHT_STACKTRACE_COLLECTION_ENABLED_WITH_INPROCESS_SYMBOLS, - DDOG_CRASHT_STACKTRACE_COLLECTION_ENABLED_WITH_SYMBOLS_IN_RECEIVER, -} ddog_crasht_StacktraceCollection; +typedef enum ddog_crasht_CallbackResult { + DDOG_CRASHT_CALLBACK_RESULT_OK, + DDOG_CRASHT_CALLBACK_RESULT_ERROR, +} ddog_crasht_CallbackResult; + +typedef enum ddog_crasht_DemangleOptions { + DDOG_CRASHT_DEMANGLE_OPTIONS_COMPLETE, + DDOG_CRASHT_DEMANGLE_OPTIONS_NAME_ONLY, +} ddog_crasht_DemangleOptions; + +typedef enum ddog_crasht_ErrorKind { + DDOG_CRASHT_ERROR_KIND_PANIC, + DDOG_CRASHT_ERROR_KIND_UNHANDLED_EXCEPTION, + DDOG_CRASHT_ERROR_KIND_UNIX_SIGNAL, +} ddog_crasht_ErrorKind; + +typedef enum ddog_crasht_FileType { + DDOG_CRASHT_FILE_TYPE_APK, + DDOG_CRASHT_FILE_TYPE_ELF, + DDOG_CRASHT_FILE_TYPE_PE, +} ddog_crasht_FileType; /** * This enum represents operations a the tracked library might be engaged in. @@ -1300,12 +1338,6 @@ typedef enum ddog_crasht_OpTypes { DDOG_CRASHT_OP_TYPES_SIZE, } ddog_crasht_OpTypes; -typedef enum ddog_crasht_ErrorKind { - DDOG_CRASHT_ERROR_KIND_PANIC, - DDOG_CRASHT_ERROR_KIND_UNHANDLED_EXCEPTION, - DDOG_CRASHT_ERROR_KIND_UNIX_SIGNAL, -} ddog_crasht_ErrorKind; - /** * See https://man7.org/linux/man-pages/man2/sigaction.2.html * MUST REMAIN IN SYNC WITH THE ENUM IN emit_sigcodes.c @@ -1378,31 +1410,25 @@ typedef enum ddog_crasht_SignalNames { DDOG_CRASHT_SIGNAL_NAMES_UNKNOWN, } ddog_crasht_SignalNames; -typedef enum ddog_crasht_BuildIdType { - DDOG_CRASHT_BUILD_ID_TYPE_GNU, - DDOG_CRASHT_BUILD_ID_TYPE_GO, - DDOG_CRASHT_BUILD_ID_TYPE_PDB, - DDOG_CRASHT_BUILD_ID_TYPE_SHA1, -} ddog_crasht_BuildIdType; - -typedef enum ddog_crasht_FileType { - DDOG_CRASHT_FILE_TYPE_APK, - DDOG_CRASHT_FILE_TYPE_ELF, - DDOG_CRASHT_FILE_TYPE_PE, -} ddog_crasht_FileType; - -typedef enum ddog_crasht_DemangleOptions { - DDOG_CRASHT_DEMANGLE_OPTIONS_COMPLETE, - DDOG_CRASHT_DEMANGLE_OPTIONS_NAME_ONLY, -} ddog_crasht_DemangleOptions; - /** - * Result type for runtime callback registration + * Stacktrace collection occurs in the context of a crashing process. + * If the stack is sufficiently corruputed, it is possible (but unlikely), + * for stack trace collection itself to crash. + * We recommend fully enabling stacktrace collection, but having an environment + * variable to allow downgrading the collector. */ -typedef enum ddog_crasht_CallbackResult { - DDOG_CRASHT_CALLBACK_RESULT_OK, - DDOG_CRASHT_CALLBACK_RESULT_ERROR, -} ddog_crasht_CallbackResult; +typedef enum ddog_crasht_StacktraceCollection { + DDOG_CRASHT_STACKTRACE_COLLECTION_DISABLED, + DDOG_CRASHT_STACKTRACE_COLLECTION_WITHOUT_SYMBOLS, + /** + * This option uses `backtrace::resolve_frame_unsynchronized()` to gather symbol information + * and also unwind inlined functions. Enabling this feature will not only provide symbolic + * details, but may also yield additional or less stack frames compared to other + * configurations. + */ + DDOG_CRASHT_STACKTRACE_COLLECTION_ENABLED_WITH_INPROCESS_SYMBOLS, + DDOG_CRASHT_STACKTRACE_COLLECTION_ENABLED_WITH_SYMBOLS_IN_RECEIVER, +} ddog_crasht_StacktraceCollection; typedef struct ddog_crasht_CrashInfo ddog_crasht_CrashInfo; diff --git a/components-rs/ddtrace.h b/components-rs/ddtrace.h index cbcd6a9d02b..1d6d866a132 100644 --- a/components-rs/ddtrace.h +++ b/components-rs/ddtrace.h @@ -61,18 +61,6 @@ int posix_spawn_file_actions_addchdir_np(void *file_actions, const char *path); uint64_t dd_fnv1a_64(const uint8_t *data, uintptr_t len); -bool ddog_ffe_load_config(ddog_CharSlice json); - -bool ddog_ffe_has_config(void); - -uint64_t ddog_ffe_config_version(void); - -struct ddog_FfeResult ddog_ffe_evaluate(ddog_CharSlice flag_key, - int32_t expected_type, - ddog_CharSlice targeting_key, - const struct ddog_FfeAttribute *attributes, - uintptr_t attributes_count); - const char *ddog_normalize_process_tag_value(ddog_CharSlice tag_value); void ddog_free_normalized_tag_value(const char *ptr); @@ -118,6 +106,18 @@ void ddog_agent_info_json_free(char *ptr); */ void ddog_apply_agent_info_concentrator_config(struct ddog_AgentInfoReader *reader); +bool ddog_ffe_load_config(ddog_CharSlice json); + +bool ddog_ffe_has_config(void); + +uint64_t ddog_ffe_config_version(void); + +struct ddog_FfeResult ddog_ffe_evaluate(ddog_CharSlice flag_key, + int32_t expected_type, + ddog_CharSlice targeting_key, + const struct ddog_FfeAttribute *attributes, + uintptr_t attributes_count); + bool ddog_shall_log(enum ddog_Log category); void ddog_set_error_log_level(bool once); diff --git a/components-rs/ffe.rs b/components-rs/ffe.rs index 33b132af7a0..83530ee00cb 100644 --- a/components-rs/ffe.rs +++ b/components-rs/ffe.rs @@ -1,4 +1,4 @@ -use crate::bytes::OwnedZendString; +use crate::bytes::{OwnedZendString, ZendString}; use datadog_ffe::rules_based::{ self as ffe, AssignmentReason, AssignmentValue, Attribute, Configuration, EvaluationContext, EvaluationError, ExpectedFlagType, Str, UniversalFlagConfig, @@ -6,6 +6,7 @@ use datadog_ffe::rules_based::{ use libdd_common_ffi::slice::{AsBytes, CharSlice}; use std::cell::RefCell; use std::collections::HashMap; +use std::ptr::NonNull; use std::sync::Arc; struct FfeState { @@ -92,9 +93,9 @@ const TYPE_OBJECT: i32 = 4; #[repr(C)] pub struct FfeResult { - pub value_json: Option, - pub variant: Option, - pub allocation_key: Option, + pub value_json: Option>, + pub variant: Option>, + pub allocation_key: Option>, pub reason: i32, pub error_code: i32, pub do_log: bool, @@ -210,9 +211,9 @@ fn result_from_assignment(assignment: Result) Ok(assignment) => { let value_json = assignment_value_to_json(&assignment.value); FfeResult { - value_json: Some(value_json.as_str().into()), - variant: Some(assignment.variation_key.as_str().into()), - allocation_key: Some(assignment.allocation_key.as_str().into()), + value_json: result_string(value_json.as_str().into()), + variant: result_string(assignment.variation_key.as_str().into()), + allocation_key: result_string(assignment.allocation_key.as_str().into()), reason: match assignment.reason { AssignmentReason::Static => REASON_STATIC, AssignmentReason::TargetingMatch => REASON_TARGETING_MATCH, @@ -237,7 +238,7 @@ fn result_from_assignment(assignment: Result) }; FfeResult { - value_json: Some("null".into()), + value_json: result_string("null".into()), variant: None, allocation_key: None, reason, @@ -249,6 +250,12 @@ fn result_from_assignment(assignment: Result) } } +fn result_string(value: OwnedZendString) -> Option> { + let ptr = value.0; + std::mem::forget(value); + Some(ptr) +} + fn invalid_result() -> FfeResult { FfeResult { value_json: None, @@ -410,8 +417,9 @@ mod tests { assert_eq!(result.reason, REASON_SPLIT); assert_eq!(result.error_code, ERROR_NONE); assert_eq!(result.do_log, true); + let value_json = unsafe { result.value_json.unwrap().as_ref() }; assert_eq!( - std::str::from_utf8(result.value_json.as_ref().unwrap().as_ref()).unwrap(), + std::str::from_utf8(value_json.as_ref()).unwrap(), r#""empty-targeting-key""# ); clear_config(); diff --git a/components-rs/sidecar.h b/components-rs/sidecar.h index 4a3aa617416..f8d13eb2de2 100644 --- a/components-rs/sidecar.h +++ b/components-rs/sidecar.h @@ -298,6 +298,23 @@ ddog_MaybeError ddog_sidecar_send_debugger_datum(struct ddog_SidecarTransport ** ddog_QueueId queue_id, struct ddog_DebuggerPayload *payload); +/** + * 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, @@ -441,8 +458,21 @@ ddog_SpanBytes *ddog_trace_new_span_with_capacities(ddog_TraceBytes *trace, uintptr_t meta_size, uintptr_t metrics_size); +/** + * The returned slice is an owned allocation that must be properly freed using + * [`ddog_free_charslice`]. + */ ddog_CharSlice ddog_span_debug_log(const ddog_SpanBytes *span); +/** + * Frees an owned [`CharSlice`]. Note that some functions of this API return borrowed slices that + * must NOT be freed. Only a few selected functions return slices that must be freed, and this is + * mentioned explicitly in their documentation. + * + * # Safety + * + * `slice` must be an owned char slice that has been returned by one of the functions of this API. + */ void ddog_free_charslice(ddog_CharSlice slice); void ddog_set_span_service(ddog_SpanBytes *span, ddog_CharSlice slice); @@ -493,6 +523,10 @@ ddog_CharSlice ddog_get_span_meta(ddog_SpanBytes *span, ddog_CharSlice key); bool ddog_has_span_meta(ddog_SpanBytes *span, ddog_CharSlice key); +/** + * The return value is an owned array of slices (`Box<[CharSlice]>`) that must be freed explicitly + * through [`ddog_span_free_keys_ptr`]. + */ ddog_CharSlice *ddog_span_meta_get_keys(ddog_SpanBytes *span, uintptr_t *out_count); void ddog_add_span_metrics(ddog_SpanBytes *span, ddog_CharSlice key, double val); @@ -513,8 +547,18 @@ ddog_CharSlice ddog_get_span_meta_struct(ddog_SpanBytes *span, ddog_CharSlice ke bool ddog_has_span_meta_struct(ddog_SpanBytes *span, ddog_CharSlice key); +/** + * The return value is an array of slices (`Box<[CharSlice]>`) that must be freed explicitly + * through [`ddog_span_free_keys_ptr`]. + */ ddog_CharSlice *ddog_span_meta_struct_get_keys(ddog_SpanBytes *span, uintptr_t *out_count); +/** + * # Safety + * + * `keys_ptr` must have been returned by one of the `ddog_xxx_get_keys()` functions, and must not + * have been already freed. + */ void ddog_span_free_keys_ptr(ddog_CharSlice *keys_ptr, uintptr_t count); ddog_SpanLinkBytes *ddog_span_new_link(ddog_SpanBytes *span); @@ -547,6 +591,10 @@ void ddog_add_event_attributes_int(ddog_SpanEventBytes *event, ddog_CharSlice ke void ddog_add_event_attributes_float(ddog_SpanEventBytes *event, ddog_CharSlice key, double val); +/** + * The returned slice is an owned allocation that must be properly freed using + * [`ddog_free_charslice`]. + */ ddog_CharSlice ddog_serialize_trace_into_charslice(ddog_TraceBytes *trace); #endif /* DDOG_SIDECAR_H */ diff --git a/ext/ddtrace.c b/ext/ddtrace.c index 08cabc95e29..2bf07deeeab 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -525,7 +525,11 @@ static void ddtrace_activate(void) { pthread_once(&dd_activate_once_control, dd_activate_once); zai_config_rinit(); - if (!ddtrace_disable && (get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED() || get_global_DD_TRACE_SIDECAR_TRACE_SENDER())) { + if (!ddtrace_disable && ( + get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED() || + get_global_DD_TRACE_SIDECAR_TRACE_SENDER() || + get_global_DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED() + )) { ddtrace_sidecar_ensure_active(); } @@ -1932,6 +1936,8 @@ static PHP_RSHUTDOWN_FUNCTION(ddtrace) { ddtrace_rshutdown_remote_config(); } + ddtrace_ffe_flush_evaluation_metrics(); + if (!ddtrace_disable) { ddtrace_autoload_rshutdown(); @@ -2786,6 +2792,45 @@ PHP_FUNCTION(DDTrace_dogstatsd_count) { RETURN_NULL(); } +PHP_FUNCTION(DDTrace_Internal_record_ffe_evaluation_metric) { + char *flag_key; + size_t flag_key_len; + char *variant = NULL; + size_t variant_len = 0; + char *reason = NULL; + size_t reason_len = 0; + char *error_type = NULL; + size_t error_type_len = 0; + char *allocation_key = NULL; + size_t allocation_key_len = 0; + + ZEND_PARSE_PARAMETERS_START(5, 5) + Z_PARAM_STRING(flag_key, flag_key_len) + Z_PARAM_STRING_OR_NULL(variant, variant_len) + Z_PARAM_STRING_OR_NULL(reason, reason_len) + Z_PARAM_STRING_OR_NULL(error_type, error_type_len) + Z_PARAM_STRING_OR_NULL(allocation_key, allocation_key_len) + ZEND_PARSE_PARAMETERS_END(); + + RETURN_BOOL(ddtrace_ffe_record_evaluation_metric( + flag_key, + flag_key_len, + variant, + variant_len, + reason, + reason_len, + error_type, + error_type_len, + allocation_key, + allocation_key_len)); +} + +PHP_FUNCTION(DDTrace_Internal_flush_ffe_evaluation_metrics) { + ZEND_PARSE_PARAMETERS_NONE(); + + RETURN_BOOL(ddtrace_ffe_flush_evaluation_metrics()); +} + PHP_FUNCTION(DDTrace_dogstatsd_distribution) { zend_string *metric; double value; @@ -2975,6 +3020,68 @@ PHP_FUNCTION(DDTrace_Testing_ffe_load_config) { RETURN_BOOL(ddog_ffe_load_config(dd_zend_string_to_CharSlice(json))); } +static const char *ddtrace_ffe_reason_name(int32_t reason) { + switch (reason) { + case 0: + return "STATIC"; + case 2: + return "TARGETING_MATCH"; + case 3: + return "SPLIT"; + case 4: + return "DISABLED"; + case 5: + return "ERROR"; + case 1: + default: + return "DEFAULT"; + } +} + +static const char *ddtrace_ffe_error_name(int32_t error_code) { + switch (error_code) { + case 0: + return NULL; + case 1: + return "TYPE_MISMATCH"; + case 2: + return "PARSE_ERROR"; + case 3: + return "FLAG_NOT_FOUND"; + case 6: + return "PROVIDER_NOT_READY"; + case 7: + default: + return "GENERAL"; + } +} + +static int32_t ddtrace_ffe_effective_reason(int32_t reason, int32_t error_code) { + return error_code == 0 ? reason : 5; +} + +static void ddtrace_ffe_record_evaluation_metric_result( + zend_string *flag_key, + zend_string *variant, + zend_string *allocation_key, + int32_t reason, + int32_t error_code +) { + const char *reason_name = ddtrace_ffe_reason_name(ddtrace_ffe_effective_reason(reason, error_code)); + const char *error_name = ddtrace_ffe_error_name(error_code); + ddtrace_ffe_record_evaluation_metric( + ZSTR_VAL(flag_key), + ZSTR_LEN(flag_key), + variant ? ZSTR_VAL(variant) : NULL, + variant ? ZSTR_LEN(variant) : 0, + reason_name, + strlen(reason_name), + error_name, + error_name ? strlen(error_name) : 0, + allocation_key ? ZSTR_VAL(allocation_key) : NULL, + allocation_key ? ZSTR_LEN(allocation_key) : 0); +} + 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); @@ -3017,6 +3124,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; @@ -3032,15 +3155,22 @@ PHP_FUNCTION(DDTrace_ffe_evaluate) { zend_string *key; zval *value; struct ddog_FfeResult result; + zend_bool record_metric = true; + zend_string *value_json; + zend_string *variant; + zend_string *allocation_key; - ZEND_PARSE_PARAMETERS_START(4, 4) + ZEND_PARSE_PARAMETERS_START(4, 5) Z_PARAM_STR(flag_key) Z_PARAM_LONG(type_id_zl) Z_PARAM_STR_OR_NULL(targeting_key) Z_PARAM_ARRAY(attrs_zv) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(record_metric) 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); @@ -3114,14 +3244,38 @@ PHP_FUNCTION(DDTrace_ffe_evaluate) { } if (!result.valid) { + if (record_metric) { + ddtrace_ffe_record_evaluation_metric( + ZSTR_VAL(flag_key), + ZSTR_LEN(flag_key), + NULL, + 0, + ZEND_STRL("ERROR"), + ZEND_STRL("PROVIDER_NOT_READY"), + NULL, + 0); + } RETURN_NULL(); } + value_json = result.value_json; + variant = result.variant; + allocation_key = result.allocation_key; + + if (record_metric) { + ddtrace_ffe_record_evaluation_metric_result( + flag_key, + variant, + allocation_key, + result.reason, + result.error_code); + } + 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); - ddtrace_ffe_update_nullable_string_property(return_value, ZEND_STRL("allocationKey"), result.allocation_key); - ddtrace_ffe_update_long_property(return_value, ZEND_STRL("reason"), result.reason); + ddtrace_ffe_update_nullable_string_property(return_value, ZEND_STRL("valueJson"), value_json); + ddtrace_ffe_update_nullable_string_property(return_value, ZEND_STRL("variant"), variant); + ddtrace_ffe_update_nullable_string_property(return_value, ZEND_STRL("allocationKey"), allocation_key); + ddtrace_ffe_update_long_property(return_value, ZEND_STRL("reason"), ddtrace_ffe_effective_reason(result.reason, result.error_code)); ddtrace_ffe_update_long_property(return_value, ZEND_STRL("errorCode"), result.error_code); ddtrace_ffe_update_bool_property(return_value, ZEND_STRL("doLog"), result.do_log); ddtrace_ffe_update_empty_array_property(return_value, ZEND_STRL("providerState")); diff --git a/ext/ddtrace.h b/ext/ddtrace.h index 6f9e5cc08cf..d646f531b7c 100644 --- a/ext/ddtrace.h +++ b/ext/ddtrace.h @@ -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_metric_buffer; + size_t ffe_metric_buffer_len; + size_t ffe_metric_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 diff --git a/ext/ddtrace.stub.php b/ext/ddtrace.stub.php index ee00a8ee3b2..b179289500a 100644 --- a/ext/ddtrace.stub.php +++ b/ext/ddtrace.stub.php @@ -900,7 +900,7 @@ function flush_endpoints(): void {} * * @internal Used by the Datadog feature flag client. */ - function ffe_evaluate(string $flagKey, int $expectedType, ?string $targetingKey, array $attributes): ?FfeResult {} + function ffe_evaluate(string $flagKey, int $expectedType, ?string $targetingKey, array $attributes, bool $recordMetric = true): ?FfeResult {} /** * Check if FFE (Feature Flag Evaluation) configuration is loaded. @@ -1061,6 +1061,24 @@ function add_span_flag(\DDTrace\SpanData $span, int $flag): void {} */ function handle_fork(): void {} + /** + * Record a Feature Flag Evaluation metric event in native request-local + * memory. The batch is flushed to the shared sidecar during request + * shutdown; PHP does not aggregate, encode OTLP, or perform transport. + * + * @internal + */ + function record_ffe_evaluation_metric(string $flagKey, ?string $variant, ?string $reason, ?string $errorType, ?string $allocationKey): bool {} + + /** + * Flush request-local Feature Flag Evaluation metric events to the shared + * sidecar. Intended for long-lived integration test servers; normal PHP + * requests flush during request shutdown. + * + * @internal + */ + function flush_ffe_evaluation_metrics(): bool {} + } namespace datadog\appsec\v2 { diff --git a/ext/ddtrace_arginfo.h b/ext/ddtrace_arginfo.h index 9722e64a323..f38219795b9 100644 --- a/ext/ddtrace_arginfo.h +++ b/ext/ddtrace_arginfo.h @@ -181,6 +181,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_DDTrace_ffe_evaluate, 0, 4, DDTra ZEND_ARG_TYPE_INFO(0, expectedType, IS_LONG, 0) ZEND_ARG_TYPE_INFO(0, targetingKey, IS_STRING, 1) ZEND_ARG_TYPE_INFO(0, attributes, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, recordMetric, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_ffe_has_config, 0, 0, _IS_BOOL, 0) @@ -244,6 +245,17 @@ ZEND_END_ARG_INFO() #define arginfo_DDTrace_Internal_handle_fork arginfo_DDTrace_flush +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Internal_record_ffe_evaluation_metric, 0, 5, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, flagKey, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, variant, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, reason, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, errorType, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, allocationKey, IS_STRING, 1) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_DDTrace_Internal_flush_ffe_evaluation_metrics, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + 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") @@ -428,6 +440,8 @@ 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_record_ffe_evaluation_metric); +ZEND_FUNCTION(DDTrace_Internal_flush_ffe_evaluation_metrics); ZEND_FUNCTION(datadog_appsec_v2_track_user_login_success); ZEND_FUNCTION(datadog_appsec_v2_track_user_login_failure); ZEND_FUNCTION(dd_trace_env_config); @@ -527,6 +541,8 @@ 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", "record_ffe_evaluation_metric"), zif_DDTrace_Internal_record_ffe_evaluation_metric, arginfo_DDTrace_Internal_record_ffe_evaluation_metric, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("DDTrace\\Internal", "flush_ffe_evaluation_metrics"), zif_DDTrace_Internal_flush_ffe_evaluation_metrics, arginfo_DDTrace_Internal_flush_ffe_evaluation_metrics, 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) diff --git a/ext/sidecar.c b/ext/sidecar.c index 1713e86691e..7462f4c0fcc 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -28,6 +28,75 @@ ddog_Endpoint *ddtrace_endpoint; ddog_Endpoint *dogstatsd_endpoint; // always set when ddtrace_endpoint is set struct ddog_InstanceId *ddtrace_sidecar_instance_id; +#define DDTRACE_FFE_METRIC_BUFFER_LIMIT 1000 + +typedef struct { + zend_string *flag_key; + zend_string *variant; + zend_string *reason; + zend_string *error_type; + zend_string *allocation_key; +} ddtrace_ffe_metric; + +static void ddtrace_ffe_release_metric(ddtrace_ffe_metric *metric) { + if (!metric) { + return; + } + if (metric->flag_key) { + zend_string_release(metric->flag_key); + } + if (metric->variant) { + zend_string_release(metric->variant); + } + if (metric->reason) { + zend_string_release(metric->reason); + } + if (metric->error_type) { + zend_string_release(metric->error_type); + } + if (metric->allocation_key) { + zend_string_release(metric->allocation_key); + } +} + +static void ddtrace_ffe_clear_evaluation_metrics(void) { + ddtrace_ffe_metric *buffer = (ddtrace_ffe_metric *) DDTRACE_G(ffe_metric_buffer); + for (size_t i = 0; i < DDTRACE_G(ffe_metric_buffer_len); i++) { + ddtrace_ffe_release_metric(&buffer[i]); + } + if (buffer) { + efree(buffer); + } + DDTRACE_G(ffe_metric_buffer) = NULL; + DDTRACE_G(ffe_metric_buffer_len) = 0; + DDTRACE_G(ffe_metric_buffer_cap) = 0; +} + +static zend_string *ddtrace_ffe_otlp_metrics_endpoint(void) { + const char *metrics_endpoint = getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"); + if (metrics_endpoint && metrics_endpoint[0] != '\0') { + return zend_string_init(metrics_endpoint, strlen(metrics_endpoint), 0); + } + + const char *base_endpoint = getenv("OTEL_EXPORTER_OTLP_ENDPOINT"); + if (base_endpoint && base_endpoint[0] != '\0') { + size_t base_len = strlen(base_endpoint); + while (base_len > 0 && base_endpoint[base_len - 1] == '/') { + base_len--; + } + + const char suffix[] = "/v1/metrics"; + size_t suffix_len = sizeof(suffix) - 1; + zend_string *endpoint = zend_string_alloc(base_len + suffix_len, 0); + memcpy(ZSTR_VAL(endpoint), base_endpoint, base_len); + memcpy(ZSTR_VAL(endpoint) + base_len, suffix, suffix_len); + ZSTR_VAL(endpoint)[base_len + suffix_len] = '\0'; + return endpoint; + } + + return zend_string_init(ZEND_STRL("http://localhost:4318/v1/metrics"), 0); +} + // Best-effort pointer for the signal handler (SIGTERM/SIGINT). Set to the first // per-thread connection; never cleared until MSHUTDOWN. Not atomic: concurrent // shutdown is already a best-effort race for signal handlers, so atomicity of @@ -675,6 +744,102 @@ void ddtrace_sidecar_dogstatsd_count(zend_string *metric, zend_long value, zval ddog_Vec_Tag_drop(vec); } +bool ddtrace_ffe_record_evaluation_metric( + const char *flag_key, + size_t flag_key_len, + const char *variant, + size_t variant_len, + const char *reason, + size_t reason_len, + const char *error_type, + size_t error_type_len, + const char *allocation_key, + size_t allocation_key_len +) { + if (!get_DD_METRICS_OTEL_ENABLED() || !flag_key || flag_key_len == 0) { + return false; + } + + if (DDTRACE_G(ffe_metric_buffer_len) >= DDTRACE_FFE_METRIC_BUFFER_LIMIT) { + return false; + } + + if (DDTRACE_G(ffe_metric_buffer_len) == DDTRACE_G(ffe_metric_buffer_cap)) { + size_t new_cap = DDTRACE_G(ffe_metric_buffer_cap) == 0 ? 8 : DDTRACE_G(ffe_metric_buffer_cap) * 2; + if (new_cap > DDTRACE_FFE_METRIC_BUFFER_LIMIT) { + new_cap = DDTRACE_FFE_METRIC_BUFFER_LIMIT; + } + DDTRACE_G(ffe_metric_buffer) = safe_erealloc( + DDTRACE_G(ffe_metric_buffer), + new_cap, + sizeof(ddtrace_ffe_metric), + 0 + ); + DDTRACE_G(ffe_metric_buffer_cap) = new_cap; + } + + ddtrace_ffe_metric *buffer = (ddtrace_ffe_metric *) DDTRACE_G(ffe_metric_buffer); + ddtrace_ffe_metric *metric = &buffer[DDTRACE_G(ffe_metric_buffer_len)++]; + metric->flag_key = zend_string_init(flag_key, flag_key_len, 0); + metric->variant = zend_string_init(variant ? variant : "", variant ? variant_len : 0, 0); + metric->reason = zend_string_init(reason ? reason : "", reason ? reason_len : 0, 0); + metric->error_type = zend_string_init(error_type ? error_type : "", error_type ? error_type_len : 0, 0); + metric->allocation_key = zend_string_init(allocation_key ? allocation_key : "", allocation_key ? allocation_key_len : 0, 0); + + return true; +} + +bool ddtrace_ffe_flush_evaluation_metrics(void) { + size_t metric_count = DDTRACE_G(ffe_metric_buffer_len); + ddtrace_ffe_metric *buffer = (ddtrace_ffe_metric *) DDTRACE_G(ffe_metric_buffer); + + if (metric_count == 0 || !buffer) { + return false; + } + + if (!DDTRACE_G(sidecar) || !ddtrace_sidecar_instance_id || !DDTRACE_G(sidecar_queue_id)) { + ddtrace_ffe_clear_evaluation_metrics(); + return false; + } + + zend_string *endpoint = ddtrace_ffe_otlp_metrics_endpoint(); + ddog_FfeEvaluationMetric *ffi_metrics = safe_emalloc(metric_count, sizeof(ddog_FfeEvaluationMetric), 0); + for (size_t i = 0; i < metric_count; i++) { + ffi_metrics[i] = (ddog_FfeEvaluationMetric) { + .flag_key = dd_zend_string_to_CharSlice(buffer[i].flag_key), + .variant = dd_zend_string_to_CharSlice(buffer[i].variant), + .reason = dd_zend_string_to_CharSlice(buffer[i].reason), + .error_type = dd_zend_string_to_CharSlice(buffer[i].error_type), + .allocation_key = dd_zend_string_to_CharSlice(buffer[i].allocation_key), + }; + } + + ddog_FfeTelemetryContext context = { + .service = dd_zend_string_to_CharSlice(get_DD_SERVICE()), + .env = dd_zend_string_to_CharSlice(get_DD_ENV()), + .version = dd_zend_string_to_CharSlice(get_DD_VERSION()), + }; + ddog_Slice_FfeEvaluationMetric metric_slice = { + .ptr = ffi_metrics, + .len = metric_count, + }; + + bool flushed = ddtrace_ffi_try( + "Failed sending FFE metrics batch to sidecar", + ddog_sidecar_send_ffe_evaluation_metrics( + &DDTRACE_G(sidecar), + ddtrace_sidecar_instance_id, + &DDTRACE_G(sidecar_queue_id), + dd_zend_string_to_CharSlice(endpoint), + &context, + metric_slice)); + + efree(ffi_metrics); + zend_string_release(endpoint); + ddtrace_ffe_clear_evaluation_metrics(); + return flushed; +} + void ddtrace_sidecar_dogstatsd_distribution(zend_string *metric, double value, zval *tags) { if (!DDTRACE_G(sidecar) || !get_DD_INTEGRATION_METRICS_ENABLED()) { return; diff --git a/ext/sidecar.h b/ext/sidecar.h index 8db3a57db5f..9d21996e9f3 100644 --- a/ext/sidecar.h +++ b/ext/sidecar.h @@ -72,6 +72,9 @@ void ddtrace_sidecar_dogstatsd_gauge(zend_string *metric, double value, zval *ta void ddtrace_sidecar_dogstatsd_histogram(zend_string *metric, double value, zval *tags); void ddtrace_sidecar_dogstatsd_set(zend_string *metric, zend_long value, zval *tags); +bool ddtrace_ffe_record_evaluation_metric(const char *flag_key, size_t flag_key_len, const char *variant, size_t variant_len, const char *reason, size_t reason_len, const char *error_type, size_t error_type_len, const char *allocation_key, size_t allocation_key_len); +bool ddtrace_ffe_flush_evaluation_metrics(void); + bool ddtrace_alter_test_session_token(zval *old_value, zval *new_value, zend_string *new_str); static inline ddog_CharSlice dd_zend_string_to_CharSlice(zend_string *str) { diff --git a/libdatadog b/libdatadog index cea1e44eddd..96d9a7bae10 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit cea1e44edddd9124f75d5095f31026904a1f58d8 +Subproject commit 96d9a7bae105449f16da96554b3a2e989b096027 diff --git a/src/DDTrace/OpenFeature/DataDogProvider.php b/src/DDTrace/OpenFeature/DataDogProvider.php index 257790a8b77..b42d928b999 100644 --- a/src/DDTrace/OpenFeature/DataDogProvider.php +++ b/src/DDTrace/OpenFeature/DataDogProvider.php @@ -4,10 +4,14 @@ namespace DDTrace\OpenFeature; -use DDTrace\FeatureFlags\Client as FeatureFlagsClient; use DDTrace\FeatureFlags\EvaluationDetails; use DDTrace\FeatureFlags\EvaluationErrorCode; use DDTrace\FeatureFlags\EvaluationReason; +use DDTrace\FeatureFlags\EvaluationType; +use DDTrace\FeatureFlags\Internal\Evaluator; +use DDTrace\FeatureFlags\Internal\Metric\EvaluationMetric; +use DDTrace\FeatureFlags\Internal\Metric\EvaluationMetricRecorder; +use DDTrace\FeatureFlags\Internal\NativeEvaluator; use DDTrace\Log\LoggerInterface; use DDTrace\Log\TriggerErrorLogger; use OpenFeature\implementation\provider\AbstractProvider; @@ -23,11 +27,37 @@ final class DataDogProvider extends AbstractProvider { protected static string $NAME = 'Datadog'; - private FeatureFlagsClient $client; + private Evaluator $evaluator; + private LoggerInterface $warningLogger; + private bool $warnedAboutNonProductionRuntime = false; + private EvaluationMetricRecorder $metricRecorder; public function __construct(?LoggerInterface $logger = null) { - $this->client = new FeatureFlagsClient($logger ?: new TriggerErrorLogger()); + // Native evaluation metrics are disabled here because OpenFeature owns + // the final provider outcome, including OF-level type mismatch mapping. + $this->evaluator = NativeEvaluator::create(false); + $this->warningLogger = $logger ?: new TriggerErrorLogger(); + $this->metricRecorder = EvaluationMetricRecorder::createDefault(); + } + + /** + * @internal Tests and Datadog-owned bridge adapters only. + */ + public static function createWithDependencies( + ?Evaluator $evaluator = null, + ?LoggerInterface $logger = null, + $metricRecorder = null + ): self { + $provider = new self($logger); + if ($evaluator !== null) { + $provider->evaluator = $evaluator; + } + if ($metricRecorder !== null) { + $provider->metricRecorder = new EvaluationMetricRecorder($metricRecorder); + } + + return $provider; } public function resolveBooleanValue( @@ -80,6 +110,8 @@ private function resolve( ?EvaluationContext $context ): ResolutionDetailsInterface { $details = $this->evaluate($flagKey, $expectedType, $defaultValue, $this->normalizeContext($context)); + $this->warnIfNonProductionRuntime($details); + $this->recordEvaluationMetric($flagKey, $details); $builder = (new ResolutionDetailsBuilder()) ->withValue($details->getValue()) @@ -100,6 +132,27 @@ private function resolve( return $builder->build(); } + private function recordEvaluationMetric(string $flagKey, EvaluationDetails $details): void + { + $this->metricRecorder->record(EvaluationMetric::create( + $flagKey, + $details->getVariant(), + $details->getReason(), + $details->getErrorCode(), + $this->allocationKey($details) + )); + } + + private function allocationKey(EvaluationDetails $details): ?string + { + $exposure = $details->getExposureData(); + if (!is_array($exposure) || !isset($exposure['allocationKey']) || !is_string($exposure['allocationKey'])) { + return null; + } + + return $exposure['allocationKey'] !== '' ? $exposure['allocationKey'] : null; + } + /** * @param bool|string|int|float|array $defaultValue * @param array $context @@ -110,14 +163,22 @@ private function evaluate( mixed $defaultValue, array $context ): EvaluationDetails { - return match ($expectedType) { - FlagValueType::BOOLEAN => $this->client->getBooleanDetails($flagKey, $defaultValue, $context), - FlagValueType::STRING => $this->client->getStringDetails($flagKey, $defaultValue, $context), - FlagValueType::INTEGER => $this->client->getIntegerDetails($flagKey, $defaultValue, $context), - FlagValueType::FLOAT => $this->client->getFloatDetails($flagKey, $defaultValue, $context), - FlagValueType::OBJECT => $this->client->getObjectDetails($flagKey, $defaultValue, $context), + $evaluationType = match ($expectedType) { + FlagValueType::BOOLEAN => EvaluationType::BOOLEAN, + FlagValueType::STRING => EvaluationType::STRING, + FlagValueType::INTEGER => EvaluationType::INTEGER, + FlagValueType::FLOAT => EvaluationType::FLOAT, + FlagValueType::OBJECT => EvaluationType::OBJECT, default => throw new \InvalidArgumentException('Unknown OpenFeature flag value type: ' . $expectedType), }; + + return $this->evaluator->evaluate( + $flagKey, + $evaluationType, + $defaultValue, + $context['targetingKey'] ?? null, + $context['attributes'] ?? [] + ); } /** @@ -142,6 +203,26 @@ private function normalizeContext(?EvaluationContext $context): array ]; } + private function warnIfNonProductionRuntime(EvaluationDetails $details): void + { + if ($this->warnedAboutNonProductionRuntime) { + return; + } + + $providerState = $details->getProviderState(); + if (!array_key_exists('productionRuntime', $providerState) || $providerState['productionRuntime'] !== false) { + return; + } + + $message = $details->getErrorMessage(); + if (!is_string($message) || $message === '') { + $message = 'Datadog-backed PHP OpenFeature evaluation is not fully enabled yet.'; + } + + $this->warningLogger->warning($message); + $this->warnedAboutNonProductionRuntime = true; + } + private function mapReason(string $reason): string { return match ($reason) { diff --git a/src/api/FeatureFlags/Internal/Metric/EvaluationMetric.php b/src/api/FeatureFlags/Internal/Metric/EvaluationMetric.php new file mode 100644 index 00000000000..8f3ecc91fdd --- /dev/null +++ b/src/api/FeatureFlags/Internal/Metric/EvaluationMetric.php @@ -0,0 +1,60 @@ +flagKey = $flagKey; + $this->variant = self::nullableString($variant); + $this->reason = self::nullableString($reason); + $this->errorCode = self::nullableString($errorCode); + $this->allocationKey = self::nullableString($allocationKey); + } + + public static function create($flagKey, $variant = null, $reason = null, $errorCode = null, $allocationKey = null) + { + return new self($flagKey, $variant, $reason, $errorCode, $allocationKey); + } + + public function getFlagKey() + { + return $this->flagKey; + } + + public function getVariant() + { + return $this->variant; + } + + public function getReason() + { + return $this->reason; + } + + public function getErrorCode() + { + return $this->errorCode; + } + + public function getAllocationKey() + { + return $this->allocationKey; + } + + private static function nullableString($value) + { + return is_string($value) && $value !== '' ? $value : null; + } +} diff --git a/src/api/FeatureFlags/Internal/Metric/EvaluationMetricRecorder.php b/src/api/FeatureFlags/Internal/Metric/EvaluationMetricRecorder.php new file mode 100644 index 00000000000..a1741c694d9 --- /dev/null +++ b/src/api/FeatureFlags/Internal/Metric/EvaluationMetricRecorder.php @@ -0,0 +1,75 @@ +recorder = $recorder; + } + + public static function createDefault() + { + return new self(self::nativeRecorder()); + } + + /** + * @internal Datadog-owned bridge adapters only. + * + * @return ?callable(EvaluationMetric): bool + */ + public static function nativeRecorder() + { + if (!self::isEnabled() || !function_exists('DDTrace\\Internal\\record_ffe_evaluation_metric')) { + return null; + } + + return static function (EvaluationMetric $metric) { + return \DDTrace\Internal\record_ffe_evaluation_metric( + $metric->getFlagKey(), + $metric->getVariant(), + $metric->getReason(), + $metric->getErrorCode(), + $metric->getAllocationKey() + ); + }; + } + + /** + * @internal Tests and Datadog-owned bridge adapters only. + */ + public function record(EvaluationMetric $metric) + { + if ($this->recorder === null) { + return false; + } + + try { + $recorder = $this->recorder; + return (bool) $recorder($metric); + } catch (\Throwable $throwable) { + return false; + } + } + + private static function isEnabled() + { + if (function_exists('dd_trace_env_config')) { + return \dd_trace_env_config('DD_METRICS_OTEL_ENABLED') === true; + } + + $value = getenv('DD_METRICS_OTEL_ENABLED'); + if ($value === false) { + return false; + } + + return in_array(strtolower((string) $value), array('1', 'true', 'yes', 'on'), true); + } +} diff --git a/src/api/FeatureFlags/Internal/NativeEvaluator.php b/src/api/FeatureFlags/Internal/NativeEvaluator.php index 483c86d0a1d..c77b76712f8 100644 --- a/src/api/FeatureFlags/Internal/NativeEvaluator.php +++ b/src/api/FeatureFlags/Internal/NativeEvaluator.php @@ -9,14 +9,16 @@ final class NativeEvaluator implements Evaluator const WARNING_MESSAGE = 'Datadog-backed PHP feature flag evaluation has no Remote Configuration data loaded for this request. Returning default values.'; private $mapper; + private $recordMetrics; - private function __construct($mapper = null) + private function __construct($mapper = null, $recordMetrics = true) { if ($mapper !== null && !$mapper instanceof ResultMapper) { throw new \InvalidArgumentException('Expected a ResultMapper instance'); } $this->mapper = $mapper ?: new ResultMapper(); + $this->recordMetrics = (bool) $recordMetrics; } public static function isAvailable() @@ -24,9 +26,9 @@ public static function isAvailable() return function_exists('DDTrace\\ffe_evaluate'); } - public static function create() + public static function create($recordMetrics = true) { - return self::isAvailable() ? new self() : new UnavailableEvaluator(); + return self::isAvailable() ? new self(null, $recordMetrics) : new UnavailableEvaluator(); } public function evaluate( @@ -40,7 +42,8 @@ public function evaluate( $flagKey, $this->typeId($expectedType), $targetingKey, - $this->normalizeAttributes($attributes) + $this->normalizeAttributes($attributes), + $this->recordMetrics ); if (is_array($rawResult) || is_object($rawResult)) { diff --git a/tests/OpenFeature/DataDogProviderTest.php b/tests/OpenFeature/DataDogProviderTest.php index c9844e6f3fe..45334ca32fc 100644 --- a/tests/OpenFeature/DataDogProviderTest.php +++ b/tests/OpenFeature/DataDogProviderTest.php @@ -4,7 +4,6 @@ namespace DDTrace\Tests\OpenFeature { -use DDTrace\FeatureFlags\Client as FeatureFlagsClient; use DDTrace\FeatureFlags\EvaluationDetails; use DDTrace\FeatureFlags\EvaluationErrorCode; use DDTrace\FeatureFlags\EvaluationReason; @@ -163,25 +162,7 @@ public function testTypeMismatchReturnsDefaultWithOpenFeatureError(): void private function providerForEvaluator(Evaluator $evaluator, ?LoggerInterface $logger = null): DataDogProvider { - $logger = $logger ?: new NullLogger(LogLevel::EMERGENCY); - $provider = new DataDogProvider($logger); - $client = $this->clientForEvaluator($evaluator, $logger); - - (function () use ($client): void { - $this->client = $client; - })->call($provider); - - return $provider; - } - - private function clientForEvaluator(Evaluator $evaluator, LoggerInterface $logger): FeatureFlagsClient - { - $client = new FeatureFlagsClient($logger); - (function () use ($evaluator): void { - $this->evaluator = $evaluator; - })->call($client); - - return $client; + return DataDogProvider::createWithDependencies($evaluator, $logger ?: new NullLogger(LogLevel::EMERGENCY)); } private function openFeatureClientFor(DataDogProvider $provider) diff --git a/tests/OpenFeature/OpenFeatureEvaluationMetricsTest.php b/tests/OpenFeature/OpenFeatureEvaluationMetricsTest.php new file mode 100644 index 00000000000..75ca6a619bc --- /dev/null +++ b/tests/OpenFeature/OpenFeatureEvaluationMetricsTest.php @@ -0,0 +1,245 @@ +setSuccess( + 'flag.allocation', + 'blue', + EvaluationReason::TARGETING_MATCH, + 'on', + ['allocationKey' => 'allocation-3baabb3c', 'doLog' => true] + ); + + $provider = DataDogProvider::createWithDependencies( + $evaluator, + null, + $recorder + ); + $client = $this->openFeatureClientFor($provider); + + $client->getStringValue('flag.allocation', 'red'); + + $calls = $recorder->calls(); + self::assertCount(1, $calls); + self::assertSame([ + 'flagKey' => 'flag.allocation', + 'variant' => 'on', + 'reason' => EvaluationReason::TARGETING_MATCH, + 'errorCode' => null, + 'allocationKey' => 'allocation-3baabb3c', + ], $calls[0]); + } + + public function testRecordsMetricThroughProviderOnTypeMismatch(): void + { + $recorder = new EvalMetricsRecordingRecorder(); + $evaluator = new OpenFeatureMetricEvaluator(); + // Provider resolves a type mismatch before returning to the OpenFeature + // SDK, so no separate PHP OpenFeature hook is needed for this case. + $evaluator->setSuccess('flag.mismatch', 'not-an-int', EvaluationReason::STATIC_REASON); + + $provider = DataDogProvider::createWithDependencies( + $evaluator, + null, + $recorder + ); + $client = $this->openFeatureClientFor($provider); + + $details = $client->getIntegerDetails('flag.mismatch', 7); + self::assertSame(EvaluationErrorCode::TYPE_MISMATCH, $details->getError()->getResolutionErrorCode()->getValue()); + + $calls = $recorder->calls(); + + self::assertCount(1, $calls); + self::assertSame('flag.mismatch', $calls[0]['flagKey']); + self::assertSame(EvaluationReason::ERROR, $calls[0]['reason']); + self::assertSame(EvaluationErrorCode::TYPE_MISMATCH, $calls[0]['errorCode']); + } + + public function testOpenFeaturePathRecordsOncePerEvaluation(): void + { + $recorder = new EvalMetricsRecordingRecorder(); + $evaluator = new OpenFeatureMetricEvaluator(); + $evaluator->setSuccess('flag.basic', 'value', EvaluationReason::STATIC_REASON, 'v1'); + + $provider = DataDogProvider::createWithDependencies( + $evaluator, + null, + $recorder + ); + $client = $this->openFeatureClientFor($provider); + + $client->getStringValue('flag.basic', 'default'); + $client->getStringValue('flag.basic', 'default'); + $client->getStringValue('flag.basic', 'default'); + + // Three evaluations, exactly three recorder calls. Aggregation happens + // natively in the sidecar, not in PHP. + self::assertCount(3, $recorder->calls()); + } + + public function testSupportsAllFlagValueTypes(): void + { + $recorder = new EvalMetricsRecordingRecorder(); + $evaluator = new OpenFeatureMetricEvaluator(); + $evaluator + ->setSuccess('b', true, EvaluationReason::STATIC_REASON) + ->setSuccess('s', 'x', EvaluationReason::STATIC_REASON) + ->setSuccess('i', 1, EvaluationReason::STATIC_REASON) + ->setSuccess('f', 1.5, EvaluationReason::STATIC_REASON) + ->setSuccess('o', ['k' => 'v'], EvaluationReason::STATIC_REASON); + + $provider = DataDogProvider::createWithDependencies( + $evaluator, + null, + $recorder + ); + $client = $this->openFeatureClientFor($provider); + + $client->getBooleanValue('b', false); + $client->getStringValue('s', ''); + $client->getIntegerValue('i', 0); + $client->getFloatValue('f', 0.0); + $client->getObjectValue('o', []); + + self::assertCount(5, $recorder->calls(), 'Recorder records for every supported flag value type'); + } + + private function openFeatureClientFor(DataDogProvider $provider) + { + $api = OpenFeatureAPI::getInstance(); + $api->setProvider($provider); + return $api->getClient('datadog-evalmetrics-test'); + } +} + +final class EvalMetricsRecordingRecorder +{ + private $calls = array(); + + public function __invoke(EvaluationMetric $metric) + { + $this->calls[] = array( + 'flagKey' => $metric->getFlagKey(), + 'variant' => $metric->getVariant(), + 'reason' => $metric->getReason(), + 'errorCode' => $metric->getErrorCode(), + 'allocationKey' => $metric->getAllocationKey(), + ); + return true; + } + + public function calls() + { + return $this->calls; + } +} + +final class OpenFeatureMetricEvaluator implements Evaluator +{ + /** @var array */ + private array $details = []; + + public function setSuccess( + string $flagKey, + mixed $value, + string $reason = EvaluationReason::STATIC_REASON, + ?string $variant = null, + array $exposureData = [] + ): self { + $this->details[$flagKey] = new EvaluationDetails( + $value, + $this->typeForValue($value), + $reason, + $variant, + null, + null, + [], + $exposureData + ); + return $this; + } + + public function evaluate($flagKey, $expectedType, $defaultValue, $targetingKey = null, array $attributes = []) + { + if (!array_key_exists($flagKey, $this->details)) { + return new EvaluationDetails( + $defaultValue, + $expectedType, + EvaluationReason::ERROR, + null, + EvaluationErrorCode::FLAG_NOT_FOUND, + 'Feature flag "' . $flagKey . '" was not found' + ); + } + + $details = $this->details[$flagKey]; + if (!$this->matchesExpectedType($details->getValue(), $expectedType)) { + return new EvaluationDetails( + $defaultValue, + $expectedType, + EvaluationReason::ERROR, + null, + EvaluationErrorCode::TYPE_MISMATCH, + 'Expected ' . $expectedType . ' flag value' + ); + } + return $details; + } + + private function typeForValue($value): string + { + if (is_bool($value)) { + return EvaluationType::BOOLEAN; + } + if (is_int($value)) { + return EvaluationType::INTEGER; + } + if (is_float($value)) { + return EvaluationType::FLOAT; + } + if (is_array($value)) { + return EvaluationType::OBJECT; + } + return EvaluationType::STRING; + } + + private function matchesExpectedType($value, string $expectedType): bool + { + switch ($expectedType) { + case EvaluationType::BOOLEAN: return is_bool($value); + case EvaluationType::STRING: return is_string($value); + case EvaluationType::INTEGER: return is_int($value); + case EvaluationType::FLOAT: return is_int($value) || is_float($value); + case EvaluationType::OBJECT: return is_array($value); + } + return false; + } +} + +} diff --git a/tests/api/Unit/FeatureFlags/EvaluationMetricRecorderTest.php b/tests/api/Unit/FeatureFlags/EvaluationMetricRecorderTest.php new file mode 100644 index 00000000000..0963129b46d --- /dev/null +++ b/tests/api/Unit/FeatureFlags/EvaluationMetricRecorderTest.php @@ -0,0 +1,82 @@ +assertTrue($metricRecorder->record(EvaluationMetric::create( + 'checkout.enabled', + 'treatment', + EvaluationReason::SPLIT, + null, + 'allocation-a' + ))); + + $this->assertSame(array(array( + 'flagKey' => 'checkout.enabled', + 'variant' => 'treatment', + 'reason' => EvaluationReason::SPLIT, + 'errorCode' => null, + 'allocationKey' => 'allocation-a', + )), $recorder->calls()); + } + + public function testRecorderNoopsWithoutCallable() + { + $metricRecorder = new EvaluationMetricRecorder(null); + + $this->assertFalse($metricRecorder->record(EvaluationMetric::create('flag.noop'))); + } + + public function testRecorderExceptionDoesNotEscape() + { + $metricRecorder = new EvaluationMetricRecorder(new ThrowingEvaluationMetricRecorder()); + + $this->assertFalse($metricRecorder->record(EvaluationMetric::create( + 'flag.throwing', + 'on', + EvaluationReason::SPLIT + ))); + } +} + +final class RecordingEvaluationMetricRecorder +{ + private $calls = array(); + + public function __invoke(EvaluationMetric $metric) + { + $this->calls[] = array( + 'flagKey' => $metric->getFlagKey(), + 'variant' => $metric->getVariant(), + 'reason' => $metric->getReason(), + 'errorCode' => $metric->getErrorCode(), + 'allocationKey' => $metric->getAllocationKey(), + ); + + return true; + } + + public function calls() + { + return $this->calls; + } +} + +final class ThrowingEvaluationMetricRecorder +{ + public function __invoke() + { + throw new \RuntimeException('metric recorder failed'); + } +} diff --git a/tests/ext/ffe/evaluation_metrics_native.phpt b/tests/ext/ffe/evaluation_metrics_native.phpt new file mode 100644 index 00000000000..572ea09fbbe --- /dev/null +++ b/tests/ext/ffe/evaluation_metrics_native.phpt @@ -0,0 +1,57 @@ +--TEST-- +FFE evaluation metrics use native recorder +--ENV-- +DD_METRICS_OTEL_ENABLED=true +--FILE-- + +--EXPECT-- +native_recorder_exists=true +native_flush_exists=true +old_metrics_forwarder_exists=false +old_exposure_forwarder_exists=false +recorded=true +load=true +evaluation_without_native_metric={"valueJson":"\"blue\"","variant":"blue","allocationKey":"alloc-string","reason":0,"errorCode":0,"doLog":true,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +missing_flag_without_native_metric={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":3,"doLog":false,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} diff --git a/tests/ext/ffe/native_bridge_evaluate.phpt b/tests/ext/ffe/native_bridge_evaluate.phpt index a136623d27f..bb262f7ffa7 100644 --- a/tests/ext/ffe/native_bridge_evaluate.phpt +++ b/tests/ext/ffe/native_bridge_evaluate.phpt @@ -134,6 +134,6 @@ object_success_value={"enabled":true,"threshold":2} object_success_metadata={"variant":"json-a","allocation_key":"alloc-json","reason":0,"error_code":0,"do_log":true} numeric_attribute_key={"valueJson":"\"numeric-attribute-name\"","variant":"numeric-key","allocationKey":"alloc-numeric-attribute","reason":2,"errorCode":0,"doLog":true,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} empty_targeting_key={"valueJson":"\"empty-targeting-key\"","variant":"empty-target","allocationKey":"alloc-empty-targeting-key","reason":3,"errorCode":0,"doLog":true,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} -missing={"valueJson":"null","variant":null,"allocationKey":null,"reason":1,"errorCode":3,"doLog":false,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} +missing={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":3,"doLog":false,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} type_mismatch={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":1,"doLog":false,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} parse_error={"valueJson":"null","variant":null,"allocationKey":null,"reason":5,"errorCode":2,"doLog":false,"providerState":[],"errorMessage":null,"hasConfig":null,"configVersion":null} diff --git a/tests/internal-api-stress-test.php b/tests/internal-api-stress-test.php index 8f881d821ec..d696d0226af 100644 --- a/tests/internal-api-stress-test.php +++ b/tests/internal-api-stress-test.php @@ -133,6 +133,7 @@ function ($hook = null) { $minFunctionArgs = [ 'DDTrace\ffe_evaluate' => 4, + 'DDTrace\Internal\record_ffe_evaluation_metric' => 5, ]; function call_function(ReflectionFunction $function) diff --git a/tooling/bin/build-debug-artifact b/tooling/bin/build-debug-artifact index 135f5392aac..3836a58a94d 100755 --- a/tooling/bin/build-debug-artifact +++ b/tooling/bin/build-debug-artifact @@ -133,7 +133,10 @@ fi # ─── Build cache management ─────────────────────────────────────────────────── CACHE_VOLUME="ddtrace-build-cache" -CACHE_TAG="${libc}-${arch}-${php_version}-${thread_safety}" +# The persistent build cache contains copied source files under tmp/build_extension. +# Keep sibling worktrees from reusing stale copied headers for the same PHP target. +CACHE_SOURCE_KEY="$(printf '%s' "$REPO_ROOT" | cksum | awk '{print $1}')" +CACHE_TAG="${libc}-${arch}-${php_version}-${thread_safety}-${CACHE_SOURCE_KEY}" CACHE_TAG_LABEL="ddtrace.build.target" if docker volume inspect "$CACHE_VOLUME" &>/dev/null; then @@ -200,8 +203,8 @@ fi echo "Building ddtrace ${php_version} ${thread_safety} [${libc}/${arch}]..." echo "Image: ${DOCKER_IMAGE}" -TMP_OUT=$(mktemp -d) -TMP_PKG=$(mktemp -d) +TMP_OUT=$(mktemp -d "${OUTPUT_DIR%/}/.build-debug-artifact-out.XXXXXX") +TMP_PKG=$(mktemp -d "${OUTPUT_DIR%/}/.build-debug-artifact-pkg.XXXXXX") trap 'rm -rf "$TMP_OUT" "$TMP_PKG"' EXIT # ─── Build script construction ────────────────────────────────────────────────