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
2 changes: 2 additions & 0 deletions components-rs/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,8 @@ typedef struct ddog_FfeResult {
_zend_string * allocation_key;
int32_t reason;
int32_t error_code;
int64_t serial_id;
bool has_serial_id;
bool do_log;
bool valid;
} ddog_FfeResult;
Expand Down
14 changes: 14 additions & 0 deletions components-rs/ffe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ pub struct FfeResult {
pub allocation_key: MaybeOwnedZendString,
pub reason: i32,
pub error_code: i32,
// serial_id is the selected split's serial id, carried for APM span
// enrichment (ffe_flags_enc). The source field is Option<i32>; since the
// C ABI cannot represent Option<i32> as a plain field, we surface the
// presence separately via has_serial_id. Consumers MUST gate on
// has_serial_id (the Pattern B "missing variant => default" semantic) and
// never treat serial_id == 0 as "absent".
pub serial_id: i64,
pub has_serial_id: bool,
pub do_log: bool,
pub valid: bool,
}
Expand Down Expand Up @@ -220,6 +228,8 @@ fn result_from_assignment(assignment: Result<ffe::Assignment, EvaluationError>)
AssignmentReason::Default => REASON_DEFAULT,
},
error_code: ERROR_NONE,
serial_id: assignment.serial_id.unwrap_or(0) as i64,
has_serial_id: assignment.serial_id.is_some(),
do_log: assignment.do_log,
valid: true,
}
Expand All @@ -244,6 +254,8 @@ fn result_from_assignment(assignment: Result<ffe::Assignment, EvaluationError>)
allocation_key: None,
reason,
error_code,
serial_id: 0,
has_serial_id: false,
do_log: false,
valid: true,
}
Expand All @@ -258,6 +270,8 @@ fn invalid_result() -> FfeResult {
allocation_key: None,
reason: REASON_ERROR,
error_code: ERROR_GENERAL,
serial_id: 0,
has_serial_id: false,
do_log: false,
valid: false,
}
Expand Down
9 changes: 9 additions & 0 deletions config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,15 @@ if test "$PHP_DDTRACE" != "no"; then

DATADOG_EXTENSION_FLAGS="-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 -Wall -std=gnu11"

dnl Test-only internal helpers exposed via dd_trace_internal_fn (e.g.
dnl await_ffe_config, which actively pumps Remote Config and can block for
dnl seconds). Defined for the standard CI/test/package builds the
dnl system-tests and ffe-dogfooding harnesses run against; a hardened
dnl production build can drop -DDD_TEST_HELPERS to compile these heavyweight
dnl test surfaces out of the dispatcher entirely so they have no production
dnl effect.
DATADOG_EXTENSION_FLAGS="$DATADOG_EXTENSION_FLAGS -DDD_TEST_HELPERS=1"

