Skip to content

Commit a663f36

Browse files
feat: dynamic instrumentation public interface (#4052) (#4216)
## Description This change exposes the public API for the dynamic instrumentation service and updates the documentation with the new configuration interface. ## Checklist - [x] Title must conform to [conventional commit](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional). - [x] Add additional sections for `feat` and `fix` pull requests. - [x] Ensure tests are passing for affected code. - [x] [Library documentation](https://github.com/DataDog/dd-trace-py/tree/1.x/docs) and/or [Datadog's documentation site](https://github.com/DataDog/documentation/) is updated. Link to doc PR in description. ## Motivation The dynamic instrumentation service is now fully merged into the public repository and it needs a public API to be enabled and configured. ## Design The design follows the pattern of similar services already exposed by the library, e.g. `RuntimeMetrics`. The public API exposes the `DynamicInstrumentation` class, which has the public methods `enable` and `disable`. When using `ddtrace-run`, enablement of the new service can be controlled via the environment, as per configuration documentation. ## Testing strategy The enablement API is tested for both the environment approach via `ddtrace-run` using subprocess tests, as well as via the programmatic interface, using the `DynamicInstrumentation.enable` and `DynamicInstrumentation.disable` class methods. An additional test to check the expected behaviour during fork is also included. ## Reviewer Checklist - [x] Title is accurate. - [x] Description motivates each change. - [x] No unnecessary changes were introduced in this PR. - [x] PR cannot be broken up into smaller PRs. - [x] Avoid breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [x] Tests provided or description of manual testing performed is included in the code or PR. - [x] Release note has been added for fixes and features, or else `changelog/no-changelog` label added. - [x] All relevant GitHub issues are correctly linked. - [x] Backports are identified and tagged with Mergifyio. - [x] Add to milestone. (cherry picked from commit c55a28c) Co-authored-by: Gabriele N. Tornetta <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent c8a32df commit a663f36

File tree

17 files changed

+210
-114
lines changed

17 files changed

+210
-114
lines changed

ddtrace/bootstrap/sitecustomize.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from ddtrace import config # noqa
2121
from ddtrace import constants
22+
from ddtrace.debugging._config import config as debugger_config
2223
from ddtrace.internal.logger import get_logger # noqa
2324
from ddtrace.internal.runtime.runtime_metrics import RuntimeWorker
2425
from ddtrace.internal.utils.formats import asbool # noqa
@@ -84,6 +85,11 @@ def update_patched_modules():
8485
log.debug("profiler enabled via environment variable")
8586
import ddtrace.profiling.auto # noqa: F401
8687

88+
if debugger_config.enabled:
89+
from ddtrace.debugging import DynamicInstrumentation
90+
91+
DynamicInstrumentation.enable()
92+
8793
if asbool(os.getenv("DD_RUNTIME_METRICS_ENABLED")):
8894
RuntimeWorker.enable()
8995

ddtrace/debugging/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""
2+
Dynamic Instrumentation
3+
=======================
4+
5+
Configuration
6+
-------------
7+
8+
When using ``ddtrace-run``, dynamic instrumentation can be enabled by setting
9+
the ``DD_DYNAMIC_INSTRUMENTATION_ENABLED`` variable, or programmatically with::
10+
11+
from ddtrace.debugging import DynamicInstrumentation
12+
13+
# Enable dynamic instrumentation
14+
DynamicInstrumentation.enable()
15+
16+
...
17+
18+
# Disable the debugger
19+
DynamicInstrumentation.disable()
20+
"""
21+
22+
from ddtrace.debugging._debugger import Debugger as DynamicInstrumentation
23+
24+
25+
__all__ = ["DynamicInstrumentation"]

ddtrace/debugging/_config.py

Lines changed: 3 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,9 @@
1-
import os
2-
from typing import Dict
3-
from typing import Optional
4-
5-
from ddtrace import config as tracer_config
6-
from ddtrace.internal.agent import get_trace_url
7-
from ddtrace.internal.constants import DEFAULT_SERVICE_NAME
81
from ddtrace.internal.logger import get_logger
9-
from ddtrace.internal.utils.config import get_application_name
10-
from ddtrace.internal.utils.formats import asbool
11-
from ddtrace.version import get_version
2+
from ddtrace.settings.dynamic_instrumentation import DynamicInstrumentationConfig
123

134

145
log = get_logger(__name__)
156

7+
config = DynamicInstrumentationConfig()
168

17-
DEFAULT_DEBUGGER_PORT = 8126
18-
DEFAULT_MAX_PROBES = 100
19-
DEFAULT_METRICS = True
20-
DEFAULT_GLOBAL_RATE_LIMIT = 100.0
21-
DEFAULT_MAX_PAYLOAD_SIZE = 1 << 20 # 1 MB
22-
DEFAULT_CONFIG_TIMEOUT = 30 # s
23-
DEFAULT_UPLOAD_TIMEOUT = 30 # seconds
24-
DEFAULT_UPLOAD_FLUSH_INTERVAL = 1.0 # seconds
25-
DEFAULT_PROBE_POLL_INTERVAL = 1.0 # seconds
26-
DEFAULT_DIAGNOSTIC_INTERVAL = 3600 # 1 hour
27-
28-
29-
class DebuggerConfig(object):
30-
"""Debugger configuration."""
31-
32-
service_name = DEFAULT_SERVICE_NAME
33-
probe_url = get_trace_url()
34-
snapshot_intake_url = get_trace_url()
35-
max_probes = DEFAULT_MAX_PROBES
36-
metrics = DEFAULT_METRICS
37-
global_rate_limit = DEFAULT_GLOBAL_RATE_LIMIT
38-
max_payload_size = DEFAULT_MAX_PAYLOAD_SIZE
39-
config_timeout = DEFAULT_CONFIG_TIMEOUT
40-
upload_timeout = DEFAULT_UPLOAD_TIMEOUT
41-
upload_flush_interval = DEFAULT_UPLOAD_FLUSH_INTERVAL
42-
poll_interval = DEFAULT_PROBE_POLL_INTERVAL
43-
diagnostic_interval = DEFAULT_DIAGNOSTIC_INTERVAL
44-
tags = None # type: Optional[str]
45-
_tags = {} # type: Dict[str, str]
46-
_tags_in_qs = True
47-
_snapshot_intake_endpoint = "/v1/input"
48-
49-
def __init__(self):
50-
# type: () -> None
51-
try:
52-
self.snapshot_intake_url = os.environ["DD_DEBUGGER_SNAPSHOT_INTAKE_URL"]
53-
self._tags_in_qs = False
54-
except KeyError:
55-
self.snapshot_intake_url = get_trace_url()
56-
self._snapshot_intake_endpoint = "/debugger" + self._snapshot_intake_endpoint
57-
58-
self.probe_url = os.getenv("DD_DEBUGGER_PROBE_URL", get_trace_url())
59-
self.upload_timeout = int(os.getenv("DD_DEBUGGER_UPLOAD_TIMEOUT", DEFAULT_UPLOAD_TIMEOUT))
60-
self.upload_flush_interval = float(
61-
os.getenv("DD_DEBUGGER_UPLOAD_FLUSH_INTERVAL", DEFAULT_UPLOAD_FLUSH_INTERVAL)
62-
)
63-
64-
self._tags["env"] = tracer_config.env
65-
self._tags["version"] = tracer_config.version
66-
self._tags["debugger_version"] = get_version()
67-
68-
self._tags.update(tracer_config.tags)
69-
70-
self.tags = ",".join([":".join((k, v)) for (k, v) in self._tags.items() if v is not None])
71-
72-
self.service_name = tracer_config.service or get_application_name() or DEFAULT_SERVICE_NAME
73-
self.metrics = asbool(os.getenv("DD_DEBUGGER_METRICS_ENABLED", DEFAULT_METRICS))
74-
self.max_payload_size = int(os.getenv("DD_DEBUGGER_MAX_PAYLOAD_SIZE", DEFAULT_MAX_PAYLOAD_SIZE))
75-
76-
self.config_timeout = int(os.getenv("DD_DEBUGGER_CONFIG_TIMEOUT", DEFAULT_CONFIG_TIMEOUT))
77-
self.poll_interval = int(os.getenv("DD_DEBUGGER_POLL_INTERVAL", DEFAULT_PROBE_POLL_INTERVAL))
78-
self.diagnostic_interval = int(os.getenv("DD_DEBUGGER_DIAGNOSTIC_INTERVAL", DEFAULT_DIAGNOSTIC_INTERVAL))
79-
80-
log.debug(
81-
"Debugger configuration: %r",
82-
{k: v for k, v in ((k, getattr(self, k)) for k in type(self).__dict__ if not k.startswith("__"))},
83-
)
84-
85-
86-
config = DebuggerConfig()
9+
log.debug("Dynamic instrumentation configuration: %r", config.__dict__)

ddtrace/debugging/_probe/remoteconfig.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def __init__(self, callback):
163163

164164
def _next_status_update_timestamp(self):
165165
# type: () -> None
166-
self._status_timestamp = time.time() + config.diagnostic_interval
166+
self._status_timestamp = time.time() + config.diagnostics_interval
167167

168168
def __call__(self, metadata, config):
169169
# type: (Any, Any) -> None

ddtrace/debugging/_probe/status.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020

2121

2222
class ProbeStatusLogger(object):
23-
ENDPOINT = config.snapshot_intake_url
24-
2523
def __init__(self, service, encoder):
2624
# type: (str, BufferedEncoder) -> None
2725
self._service = service
@@ -65,7 +63,7 @@ def _write(self, probe, status, message, exc_info=None):
6563

6664
while self._retry_queue:
6765
item, ts = self._retry_queue.popleft()
68-
if now - ts > config.diagnostic_interval:
66+
if now - ts > config.diagnostics_interval:
6967
# We discard the expired items as they wouldn't be picked
7068
# up by the backend anyway.
7169
continue

ddtrace/debugging/_uploader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def __init__(self, encoder, interval=None):
3636
}
3737
if config._tags_in_qs and config.tags:
3838
self.ENDPOINT += "?ddtags=" + config.tags
39-
self._connect = connector(config.snapshot_intake_url, timeout=config.upload_timeout)
39+
self._connect = connector(config._snapshot_intake_url, timeout=config.upload_timeout)
4040
self._retry_upload = tenacity.Retrying(
4141
# Retry RETRY_ATTEMPTS times within the first half of the processing
4242
# interval, using a Fibonacci policy with jitter
@@ -48,7 +48,7 @@ def __init__(self, encoder, interval=None):
4848

4949
log.debug(
5050
"Logs intake uploader initialized (url: %s, endpoint: %s, interval: %f)",
51-
config.snapshot_intake_url,
51+
config._snapshot_intake_url,
5252
self.ENDPOINT,
5353
self.interval,
5454
)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from envier import En
2+
3+
from ddtrace import config
4+
from ddtrace.internal.agent import get_trace_url
5+
from ddtrace.internal.constants import DEFAULT_SERVICE_NAME
6+
from ddtrace.internal.utils.config import get_application_name
7+
from ddtrace.version import get_version
8+
9+
10+
DEFAULT_MAX_PROBES = 100
11+
DEFAULT_GLOBAL_RATE_LIMIT = 100.0
12+
13+
14+
def _derive_tags(c):
15+
# type: (En) -> str
16+
_tags = dict(env=config.env, version=config.version, debugger_version=get_version())
17+
_tags.update(config.tags)
18+
19+
return ",".join([":".join((k, v)) for (k, v) in _tags.items() if v is not None])
20+
21+
22+
class DynamicInstrumentationConfig(En):
23+
__prefix__ = "dd.dynamic_instrumentation"
24+
25+
service_name = En.d(str, lambda _: config.service or get_application_name() or DEFAULT_SERVICE_NAME)
26+
_snapshot_intake_url = En.d(str, lambda _: get_trace_url())
27+
max_probes = En.d(int, lambda _: DEFAULT_MAX_PROBES)
28+
global_rate_limit = En.d(float, lambda _: DEFAULT_GLOBAL_RATE_LIMIT)
29+
_tags_in_qs = En.d(bool, lambda _: True)
30+
_snapshot_intake_endpoint = En.d(str, lambda _: "/debugger/v1/input")
31+
tags = En.d(str, _derive_tags)
32+
33+
enabled = En.v(
34+
bool,
35+
"enabled",
36+
default=False,
37+
help_type="Boolean",
38+
help="Enable the debugger",
39+
)
40+
41+
metrics = En.v(
42+
bool,
43+
"metrics.enabled",
44+
default=True,
45+
help_type="Boolean",
46+
help="Enable diagnostic metrics",
47+
)
48+
49+
max_payload_size = En.v(
50+
int,
51+
"max_payload_size",
52+
default=1 << 20, # 1 MB
53+
help_type="Integer",
54+
help="Maximum size in bytes of a single configuration payload that can be handled per request",
55+
)
56+
57+
upload_timeout = En.v(
58+
int,
59+
"upload.timeout",
60+
default=30, # seconds
61+
help_type="Integer",
62+
help="Timeout in seconds for uploading a snapshot",
63+
)
64+
65+
upload_flush_interval = En.v(
66+
float,
67+
"upload.flush_interval",
68+
default=1.0, # seconds
69+
help_type="Float",
70+
help="Interval in seconds for flushing the snapshot upload queue.",
71+
)
72+
73+
diagnostics_interval = En.v(
74+
int,
75+
"diagnostics.interval",
76+
default=3600, # 1 hour
77+
help_type="Integer",
78+
help="Interval in seconds for periodically sending probe diagnostic messages",
79+
)

docs/conf.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@ def _skip(self, word):
6565
# Add any Sphinx extension module names here, as strings. They can be
6666
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
6767
# ones.
68-
extensions = ["sphinx.ext.autodoc", "sphinx.ext.extlinks", "reno.sphinxext", "sphinxcontrib.spelling"]
68+
extensions = [
69+
"sphinx.ext.autodoc",
70+
"sphinx.ext.extlinks",
71+
"reno.sphinxext",
72+
"sphinxcontrib.spelling",
73+
"envier.sphinx",
74+
]
6975

7076
# Add filters for sphinxcontrib.spelling
7177
spelling_filters = [VersionTagFilter]

docs/configuration.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,9 @@ below:
353353
- Sensitive parameter value regexp for obfuscation.
354354

355355
.. _Unified Service Tagging: https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging/
356+
357+
358+
Dynamic Instrumentation
359+
-----------------------
360+
361+
.. envier:: ddtrace.settings.dynamic_instrumentation:DynamicInstrumentationConfig

mypy.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ warn_unused_configs = true
1111
no_implicit_optional = true
1212
ignore_missing_imports = true
1313
namespace_packages = true
14+
plugins = envier.mypy
1415

1516
[mypy-ddtrace.contrib.*]
1617
ignore_errors = true

0 commit comments

Comments
 (0)