Skip to content
Merged
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
24 changes: 12 additions & 12 deletions src/DDTrace/OpenFeature/DataDogProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
use DDTrace\FeatureFlags\Internal\Metric\EvaluationMetric;
use DDTrace\FeatureFlags\Internal\Metric\EvaluationMetricRecorder;
use DDTrace\FeatureFlags\Internal\NativeEvaluator;
use DDTrace\Log\Logger as GlobalLogger;
use DDTrace\Log\LoggerInterface;
use DDTrace\Log\TriggerErrorLogger;
use DDTrace\Log\NonThrowingLogger;
use OpenFeature\implementation\provider\AbstractProvider;
use OpenFeature\implementation\provider\ResolutionDetailsBuilder;
use OpenFeature\implementation\provider\ResolutionError;
Expand All @@ -30,16 +31,16 @@ final class DataDogProvider extends AbstractProvider
protected static string $NAME = 'Datadog';

private Evaluator $evaluator;
private LoggerInterface $warningLogger;
private bool $warnedAboutNonProductionRuntime = false;
private LoggerInterface $datadogLogger;
private bool $warnedAboutRuntimeNotReady = false;
private EvaluationMetricRecorder $metricRecorder;

public function __construct(?LoggerInterface $logger = null)
{
// 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->datadogLogger = new NonThrowingLogger($logger ?: GlobalLogger::get());
$this->metricRecorder = EvaluationMetricRecorder::createDefault();
}

Expand Down Expand Up @@ -113,7 +114,7 @@ private function resolve(
): ResolutionDetailsInterface {
$normalizedContext = $this->normalizeContext($context);
$details = $this->evaluate($flagKey, $expectedType, $defaultValue, $normalizedContext);
$this->warnIfNonProductionRuntime($details);
$this->warnIfRuntimeNotReady($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.
Expand Down Expand Up @@ -214,24 +215,23 @@ private function normalizeContext(?EvaluationContext $context): array
];
}

