Skip to content

Commit 7e85c4b

Browse files
Add FlagAwareModel comments, log_level setting, align tests with simplified FeatureFlagManager
- Add block comment on FlagAwareModel explaining how LD flag lookup works (prefix propagation, .get() vs direct access, model_dump unaffected) - Add log_level as top-level Settings field (default "INFO"), LD-overridable via flag key "log_level" - Settings now extends FlagAwareModel so .get() works at the top level - Update tests to match user's simplified FeatureFlagManager (raises on construction failure, no empty-string fallback) - Add TestLogLevel covering default, config, env, and LD override Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ec528cc commit 7e85c4b

3 files changed

Lines changed: 80 additions & 53 deletions

File tree

src/dremioai/config/feature_flags.py

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16-
from typing import Optional, Any, Self
16+
from typing import Optional, Any, Self, ClassVar
1717
from dremioai import log
1818
import ldclient
1919
from ldclient.config import Config
@@ -22,66 +22,49 @@
2222
class FeatureFlagManager:
2323
"""Manages LaunchDarkly feature flags for MCP server."""
2424

25-
_instance: Optional[Self] = None
26-
_client: Optional[ldclient.LDClient] = None
25+
_log = log.logger("feature_flags")
26+
_instance: ClassVar[Self] = None
2727

2828
def __init__(self, sdk_key: str):
29-
if not sdk_key:
30-
log.logger("feature_flags").warning(
31-
"LaunchDarkly SDK key is empty, feature flags will be disabled"
32-
)
33-
return
34-
3529
try:
3630
ldclient.set_config(Config(sdk_key))
3731
self._client = ldclient.get()
3832

3933
if self._client.is_initialized():
40-
log.logger("feature_flags").info(
41-
"LaunchDarkly client initialized successfully"
42-
)
34+
self._log.info("LaunchDarkly client initialized successfully")
4335
else:
44-
log.logger("feature_flags").warning(
45-
"LaunchDarkly client initialization pending"
46-
)
47-
except Exception as e:
48-
log.logger("feature_flags").error(
49-
f"Failed to initialize LaunchDarkly client: {e}"
50-
)
36+
self._log.warning("LaunchDarkly client initialization pending")
37+
except:
38+
self._log.exception(f"Failed to initialize LaunchDarkly client")
39+
raise
5140

5241
@classmethod
5342
def instance(cls) -> Self:
5443
"""Lazily initializes from settings.instance().dremio.launchdarkly.sdk_key."""
5544
if cls._instance is None:
56-
from dremioai.config import settings as _settings
45+
from dremioai.config import settings
5746

58-
sdk_key = None
59-
s = _settings.instance()
60-
if s and s.dremio and s.dremio.launchdarkly:
61-
sdk_key = s.dremio.launchdarkly.sdk_key
62-
cls._instance = cls(sdk_key or "")
47+
sdk_key = settings.instance().dremio.launchdarkly.sdk_key
48+
cls._instance = cls(sdk_key)
6349
return cls._instance
6450

6551
@classmethod
6652
def reset(cls):
6753
if cls._instance and cls._instance._client:
6854
cls._instance._client.close()
6955
cls._instance = None
70-
cls._client = None
7156

7257
def is_enabled(self) -> bool:
7358
return self._client is not None and self._client.is_initialized()
7459

7560
def get_flag(self, flag_key: str, default: Any) -> Any:
76-
try:
77-
if self.is_enabled():
78-
value = self._client.variation(flag_key, ldclient.Context.create("mcp-server"), default)
79-
log.logger("feature_flags").debug(
80-
f"Flag '{flag_key}' evaluated to: {value} (default: {default})"
81-
)
82-
return value
83-
except Exception:
84-
log.logger("feature_flags").exception(
85-
f"Error evaluating flag '{flag_key}' using default: {default}"
61+
if not self.is_enabled():
62+
self._log.debug(
63+
f"Flag '{flag_key}' not evaluated, LaunchDarkly not enabled/initialized (default: {default})"
8664
)
87-
return default
65+
return default
66+
value = self._client.variation(
67+
flag_key, ldclient.Context.create("mcp-server"), default
68+
)
69+
self._log.debug(f"Flag '{flag_key}' evaluated to: {value} (default: {default})")
70+
return value

src/dremioai/config/settings.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,24 @@ def get(self, field_name: str, default: Any = None):
6363
return getattr(self, field_name, default)
6464

6565

66+
# FlagAwareModel overrides .get() to check LaunchDarkly before returning the
67+
# config value. The LD flag key is "{_flag_prefix}.{field_name}", e.g.
68+
# "dremio.allow_dml" or "dremio.api.http_retry.max_retries".
69+
#
70+
# _flag_prefix is auto-set by _propagate_flag_prefixes() during
71+
# Settings.model_post_init — it mirrors the model's nesting path.
72+
#
73+
# Direct attribute access (obj.field) always returns the config value;
74+
# only .get() consults LD. This keeps model_dump() and serialization
75+
# unaffected by remote flag state.
6676
class FlagAwareModel(GetterModel):
6777
_flag_prefix: str = ""
6878

6979
def get(self, field_name: str, default: Any = None):
70-
mgr = FeatureFlagManager.instance()
80+
try:
81+
mgr = FeatureFlagManager.instance()
82+
except Exception:
83+
return super().get(field_name, default)
7184
if mgr.is_enabled():
7285
key = f"{self._flag_prefix}.{field_name}" if self._flag_prefix else field_name
7386
if (flag := mgr.get_flag(key, default=default)) is not None:
@@ -339,7 +352,8 @@ class BeeAI(FlagAwareModel):
339352
ollama: Optional[Ollama] = Field(default=None)
340353

341354

342-
class Settings(BaseSettings):
355+
class Settings(FlagAwareModel, BaseSettings):
356+
log_level: Optional[str] = Field(default="INFO")
343357
dremio: Optional[Dremio] = Field(default=None)
344358
tools: Optional[Tools] = Field(default_factory=Tools)
345359
prometheus: Optional[Prometheus] = Field(default=None)
@@ -351,6 +365,7 @@ class Settings(BaseSettings):
351365
env_prefix="DREMIOAI_",
352366
env_extra="allow",
353367
use_enum_values=True,
368+
validate_assignment=True,
354369
)
355370

