Skip to content

Commit 41477ba

Browse files
vjfridgeCodex GPT-6
andcommitted
fix: gate feature flag flushing and cover wrapper contract
Respect DD_FEATURE_FLAGS_ENABLED during import and cleanup, test the real wrapper contract with released dependencies, and distinguish expected skips in debug logs. Environment: Datadog workspace Co-Authored-By: Codex GPT-6 <noreply@localhost>
1 parent 5b47539 commit 41477ba

6 files changed

Lines changed: 128 additions & 5 deletions

File tree

‎datadog_lambda/config.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def _resolve_env(self, key, default=None, cast=None, depends_on_tracing=False):
8585
enhanced_metrics_enabled = _get_env("DD_ENHANCED_METRICS", "true", as_bool)
8686

8787
flush_in_thread = _get_env("DD_FLUSH_IN_THREAD", "false", as_bool)
88+
feature_flags_enabled = _get_env("DD_FEATURE_FLAGS_ENABLED", "true", as_bool)
8889
feature_flag_flush_timeout_ms = _get_env(
8990
"DD_FEATURE_FLAGS_FLUSH_TIMEOUT_MS", 500.0, float
9091
)

‎datadog_lambda/feature_flags.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@
1717

1818
def flush_feature_flag_telemetry(context):
1919
try:
20-
if _telemetry is None or getattr(_telemetry, "FLUSH_API_VERSION", None) != 1:
20+
if _telemetry is None:
21+
return
22+
if getattr(_telemetry, "FLUSH_API_VERSION", None) != 1:
23+
logger.debug("Skipping feature flag telemetry flush: unsupported tracer")
2124
return
2225
cap_ms = config.feature_flag_flush_timeout_ms
2326
if not math.isfinite(cap_ms) or cap_ms <= 0:
2427
return
2528
# No remaining-time information means no safe invocation budget.
26-
remaining_ms = context.get_remaining_time_in_millis()
27-
budget_ms = min(cap_ms, remaining_ms - SAFETY_MARGIN_MS)
29+
get_remaining_ms = getattr(context, "get_remaining_time_in_millis", None)
30+
if get_remaining_ms is None:
31+
return
32+
budget_ms = min(cap_ms, get_remaining_ms() - SAFETY_MARGIN_MS)
2833
if budget_ms <= 0:
2934
logger.debug(
3035
"Skipping feature flag telemetry flush: no invocation time remains"

‎datadog_lambda/wrapper.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from importlib import import_module
1010
from time import time_ns
1111

12-
from datadog_lambda.feature_flags import flush_feature_flag_telemetry
1312
from datadog_lambda.extension import should_use_extension, flush_extension
1413
from datadog_lambda.cold_start import (
1514
set_cold_start,
@@ -78,6 +77,10 @@
7877
if config.llmobs_enabled:
7978
from ddtrace.llmobs import LLMObs
8079

80+
flush_feature_flag_telemetry = None
81+
if config.feature_flags_enabled:
82+
from datadog_lambda.feature_flags import flush_feature_flag_telemetry
83+
8184
if config.exception_replay_enabled:
8285
from ddtrace.debugging._exception.replay import SpanExceptionHandler
8386

@@ -212,7 +215,11 @@ def __call__(self, event, context, **kwargs):
212215
except Exception as e:
213216
logger.error(format_err_with_traceback(e))
214217
finally:
215-
flush_feature_flag_telemetry(context)
218+
if (
219+
config.feature_flags_enabled
220+
and flush_feature_flag_telemetry is not None
221+
):
222+
flush_feature_flag_telemetry(context)
216223
if self.blocking_response:
217224
return self.blocking_response
218225
return self.response

‎tests/test_config.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def _test_as_list(env_key, conf_key, default):
101101
*_test_as_bool("DD_MERGE_XRAY_TRACES", "merge_xray_traces", default=False),
102102
*_test_as_bool("DD_PROFILING_ENABLED", "profiling_enabled", default=False),
103103
*_test_as_bool("DD_LLMOBS_ENABLED", "llmobs_enabled", default=False),
104+
*_test_as_bool("DD_FEATURE_FLAGS_ENABLED", "feature_flags_enabled", default=True),
104105
*_test_as_bool(
105106
"DD_EXCEPTION_REPLAY_ENABLED", "exception_replay_enabled", default=False
106107
),

‎tests/test_feature_flags.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import importlib.util
22
import os
3+
import subprocess
4+
import sys
5+
import textwrap
36
import unittest
47
from unittest.mock import Mock, patch
58

@@ -92,6 +95,59 @@ def test_missing_context_and_telemetry_errors_are_safe(self):
9295
)
9396
telemetry.flush.assert_called_once()
9497

98+
def test_missing_remaining_time_skips_without_failure_log(self):
99+
for context in (None, object()):
100+
with self.subTest(context=context), patch.object(
101+
feature_flags, "_telemetry"
102+
) as telemetry, patch.object(feature_flags, "logger") as log:
103+
telemetry.FLUSH_API_VERSION = 1
104+
feature_flags.flush_feature_flag_telemetry(context)
105+
telemetry.flush.assert_not_called()
106+
log.debug.assert_not_called()
107+
108+
def test_unsupported_tracer_skip_is_logged(self):
109+
with patch.object(
110+
feature_flags, "_telemetry", Mock(FLUSH_API_VERSION=2)
111+
) as telemetry, patch.object(feature_flags, "logger") as log:
112+
feature_flags.flush_feature_flag_telemetry(
113+
Mock(get_remaining_time_in_millis=lambda: 1000)
114+
)
115+
telemetry.flush.assert_not_called()
116+
log.debug.assert_called_once_with(
117+
"Skipping feature flag telemetry flush: unsupported tracer"
118+
)
119+
120+
def test_disabled_feature_flags_do_not_import_flush_modules(self):
121+
code = textwrap.dedent("""
122+
import sys
123+
124+
class BlockFlushImports:
125+
def find_spec(self, fullname, *args):
126+
if fullname in (
127+
"datadog_lambda.feature_flags",
128+
"ddtrace.internal.openfeature._telemetry",
129+
):
130+
raise AssertionError("unexpected import: " + fullname)
131+
132+
sys.meta_path.insert(0, BlockFlushImports())
133+
from datadog_lambda import wrapper
134+
assert not wrapper.config.feature_flags_enabled
135+
assert "datadog_lambda.feature_flags" not in sys.modules
136+
assert "ddtrace.internal.openfeature._telemetry" not in sys.modules
137+
""")
138+
result = subprocess.run(
139+
[sys.executable, "-c", code],
140+
env={
141+
**os.environ,
142+
"DD_FEATURE_FLAGS_ENABLED": "false",
143+
"DD_INSTRUMENTATION_TELEMETRY_ENABLED": "false",
144+
},
145+
capture_output=True,
146+
text=True,
147+
timeout=30,
148+
)
149+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
150+
95151
def test_incomplete_diagnostic(self):
96152
with patch.object(feature_flags, "_telemetry") as telemetry, patch.object(
97153
feature_flags, "logger"

‎tests/test_wrapper.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import unittest
55
import importlib
66
import sys
7+
import types
78
import pytest
89

910
from unittest.mock import MagicMock, patch, call, ANY
@@ -77,6 +78,58 @@ def setUp(self):
7778
self.mock_dd_lambda_layer_tag = patcher.start()
7879
self.addCleanup(patcher.stop)
7980

81+
def test_real_decorator_calls_feature_flag_flush_contract(self):
82+
import ddtrace.internal.openfeature as openfeature_pkg
83+
from datadog_lambda import feature_flags
84+
85+
calls = []
86+
fake = types.ModuleType("ddtrace.internal.openfeature._telemetry")
87+
fake.FLUSH_API_VERSION = 1
88+
fake.flush = lambda timeout: calls.append(timeout) or "completed"
89+
self.addCleanup(importlib.reload, feature_flags)
90+
with patch.dict(sys.modules, {fake.__name__: fake}), patch.object(
91+
openfeature_pkg, "_telemetry", fake, create=True
92+
), patch.object(type(config), "feature_flags_enabled", True):
93+
importlib.reload(feature_flags)
94+
context = get_mock_context()
95+
context.get_remaining_time_in_millis = lambda: 30000
96+
97+
handler = wrapper.datadog_lambda_wrapper(
98+
lambda event, context: {"ok": True}
99+
)
100+
self.assertEqual(handler({}, context), {"ok": True})
101+
102+
error = ValueError("handler error")
103+
104+
def raising(event, context):
105+
raise error
106+
107+
with self.assertRaises(ValueError) as caught:
108+
wrapper.datadog_lambda_wrapper(raising)({}, context)
109+
self.assertIs(caught.exception, error)
110+
self.assertEqual(calls, [0.5, 0.5])
111+
112+
def test_disabled_feature_flags_skip_flush(self):
113+
with patch.object(type(config), "feature_flags_enabled", False), patch.object(
114+
wrapper, "flush_feature_flag_telemetry"
115+
) as flush:
116+
context = get_mock_context()
117+
context.get_remaining_time_in_millis = lambda: 30000
118+
handler = wrapper.datadog_lambda_wrapper(
119+
lambda event, context: {"ok": True}
120+
)
121+
self.assertEqual(handler({}, context), {"ok": True})
122+
123+
error = ValueError("handler error")
124+
125+
def raising(event, context):
126+
raise error
127+
128+
with self.assertRaises(ValueError) as caught:
129+
wrapper.datadog_lambda_wrapper(raising)({}, context)
130+
self.assertIs(caught.exception, error)
131+
flush.assert_not_called()
132+
80133
@patch("datadog_lambda.config.Config.trace_enabled", False)
81134
def test_datadog_lambda_wrapper(self):
82135
@wrapper.datadog_lambda_wrapper

0 commit comments

Comments
 (0)