private function warnIfNonProductionRuntime(EvaluationDetails $details): void
private function warnIfRuntimeNotReady(EvaluationDetails $details): void
{
if ($this->warnedAboutNonProductionRuntime) {
if ($this->warnedAboutRuntimeNotReady) {
return;
}

$providerState = $details->getProviderState();
if (!array_key_exists('productionRuntime', $providerState) || $providerState['productionRuntime'] !== false) {
if ($details->getErrorCode() !== EvaluationErrorCode::PROVIDER_NOT_READY) {
return;
}

$message = $details->getErrorMessage();
if (!is_string($message) || $message === '') {
$message = 'Datadog-backed PHP OpenFeature evaluation is not fully enabled yet.';
$message = 'Datadog-backed PHP OpenFeature evaluation is not ready. Returning the default value.';
}

$this->warningLogger->warning($message);
$this->warnedAboutNonProductionRuntime = true;
$this->warnedAboutRuntimeNotReady = true;
$this->datadogLogger->warning($message);
}

private function mapReason(string $reason): string
Expand Down
21 changes: 11 additions & 10 deletions src/api/FeatureFlags/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
namespace DDTrace\FeatureFlags;

use DDTrace\FeatureFlags\Internal\NativeEvaluator;
use DDTrace\Log\Logger as GlobalLogger;
use DDTrace\Log\LoggerInterface;
use DDTrace\Log\TriggerErrorLogger;
use DDTrace\Log\NonThrowingLogger;

final class Client
{
private $evaluator;
/** @var LoggerInterface */
private $logger;
private $warnedAboutNonProductionRuntime = false;
private $warnedAboutRuntimeNotReady = false;

public function __construct($logger = null)
{
Expand All @@ -19,7 +21,7 @@ public function __construct($logger = null)
}

$this->evaluator = NativeEvaluator::create();
$this->logger = $logger ?: new TriggerErrorLogger();
$this->logger = new NonThrowingLogger($logger ?: GlobalLogger::get());

@pavlokhrebto pavlokhrebto Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Behavior change worth calling out: Logger::get() returns NullLogger(EMERGENCY) unless DD_TRACE_DEBUG is set, so the not-ready warning is now effectively silent by default (previously TriggerErrorLogger always emitted E_USER_WARNING).

As far as i remember this is intended, but please note in the PR that not-ready state is now surfaced via exposures/metrics rather than a userland warning. Same applies to the equivalent line in DataDogProvider.

}

/**
Expand Down Expand Up @@ -115,7 +117,7 @@ private function evaluate($flagKey, $expectedType, $defaultValue, array $context
$attributes
);

$this->warnIfNonProductionRuntime($details);
$this->warnIfRuntimeNotReady($details);

return $details;
}
Expand All @@ -139,24 +141,23 @@ private function normalizeContext(array $context)
return array($targetingKey, $attributes);
}

private function warnIfNonProductionRuntime(EvaluationDetails $details)
private function warnIfRuntimeNotReady(EvaluationDetails $details)
{
if ($this->warnedAboutNonProductionRuntime) {
if ($this->warnedAboutRuntimeNotReady) {
return;
}

$providerState = $details->getProviderState();
if (!array_key_exists('productionRuntime', $providerState) || $providerState['productionRuntime'] !== false) {
if ($details->getErrorCode() !== EvaluationErrorCode::PROVIDER_NOT_READY) {
return;
}

$message = $details->getErrorMessage();
if (!is_string($message) || $message === '') {
$message = 'Datadog-backed PHP feature flag evaluation is running without exposure and metric reporting in this milestone.';
$message = 'Datadog-backed PHP feature flag evaluation is not ready. Returning the default value.';
}

$this->warnedAboutRuntimeNotReady = true;
$this->logger->warning($message);
$this->warnedAboutNonProductionRuntime = true;
}

private function expectFlagKey($flagKey)
Expand Down
4 changes: 2 additions & 2 deletions src/api/FeatureFlags/Internal/NativeEvaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ private function withProviderState($rawResult)
'ready' => $hasConfig,
'hasConfig' => $hasConfig,
'configVersion' => $configVersion,
'productionRuntime' => false,
'productionRuntime' => true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

After this PR nothing reads productionRuntime for logic (the old warnIfNonProductionRuntime checks are removed); it is only surfaced as metadata / asserted in the .phpt. Two small things: (a) consider => $hasConfig instead of a constant true — a config-missing result (reason='configuration_missing' -> PROVIDER_NOT_READY) currently reports productionRuntime=true alongside a not-ready error, which is contradictory; (b) a one-line comment that it is informational-only would help the next reader.

'mode' => 'native_remote_config',
'reason' => $hasConfig ? 'metrics_delivery_pending' : 'configuration_missing',
'reason' => $hasConfig ? 'ready' : 'configuration_missing',
);

if (is_array($rawResult)) {
Expand Down
52 changes: 52 additions & 0 deletions src/api/Log/NonThrowingLogger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace DDTrace\Log;

/**
* Prevents logger failures from escaping into application code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice boundary. Note it swallows all Throwables silently on every method, so a genuinely broken/misconfigured underlying logger becomes invisible. Acceptable trade-off here (and I would not add an error_log fallback — recursion risk), just flagging the blind spot.

*
* @internal
*/
final class NonThrowingLogger implements LoggerInterface
{
/** @var LoggerInterface */
private $logger;

public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}

public function debug($message, array $context = array())
{
try {
$this->logger->debug($message, $context);
} catch (\Throwable $ignored) {
}
}

public function warning($message, array $context = array())
{
try {
$this->logger->warning($message, $context);
} catch (\Throwable $ignored) {
}
}

public function error($message, array $context = array())
{
try {
$this->logger->error($message, $context);
} catch (\Throwable $ignored) {
}
}

public function isLevelActive($level)
{
try {
return (bool) $this->logger->isLevelActive($level);
} catch (\Throwable $ignored) {
return false;
}
}
}
37 changes: 0 additions & 37 deletions src/api/Log/TriggerErrorLogger.php

This file was deleted.

2 changes: 1 addition & 1 deletion src/bridge/_files_tracer.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
__DIR__ . '/../api/Log/DatadogLogger.php',
__DIR__ . '/../api/Log/ErrorLogLogger.php',
__DIR__ . '/../api/Log/NullLogger.php',
__DIR__ . '/../api/Log/TriggerErrorLogger.php',
__DIR__ . '/../api/Log/NonThrowingLogger.php',
__DIR__ . '/../api/Log/LoggingTrait.php',
__DIR__ . '/../api/StartSpanOptions.php',
__DIR__ . '/../DDTrace/SpanContext.php',
Expand Down
67 changes: 59 additions & 8 deletions tests/OpenFeature/DataDogProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use DDTrace\FeatureFlags\EvaluationReason;
use DDTrace\FeatureFlags\EvaluationType;
use DDTrace\FeatureFlags\Internal\Evaluator;
use DDTrace\FeatureFlags\Internal\NativeEvaluator;
use DDTrace\FeatureFlags\Internal\UnavailableEvaluator;
use DDTrace\Log\LoggerInterface;
use DDTrace\Log\LogLevel;
Expand Down Expand Up @@ -101,7 +100,7 @@ public function testEvaluationContextIsNormalizedForDatadogClient(): void
public function testUnavailableRuntimeReturnsDefaultDetailsAndOneWarning(): void
{
$logger = new OpenFeatureRecordingLogger();
$client = $this->openFeatureClientFor(new DataDogProvider($logger));
$client = $this->openFeatureClientFor($this->providerForEvaluator(new UnavailableEvaluator(), $logger));

$value = $client->getBooleanValue('checkout.enabled', true);
$details = $client->getStringDetails('checkout.copy', 'fallback');
Expand All @@ -110,10 +109,7 @@ public function testUnavailableRuntimeReturnsDefaultDetailsAndOneWarning(): void
self::assertSame('fallback', $details->getValue());
self::assertSame(Reason::ERROR, $details->getReason());
self::assertSame(ErrorCode::PROVIDER_NOT_READY()->getValue(), $details->getError()->getResolutionErrorCode()->getValue());
self::assertContains($details->getError()->getResolutionErrorMessage(), [
NativeEvaluator::WARNING_MESSAGE,
UnavailableEvaluator::WARNING_MESSAGE,
]);
self::assertSame(UnavailableEvaluator::WARNING_MESSAGE, $details->getError()->getResolutionErrorMessage());
self::assertSame([$details->getError()->getResolutionErrorMessage()], $logger->warnings());
}

Expand All @@ -133,6 +129,26 @@ public function testProviderWarningIsEmittedOncePerProvider(): void
self::assertSame(['temporary unavailable'], $logger->warnings());
}

