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
36 changes: 36 additions & 0 deletions ddtrace/internal/settings/openfeature.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,43 @@
OpenFeature configuration settings.
"""

from typing import Callable
from typing import Optional

from ddtrace.internal.logger import get_logger
from ddtrace.internal.settings._core import DDConfig


log = get_logger(__name__)


# AIDEV-NOTE: numeric settings here parse leniently on purpose. This class is instantiated at
# module scope (see the bottom of this file), so letting envier raise on an unparsable value
# turns it into an ImportError for ddtrace.openfeature and takes the whole application down at
# startup rather than degrading one setting. dd-trace-java substitutes the default in the same
# situation; match that.
def _lenient_int(env_name: str, default: int) -> Callable[[str], int]:
def parse(raw: str) -> int:
try:
return int(raw)
except ValueError:
log.warning("Invalid value for %s: %r is not an integer; using the default", env_name, raw)
return default

return parse

Comment thread
pavlokhrebto marked this conversation as resolved.

def _lenient_float(env_name: str, default: float) -> Callable[[str], float]:
def parse(raw: str) -> float:
try:
return float(raw)
except ValueError:
log.warning("Invalid value for %s: %r is not a number; using the default", env_name, raw)
return default

return parse


class OpenFeatureConfig(DDConfig):
"""
Configuration for OpenFeature provider and exposure reporting.
Expand Down Expand Up @@ -48,6 +80,7 @@ class OpenFeatureConfig(DDConfig):
float,
"DD_FFE_INTAKE_HEARTBEAT_INTERVAL",
default=1.0,
parser=_lenient_float("DD_FFE_INTAKE_HEARTBEAT_INTERVAL", 1.0),
)

# Provider initialization timeout in milliseconds. Controls how long initialize()
Expand All @@ -61,6 +94,7 @@ class OpenFeatureConfig(DDConfig):
int,
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS",
default=10000,
parser=_lenient_int("DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS", 10000),
)

# Stable Feature Flagging kill switch. When False, the provider is disabled
Expand Down Expand Up @@ -96,13 +130,15 @@ class OpenFeatureConfig(DDConfig):
int,
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS",
default=30,
parser=_lenient_int("DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS", 30),
)

# Agentless UFC per-request timeout in seconds.
configuration_source_agentless_request_timeout_seconds = DDConfig.var(
int,
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS",
default=5,
parser=_lenient_int("DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS", 5),
)

_openfeature_config_keys = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
fixes:
- |
openfeature: This fix resolves an issue where an invalid value for a numeric Feature
Flagging environment variable raised an exception while importing ``ddtrace.openfeature``,
preventing the application from starting. Such values are now logged and the documented
default is used instead.
37 changes: 37 additions & 0 deletions tests/openfeature/test_source_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,40 @@ def test_create_invalid_endpoint_returns_none(bad_url):
cfg = _config(DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL=bad_url)
with override_global_config({"_dd_api_key": "secret"}):
assert create_agentless_source(cfg, lambda _: None) is None


# ---------------------------------------------------------------------------
# Numeric settings degrade instead of breaking the import
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
("env_name", "attribute", "expected"),
[
(
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS",
"configuration_source_agentless_poll_interval_seconds",
30,
),
(
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS",
"configuration_source_agentless_request_timeout_seconds",
5,
),
(
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS",
"initialization_timeout_ms",
10000,
),
],
)
def test_unparsable_integer_setting_falls_back_to_default(monkeypatch, env_name, attribute, expected):
# OpenFeatureConfig is built at module scope, so raising here would surface as an
# ImportError for ddtrace.openfeature and take the application down at startup.
monkeypatch.setenv(env_name, "0.2")
assert getattr(OpenFeatureConfig(), attribute) == expected


def test_unparsable_float_setting_falls_back_to_default(monkeypatch):
monkeypatch.setenv("DD_FFE_INTAKE_HEARTBEAT_INTERVAL", "not-a-number")
assert OpenFeatureConfig().ffe_intake_heartbeat_interval == 1.0
Loading