356371
def model_post_init(self, __context):

tests/config/test_launchdarkly_integration.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,6 @@ def test_initialization_with_sdk_key(self, mock_ldclient):
5656
mock_ldclient.set_config.assert_called_once()
5757
assert mgr.is_enabled() is True
5858

59-
@patch("dremioai.config.feature_flags.ldclient")
60-
def test_initialization_without_sdk_key(self, mock_ldclient):
61-
mgr = FeatureFlagManager("")
62-
assert mgr.is_enabled() is False
63-
mock_ldclient.set_config.assert_not_called()
64-
6559
@patch("dremioai.config.feature_flags.ldclient")
6660
def test_get_flag_returns_ld_value(self, mock_ldclient):
6761
mock_client = _make_mock_ld_client({"my_flag": True})
@@ -79,14 +73,13 @@ def test_get_flag_returns_default_when_not_found(self, mock_ldclient):
7973
assert mgr.get_flag("unknown_flag", default="fallback") == "fallback"
8074

8175
@patch("dremioai.config.feature_flags.ldclient")
82-
def test_get_flag_returns_default_on_exception(self, mock_ldclient):
76+
def test_get_flag_returns_default_when_not_initialized(self, mock_ldclient):
8377
mock_client = MagicMock()
84-
mock_client.is_initialized.return_value = True
85-
mock_client.variation.side_effect = Exception("LD error")
78+
mock_client.is_initialized.return_value = False
8679
mock_ldclient.get.return_value = mock_client
8780

8881
mgr = FeatureFlagManager("test-sdk-key")
89-
assert mgr.get_flag("error_flag", default="safe_default") == "safe_default"
82+
assert mgr.get_flag("any_flag", default="fallback") == "fallback"
9083

9184
@patch("dremioai.config.feature_flags.ldclient")
9285
def test_singleton_pattern(self, mock_ldclient):
@@ -100,12 +93,17 @@ def test_singleton_pattern(self, mock_ldclient):
10093

10194
assert mgr1 is mgr2
10295

103-
def test_singleton_disabled_when_ld_not_configured(self):
96+
def test_singleton_raises_when_ld_not_configured(self):
10497
_make_settings()
105-
mgr = FeatureFlagManager.instance()
106-
assert mgr.is_enabled() is False
98+
with pytest.raises(AttributeError):
99+
FeatureFlagManager.instance()
100+
101+
@patch("dremioai.config.feature_flags.ldclient")
102+
def test_singleton_disabled_when_sdk_key_not_set(self, mock_ldclient):
103+
mock_client = MagicMock()
104+
mock_client.is_initialized.return_value = False
105+
mock_ldclient.get.return_value = mock_client
107106

108-
def test_singleton_disabled_when_sdk_key_not_set(self):
109107
_make_settings(launchdarkly={})
110108
mgr = FeatureFlagManager.instance()
111109
assert mgr.is_enabled() is False
@@ -437,3 +435,34 @@ def test_get_enable_search_with_ld_override(self, mock_ldclient):
437435
assert cfg.dremio.enable_search is False
438436
# .get(): LD override
439437
assert cfg.dremio.get("enable_search") is True
438+
439+
440+
class TestLogLevel:
441+
442+
def test_log_level_default(self):
443+
cfg = settings.Settings.model_validate({})
444+
assert cfg.log_level == "INFO"
445+
446+
def test_log_level_from_config(self):
447+
cfg = settings.Settings.model_validate({"log_level": "DEBUG"})
448+
assert cfg.log_level == "DEBUG"
449+
450+
def test_log_level_get_without_ld(self):
451+
cfg = settings.Settings.model_validate({"log_level": "WARNING"})
452+
assert cfg.get("log_level") == "WARNING"
453+
454+
@patch("dremioai.config.feature_flags.ldclient")
455+
def test_log_level_overridden_by_ld(self, mock_ldclient):
456+
mock_client = _make_mock_ld_client({"log_level": "ERROR"})
457+
mock_ldclient.get.return_value = mock_client
458+
459+
cfg = _make_settings(launchdarkly={"sdk_key": "test-key"})
460+
# Settings._flag_prefix is "" so flag key is just "log_level"
461+
assert cfg.get("log_level") == "ERROR"
462+
# Direct access returns config value
463+
assert cfg.log_level == "INFO"
464+
465+
def test_log_level_from_env(self, monkeypatch):
466+
monkeypatch.setenv("DREMIOAI_LOG_LEVEL", "TRACE")
467+
cfg = settings.Settings.model_validate({})
468+
assert cfg.log_level == "TRACE"

0 commit comments

Comments
 (0)