public function testThrowingLoggerDoesNotChangeProviderNotReadyEvaluation(): void
{
$evaluator = new OpenFeatureTestEvaluator();
$evaluator->setUnavailable('preview.flag', false, 'runtime unavailable');
$logger = new OpenFeatureThrowingLogger();
$client = $this->openFeatureClientFor($this->providerForEvaluator($evaluator, $logger));

$firstDetails = $client->getBooleanDetails('preview.flag', false);
$secondDetails = $client->getBooleanDetails('preview.flag', false);

self::assertFalse($firstDetails->getValue());
self::assertSame(Reason::ERROR, $firstDetails->getReason());
self::assertSame(
ErrorCode::PROVIDER_NOT_READY()->getValue(),
$firstDetails->getError()->getResolutionErrorCode()->getValue()
);
self::assertFalse($secondDetails->getValue());
self::assertSame(1, $logger->warningCount());
}

public function testProviderErrorsMapToOpenFeatureDetails(): void
{
$evaluator = new OpenFeatureTestEvaluator();
Expand Down Expand Up @@ -186,13 +202,19 @@ public function setSuccess(
string $flagKey,
mixed $value,
string $reason = EvaluationReason::STATIC_REASON,
?string $variant = null
?string $variant = null,
array $providerState = []
): self {
$this->details[$flagKey] = new EvaluationDetails(
$value,
$this->typeForValue($value),
$reason,
$variant
$variant,
null,
null,
[],
[],
$providerState
);

return $this;
Expand Down Expand Up @@ -336,4 +358,33 @@ public function warnings(): array
return $this->warnings;
}
}

final class OpenFeatureThrowingLogger implements LoggerInterface
{
private int $warningCount = 0;

public function debug($message, array $context = [])
{
}

public function warning($message, array $context = [])
{
++$this->warningCount;
throw new \ErrorException($message);
}

public function error($message, array $context = [])
{
}

public function isLevelActive($level)
{
return true;
}

public function warningCount(): int
{
return $this->warningCount;
}
}
}
Loading
Loading