ALL_DATADOG_SOURCES=" \
$DATADOG_PHP_SOURCES \
$ZAI_SOURCES \
Expand Down
3 changes: 2 additions & 1 deletion ext/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ enum datadog_sidecar_connection_mode {
CONFIG(DOUBLE, DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS, "5.0", .ini_change = zai_config_system_ini_change) \
CONFIG(BOOL, DD_REMOTE_CONFIG_ENABLED, "true", .ini_change = zai_config_system_ini_change) \
CONFIG(INT, DD_TRACE_RETRY_INTERVAL, "100", .ini_change = zai_config_system_ini_change) \
CONFIG(BOOL, DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "true")
CONFIG(BOOL, DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, "true") \
CONFIG(BOOL, DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED, "false")

#define DD_CONFIGURATIONS_ONLY
#ifdef DDTRACE
Expand Down
7 changes: 7 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,13 @@
"default": "false"
}
],
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED": [
{
"implementation": "A",
"type": "boolean",
"default": "false"
}
],
"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED": [
{
"implementation": "B",
Expand Down
35 changes: 34 additions & 1 deletion src/DDTrace/OpenFeature/DataDogProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use DDTrace\FeatureFlags\Internal\Metric\EvaluationMetric;
use DDTrace\FeatureFlags\Internal\Metric\EvaluationMetricRecorder;
use DDTrace\FeatureFlags\Internal\NativeEvaluator;
use DDTrace\FeatureFlags\SpanEnrichmentBinder;
use DDTrace\Log\LoggerInterface;
use DDTrace\Log\TriggerErrorLogger;
use OpenFeature\implementation\provider\AbstractProvider;
Expand All @@ -33,6 +34,16 @@ final class DataDogProvider extends AbstractProvider
private LoggerInterface $warningLogger;
private bool $warnedAboutNonProductionRuntime = false;
private EvaluationMetricRecorder $metricRecorder;
/**
* Gate-gated adapter to the SHARED request-scoped span-enrichment
* accumulator (SpanEnrichmentRegistry). Null unless the experimental
* span-enrichment gate is on (DG-005: gate off => no binder, no per-span
* overhead). Routing through the shared registry rather than a per-provider
* accumulator is what lets multiple providers / native clients / mixed
* evaluations under one root span AGGREGATE rather than OVERWRITE one
* another's tags (PR review blocker).
*/
private ?SpanEnrichmentBinder $spanEnrichmentBinder = null;

public function __construct(?LoggerInterface $logger = null)
{
Expand All @@ -41,6 +52,14 @@ public function __construct(?LoggerInterface $logger = null)
$this->evaluator = NativeEvaluator::create(false);
$this->warningLogger = $logger ?: new TriggerErrorLogger();
$this->metricRecorder = EvaluationMetricRecorder::createDefault();

// DG-005: only construct the binder when the experimental span-enrichment
// feature is opted in. With the gate off we construct nothing and never
// accumulate/stage anything, so the close-span write path stays a cheap
// no-op and there is no idle per-span overhead.
if (SpanEnrichmentBinder::gateEnabled()) {
$this->spanEnrichmentBinder = new SpanEnrichmentBinder();
}
}

/**
Expand Down Expand Up @@ -111,12 +130,26 @@ private function resolve(
mixed $defaultValue,
?EvaluationContext $context
): ResolutionDetailsInterface {
$details = $this->evaluate($flagKey, $expectedType, $defaultValue, $this->normalizeContext($context));
$normalizedContext = $this->normalizeContext($context);
$details = $this->evaluate($flagKey, $expectedType, $defaultValue, $normalizedContext);
$this->warnIfNonProductionRuntime($details);
// The PHP OpenFeature SDK does not pass ResolutionDetails to finally
// hooks, so PHP records metrics here after native evaluation has the
// final provider result.
$this->recordEvaluationMetric($flagKey, $details);
// DG-004: PHP has no finally hook with ResolutionDetails, so APM span
// enrichment is accumulated INLINE here, on the same code path and from
// the same $details, immediately after the metrics hook. Skipped entirely
// with the gate off (no binder was constructed); when on, it feeds the
// SHARED registry so this provider's tags aggregate with any other
// evaluation path active on the same root span.
if ($this->spanEnrichmentBinder !== null) {
$this->spanEnrichmentBinder->accumulate(
$flagKey,
$details,
$normalizedContext['targetingKey'] ?? null
);
}

$builder = (new ResolutionDetailsBuilder())
->withValue($details->getValue())
Expand Down
22 changes: 22 additions & 0 deletions src/api/FeatureFlags/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ final class Client
private $evaluator;
private $logger;
private $warnedAboutNonProductionRuntime = false;
/** @var SpanEnrichmentBinder|null Null unless the span-enrichment gate is on. */
private $spanEnrichmentBinder = null;

public function __construct($logger = null)
{
Expand All @@ -20,6 +22,19 @@ public function __construct($logger = null)

$this->evaluator = NativeEvaluator::create();
$this->logger = $logger ?: new TriggerErrorLogger();
// DG-004/DG-005: the native Client does NOT go through the OpenFeature
// provider, so APM span enrichment is bound here on the same evaluation
// path. To stay fully inert with the gate off (PR review should-fix:
// gate-off must allocate no binder and read no per-evaluation config),
// construct the binder ONLY when the experimental span-enrichment gate
// is on; when it is off $spanEnrichmentBinder stays null and evaluate()
// skips the enrichment call entirely.
require_once __DIR__ . '/SpanEnrichmentBinder.php';
if (SpanEnrichmentBinder::gateEnabled()) {
require_once __DIR__ . '/SpanEnrichmentAccumulator.php';
require_once __DIR__ . '/SpanEnrichmentRegistry.php';
$this->spanEnrichmentBinder = new SpanEnrichmentBinder();
}
}

/**
Expand Down Expand Up @@ -116,6 +131,13 @@ private function evaluate($flagKey, $expectedType, $defaultValue, array $context
);

$this->warnIfNonProductionRuntime($details);
// APM span enrichment. Skipped entirely with the gate off (no binder was
// constructed). When on, accumulates from the same EvaluationDetails the
// caller receives into the shared request-scoped registry; the native
// close-span path writes the staged ffe_* tags onto the root span.
if ($this->spanEnrichmentBinder !== null) {
$this->spanEnrichmentBinder->accumulate($flagKey, $details, $targetingKey);
}

return $details;
}
Expand Down
9 changes: 9 additions & 0 deletions src/api/FeatureFlags/Internal/ResultMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,15 @@ private function exposureData($rawResult)
$exposureData['doLog'] = (bool) $this->read($rawResult, array('do_log', 'doLog'), false);
}

// serialId is the selected split's serial id, surfaced from the native
// bridge for APM span enrichment. It is only present when the native
// result actually carried one; a null/absent value must be left out so
// downstream consumers can treat "no serialId" as a runtime default.
$serialId = $this->read($rawResult, array('serial_id', 'serialId'), null);
if ($serialId !== null) {
$exposureData['serialId'] = (int) $serialId;
}

return $exposureData;
}

Expand Down
Loading
Loading