Skip to content

Commit 6f9a607

Browse files
emmettbutlervlad-scherbich
authored andcommitted
refactor: break cycles involving ddtrace.internal.debug (#19298)
This change refactors `ddtrace.internal.debug` to avoid the creation of import cycles with `ddtrace._trace` and `ddtrace.config`. The core approach is to encapsulate the tracer facts needed by the debug module in a class that can be imported by both the tracer and the debug module. This new class is used in `collect` and `pretty_collect` to avoid inline imports in those functions. Co-authored-by: emmett.butler <emmett.butler@datadoghq.com>
1 parent f636dfd commit 6f9a607

5 files changed

Lines changed: 106 additions & 44 deletions

File tree

ddtrace/_trace/tracer.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from ddtrace.internal.utils import _get_metas_to_propagate
6161
from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning
6262
from ddtrace.internal.utils.formats import format_trace_id
63+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
6364
from ddtrace.internal.writer import AgentWriterInterface
6465
from ddtrace.internal.writer import HTTPWriter
6566
from ddtrace.vendor.debtcollector import deprecate
@@ -389,7 +390,14 @@ def configure(
389390
def _generate_diagnostic_logs(self):
390391
if config._debug_mode or config._startup_logs_enabled:
391392
try:
392-
info = debug.collect()
393+
tracer_debug_info = TracerDebugInfo(
394+
writer=self._span_aggregator.writer,
395+
sampling_rules=self._sampler.rules,
396+
tags=self._tags,
397+
partial_flush_enabled=self._span_aggregator.partial_flush_enabled,
398+
partial_flush_min_spans=self._span_aggregator.partial_flush_min_spans,
399+
)
400+
info = debug.collect(tracer_debug_info)
393401
except Exception as e:
394402
msg = "Failed to collect start-up logs: %s" % e
395403
self._log_compat(logging.WARNING, "- DATADOG TRACER DIAGNOSTIC - %s" % msg)

ddtrace/commands/ddtrace_run.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ def _prepare_env(parser: argparse.ArgumentParser):
8989
if args.info:
9090
# Inline imports for performance.
9191
from ddtrace.internal.debug import pretty_collect
92+
from ddtrace.trace import tracer
9293

93-
print(pretty_collect(color=not args.colorless))
94+
print(pretty_collect(tracer, color=not args.colorless))
9495
sys.exit(0)
9596

9697
root_dir = os.path.dirname(ddtrace.__file__)

ddtrace/internal/debug.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22
import logging
33
import platform
44
import sys
5-
from typing import TYPE_CHECKING # noqa:F401
65
from typing import Any # noqa:F401
76
from typing import Union # noqa:F401
87

9-
import ddtrace
8+
from ddtrace import _monkey
109
from ddtrace.internal import agent
1110
from ddtrace.internal.packages import get_distributions
1211
from ddtrace.internal.settings import env
1312
from ddtrace.internal.settings._agent import config as agent_config
13+
from ddtrace.internal.settings._config import config
1414
from ddtrace.internal.settings.asm import config as asm_config
1515
from ddtrace.internal.utils.cache import callonce
16+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
1617
from ddtrace.internal.writer import AgentWriterInterface
1718
from ddtrace.internal.writer import LogWriter
1819
from ddtrace.version import __version__
@@ -42,38 +43,37 @@ def tags_to_str(tags: dict[str, Any]) -> str:
4243
return ",".join(["%s:%s" % (k, v) for k, v in tags.items()])
4344

4445

45-
def collect() -> dict[str, Any]:
46+
def collect(tracer_info: TracerDebugInfo) -> dict[str, Any]:
4647
"""Collect system and library information into a serializable dict."""
4748

4849
# Inline expensive imports to avoid unnecessary overhead on startup.
4950
from ddtrace.internal import gitmetadata
5051
from ddtrace.internal.runtime.runtime_metrics import RuntimeWorker
5152
from ddtrace.internal.settings.crashtracker import config as crashtracker_config
52-
from ddtrace.trace import tracer
5353

54-
if isinstance(tracer._span_aggregator.writer, LogWriter):
54+
if isinstance(tracer_info.writer, LogWriter):
5555
agent_url = "AGENTLESS"
5656
agent_error = None
57-
elif isinstance(tracer._span_aggregator.writer, AgentWriterInterface):
58-
writer = tracer._span_aggregator.writer
57+
elif isinstance(tracer_info.writer, AgentWriterInterface):
58+
writer = tracer_info.writer
5959
agent_url = writer.intake_url
6060
reachable = agent.is_reachable(agent_url) # type: ignore[no-untyped-call]
6161
agent_error = None if reachable else "Agent not reachable at %s" % agent_url
6262
else:
6363
agent_url = "CUSTOM"
6464
agent_error = None
6565

66-
sampling_rules = [str(rule) for rule in tracer._sampler.rules]
66+
sampling_rules = [str(rule) for rule in tracer_info.sampling_rules]
6767

6868
is_venv = in_venv()
6969

7070
packages_available = {name: version for (name, version) in get_distributions().items()}
7171
integration_configs: dict[str, Union[dict[str, Any], str]] = {}
72-
for module, enabled in ddtrace._monkey.PATCH_MODULES.items():
72+
for module, enabled in _monkey.PATCH_MODULES.items():
7373
# TODO: this check doesn't work in all cases... we need a mapping
7474
# between the module and the library name.
7575
module_available = module in packages_available
76-
module_instrumented = module in ddtrace._monkey._PATCHED_MODULES
76+
module_instrumented = module in _monkey._PATCHED_MODULES
7777
module_imported = module in sys.modules
7878

7979
if enabled:
@@ -83,17 +83,17 @@ def collect() -> dict[str, Any]:
8383
# This also doesn't load work in all cases since we don't always
8484
# name the configuration entry the same as the integration module
8585
# name :/
86-
config = ddtrace.config._config.get(module, "N/A")
86+
module_config = config._config.get(module, "N/A")
8787
else:
88-
config = None
88+
module_config = None
8989

9090
if module_available:
9191
integration_configs[module] = dict(
9292
enabled=enabled,
9393
instrumented=module_instrumented,
9494
module_version=packages_available[module],
9595
module_imported=module_imported,
96-
config=config,
96+
config=module_config,
9797
)
9898

9999
pip_version = packages_available.get("pip", "N/A")
@@ -117,26 +117,26 @@ def collect() -> dict[str, Any]:
117117
agent_url=agent_url,
118118
agent_error=agent_error,
119119
statsd_url=agent_config.dogstatsd_url,
120-
env=ddtrace.config.env or "",
121-
ddtrace_enabled=ddtrace.config._tracing_enabled,
120+
env=config.env or "",
121+
ddtrace_enabled=config._tracing_enabled,
122122
sampling_rules=sampling_rules,
123-
service=ddtrace.config.service or "",
123+
service=config.service or "",
124124
debug=logger.isEnabledFor(logging.DEBUG),
125125
enabled_cli="ddtrace" in env.get("PYTHONPATH", ""),
126-
log_injection_enabled=ddtrace.config._logs_injection,
127-
health_metrics_enabled=ddtrace.config._health_metrics_enabled,
126+
log_injection_enabled=config._logs_injection,
127+
health_metrics_enabled=config._health_metrics_enabled,
128128
runtime_metrics_enabled=RuntimeWorker.enabled,
129-
dd_version=ddtrace.config.version or "",
130-
global_tags=tags_to_str(ddtrace.config.tags),
131-
tracer_tags=tags_to_str(tracer._tags),
129+
dd_version=config.version or "",
130+
global_tags=tags_to_str(config.tags),
131+
tracer_tags=tags_to_str(tracer_info.tags),
132132
integrations=integration_configs,
133-
partial_flush_enabled=tracer._span_aggregator.partial_flush_enabled,
134-
partial_flush_min_spans=tracer._span_aggregator.partial_flush_min_spans,
133+
partial_flush_enabled=tracer_info.partial_flush_enabled,
134+
partial_flush_min_spans=tracer_info.partial_flush_min_spans,
135135
asm_enabled=asm_config._asm_enabled,
136136
iast_enabled=asm_config._iast_enabled,
137137
waf_timeout=asm_config._waf_timeout,
138-
remote_config_enabled=ddtrace.config._remote_config_enabled,
139-
config_endpoint=ddtrace.config._from_endpoint,
138+
remote_config_enabled=config._remote_config_enabled,
139+
config_endpoint=config._from_endpoint,
140140
crashtracking_enabled=crashtracker_config.enabled,
141141
gitmetadata_enabled=gitmetadata.config.enabled,
142142
git_repository_url=git_repository_url,
@@ -146,7 +146,7 @@ def collect() -> dict[str, Any]:
146146
)
147147

148148

149-
def pretty_collect(color=True):
149+
def pretty_collect(tracer: Any, color=True):
150150
class bcolors:
151151
HEADER = "\033[95m"
152152
OKBLUE = "\033[94m"
@@ -167,7 +167,7 @@ class bcolors:
167167
bcolors.ENDC = ""
168168
bcolors.BOLD = ""
169169

170-
info = collect()
170+
info = collect(TracerDebugInfo.from_tracer(tracer))
171171

172172
info_pretty = """{blue}{bold}Tracer Configurations:{end}
173173
Tracer enabled: {tracer_enabled}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import Any
5+
6+
7+
@dataclass
8+
class TracerDebugInfo:
9+
"""Tracer state consumed by ddtrace.internal.debug.collect()."""
10+
11+
writer: Any
12+
sampling_rules: list[Any]
13+
tags: dict[str, Any]
14+
partial_flush_enabled: bool
15+
partial_flush_min_spans: int
16+
17+
@classmethod
18+
def from_tracer(cls, tracer: Any) -> TracerDebugInfo:
19+
return cls(
20+
writer=tracer._span_aggregator.writer,
21+
sampling_rules=tracer._sampler.rules,
22+
tags=tracer._tags,
23+
partial_flush_enabled=tracer._span_aggregator.partial_flush_enabled,
24+
partial_flush_min_spans=tracer._span_aggregator.partial_flush_min_spans,
25+
)

tests/integration/test_debug.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ def test_standard_tags():
3232
from datetime import datetime
3333

3434
from ddtrace.internal import debug
35+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
36+
from ddtrace.trace import tracer
3537

36-
f = debug.collect()
38+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
3739

3840
date = f.get("date")
3941
assert isinstance(date, str)
@@ -97,8 +99,10 @@ def test_debug_post_configure_uds():
9799
import re
98100

99101
from ddtrace.internal import debug
102+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
103+
from ddtrace.trace import tracer
100104

101-
f = debug.collect()
105+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
102106

103107
agent_url = f.get("agent_url")
104108
assert agent_url == "unix:///file.sock"
@@ -121,7 +125,10 @@ class TestGlobalConfig(SubprocessTestCase):
121125
)
122126
)
123127
def test_env_config(self):
124-
f = debug.collect()
128+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
129+
from ddtrace.trace import tracer
130+
131+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
125132
assert f.get("agent_url") == "http://0.0.0.0:4321"
126133
assert f.get("health_metrics_enabled") is True
127134
assert f.get("log_injection_enabled") is True
@@ -141,7 +148,10 @@ def test_env_config(self):
141148
)
142149
)
143150
def test_trace_agent_url(self):
144-
f = debug.collect()
151+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
152+
from ddtrace.trace import tracer
153+
154+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
145155
assert f.get("agent_url") == "http://0.0.0.0:1234"
146156

147157
@run_in_subprocess(
@@ -211,17 +221,19 @@ def test_tracer_info_level_log(self):
211221
@pytest.mark.subprocess(ddtrace_run=True, err=None)
212222
def test_runtime_metrics_enabled_via_manual_start():
213223
from ddtrace.internal import debug
224+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
214225
from ddtrace.runtime import RuntimeMetrics
226+
from ddtrace.trace import tracer
215227

216-
f = debug.collect()
228+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
217229
assert f.get("runtime_metrics_enabled") is False
218230

219231
RuntimeMetrics.enable()
220-
f = debug.collect()
232+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
221233
assert f.get("runtime_metrics_enabled") is True
222234

223235
RuntimeMetrics.disable()
224-
f = debug.collect()
236+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
225237
assert f.get("runtime_metrics_enabled") is False
226238

227239

@@ -231,24 +243,31 @@ def test_runtime_metrics_enabled_via_env_var_start():
231243

232244
from ddtrace.internal import debug
233245
from ddtrace.internal.utils.formats import asbool
246+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
247+
from ddtrace.trace import tracer
234248

235-
f = debug.collect()
249+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
236250
assert f.get("runtime_metrics_enabled") is asbool(os.getenv("DD_RUNTIME_METRICS_ENABLED")), (
237251
f.get("runtime_metrics_enabled"),
238252
asbool(os.getenv("DD_RUNTIME_METRICS_ENABLED")),
239253
)
240254

241255

242256
def test_to_json():
243-
info = debug.collect()
257+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
258+
from ddtrace.trace import tracer
259+
260+
info = debug.collect(TracerDebugInfo.from_tracer(tracer))
244261
json.dumps(info)
245262

246263

247264
@pytest.mark.subprocess(env={"AWS_LAMBDA_FUNCTION_NAME": "something"})
248265
def test_agentless(monkeypatch):
249266
from ddtrace.internal import debug
267+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
268+
from ddtrace.trace import tracer
250269

251-
info = debug.collect()
270+
info = debug.collect(TracerDebugInfo.from_tracer(tracer))
252271
assert info.get("agent_url") == "AGENTLESS"
253272

254273

@@ -257,6 +276,7 @@ def test_custom_writer():
257276
from typing import Optional
258277

259278
from ddtrace.internal import debug
279+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
260280
from ddtrace.internal.writer import TraceWriter
261281
from ddtrace.trace import Span
262282
from ddtrace.trace import tracer
@@ -275,16 +295,18 @@ def flush_queue(self) -> None:
275295
pass
276296

277297
tracer._span_aggregator.writer = CustomWriter()
278-
info = debug.collect()
298+
info = debug.collect(TracerDebugInfo.from_tracer(tracer))
279299

280300
assert info.get("agent_url") == "CUSTOM"
281301

282302

283303
@pytest.mark.subprocess(env={"DD_TRACE_SAMPLING_RULES": '[{"sample_rate":1.0}]'})
284304
def test_startup_logs_sampling_rules():
285305
from ddtrace.internal import debug
306+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
307+
from ddtrace.trace import tracer
286308

287-
f = debug.collect()
309+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
288310

289311
assert f.get("sampling_rules") == [
290312
"SamplingRule(sample_rate=1.0, service=None, name=None, resource=None, tags={}, provenance=default)"
@@ -294,17 +316,21 @@ def test_startup_logs_sampling_rules():
294316
@pytest.mark.subprocess()
295317
def test_startup_logs_log_level_override_default():
296318
from ddtrace.internal import debug
319+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
320+
from ddtrace.trace import tracer
297321

298-
f = debug.collect()
322+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
299323

300324
assert f.get("log_level_override") is None
301325

302326

303327
@pytest.mark.subprocess(env={"DD_TRACE_LOG_LEVEL": "WARNING"})
304328
def test_startup_logs_log_level_override_set():
305329
from ddtrace.internal import debug
330+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
331+
from ddtrace.trace import tracer
306332

307-
f = debug.collect()
333+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
308334

309335
assert f.get("log_level_override") == "WARNING"
310336

@@ -379,8 +405,10 @@ def test_debug_span_log():
379405
)
380406
def test_partial_flush_log():
381407
from ddtrace.internal import debug
408+
from ddtrace.internal.utils.tracer_debug_info import TracerDebugInfo
409+
from ddtrace.trace import tracer
382410

383-
f = debug.collect()
411+
f = debug.collect(TracerDebugInfo.from_tracer(tracer))
384412

385413
partial_flush_enabled = f.get("partial_flush_enabled")
386414
partial_flush_min_spans = f.get("partial_flush_min_spans")

0 commit comments

Comments
 (0)