Skip to content

Commit 6180246

Browse files
tylfinP403n1x87
authored andcommitted
feat(DI): add config to create probes through a file (#13747)
This pull request introduces support for loading probe definitions from a file in JSON format, enabling dynamic instrumentation via external configuration. The changes include updates to the `DynamicInstrumentationConfig` class, the debugger's initialization logic, and new tests to validate the functionality. This can be configured via `DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE` and used in combination with RC. The JSON should be an array of probe objects in the same format as received via the RC config object, for example: ``` [ { "id": "12e4866b-c2d0-4948-baf8-bd98027cd457", "version": 0, "type": "LOG_PROBE", "language": "python", "where": { "sourceFile": "tests/submod/stuff.py", "lines": [36], }, "tags": [], "template": "Hello new monitoring API", "captureSnapshot": True, "capture": {"maxReferenceDepth": 3}, "evaluateAt": "EXIT", } ] ``` ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [ ] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) Co-authored-by: Gabriele N. Tornetta <[email protected]>
1 parent f47ea19 commit 6180246

File tree

4 files changed

+112
-0
lines changed

4 files changed

+112
-0
lines changed

ddtrace/debugging/_debugger.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from collections import defaultdict
22
from collections import deque
33
from itertools import chain
4+
import json
45
import linecache
56
import os
67
from pathlib import Path
@@ -37,6 +38,7 @@
3738
from ddtrace.debugging._probe.remoteconfig import ProbePollerEvent
3839
from ddtrace.debugging._probe.remoteconfig import ProbePollerEventType
3940
from ddtrace.debugging._probe.remoteconfig import ProbeRCAdapter
41+
from ddtrace.debugging._probe.remoteconfig import build_probe
4042
from ddtrace.debugging._probe.status import ProbeStatusLogger
4143
from ddtrace.debugging._signal.collector import SignalCollector
4244
from ddtrace.debugging._signal.model import Signal
@@ -270,6 +272,8 @@ def __init__(self, tracer: Optional[Tracer] = None) -> None:
270272
raise_on_exceed=False,
271273
)
272274

275+
self.probe_file = di_config.probe_file
276+
273277
if di_config.enabled:
274278
# TODO: this is only temporary and will be reverted once the DD_REMOTE_CONFIGURATION_ENABLED variable
275279
# has been removed
@@ -281,8 +285,28 @@ def __init__(self, tracer: Optional[Tracer] = None) -> None:
281285
di_callback = self.__rc_adapter__(None, self._on_configuration, status_logger=status_logger)
282286
remoteconfig_poller.register("LIVE_DEBUGGING", di_callback, restart_on_fork=True)
283287

288+
# Load local probes from the probe file.
289+
self._load_local_config()
290+
284291
log.debug("%s initialized (service name: %s)", self.__class__.__name__, service_name)
285292

293+
def _load_local_config(self) -> None:
294+
if self.probe_file is None:
295+
return
296+
297+
# This is intentionally an all or nothing approach. If one probe is malformed, none of the
298+
# local probes will be installed, that way waiting for the success log guarantees installation.
299+
try:
300+
raw_probes = json.loads(self.probe_file.read_text())
301+
302+
probes = [build_probe(p) for p in raw_probes]
303+
304+
self._on_configuration(ProbePollerEvent.NEW_PROBES, probes)
305+
log.info("Successfully loaded probes from file %s: %s", self.probe_file, [p.probe_id for p in probes])
306+
307+
except Exception as e:
308+
log.error("Failed to load probes from file %s: %s", self.probe_file, e)
309+
286310
def _dd_debugger_hook(self, probe: Probe) -> None:
287311
"""Debugger probe hook.
288312

ddtrace/settings/dynamic_instrumentation.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from ddtrace import config as ddconfig
55
from ddtrace.internal import gitmetadata
6+
from ddtrace.internal.compat import Path
67
from ddtrace.internal.constants import DEFAULT_SERVICE_NAME
78
from ddtrace.internal.utils.config import get_application_name
89
from ddtrace.settings._agent import config as agent_config
@@ -138,5 +139,13 @@ class DynamicInstrumentationConfig(DDConfig):
138139
help="List of identifiers to exclude from redaction",
139140
)
140141

142+
probe_file = DDConfig.v(
143+
t.Optional[Path],
144+
"probe_file",
145+
default=None,
146+
help_type="Path",
147+
help="Path to a file containing probe definitions",
148+
)
149+
141150

142151
config = DynamicInstrumentationConfig()

tests/debugging/test_debugger.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from collections import Counter
22
from decimal import Decimal
3+
import json
34
import os.path
45
import sys
6+
import tempfile
57
from threading import Thread
68

79
import mock
@@ -24,6 +26,7 @@
2426
from ddtrace.debugging._signal.snapshot import _EMPTY_CAPTURED_CONTEXT
2527
from ddtrace.debugging._signal.tracing import SPAN_NAME
2628
from ddtrace.debugging._signal.utils import redacted_value
29+
from ddtrace.internal.compat import Path
2730
from ddtrace.internal.remoteconfig.worker import remoteconfig_poller
2831
from ddtrace.internal.service import ServiceStatus
2932
from ddtrace.internal.utils.formats import format_trace_id
@@ -1294,3 +1297,78 @@ def test_debugger_exception_conditional_function_probe():
12941297
return_capture = snapshot_data["captures"]["return"]
12951298
assert return_capture["throwable"]["message"] == "'Hello', 'world!', 42"
12961299
assert return_capture["throwable"]["type"] == "Exception"
1300+
1301+
1302+
def test_debugger_probe_file_configuration():
1303+
# Create sample probe configurations in the expected JSON format
1304+
probe_config = [
1305+
{
1306+
"id": "12e4866b-c2d0-4948-baf8-bd98027cd457",
1307+
"version": 0,
1308+
"type": "LOG_PROBE",
1309+
"language": "python",
1310+
"where": {
1311+
"sourceFile": "tests/submod/stuff.py",
1312+
"lines": [36],
1313+
},
1314+
"tags": [],
1315+
"template": "Hello new monitoring API",
1316+
"captureSnapshot": True,
1317+
"capture": {"maxReferenceDepth": 3},
1318+
"evaluateAt": "EXIT",
1319+
}
1320+
]
1321+
1322+
# Create temporary probe file
1323+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as temp_file:
1324+
json.dump(probe_config, temp_file)
1325+
# Capture the PATH to the file
1326+
temp_file_path = Path(temp_file.name)
1327+
1328+
try:
1329+
with debugger(probe_file=temp_file_path) as d:
1330+
from tests.submod.stuff import Stuff
1331+
1332+
Stuff().instancestuff()
1333+
1334+
assert len(d._probe_registry) == 1
1335+
assert d._probe_registry.get("12e4866b-c2d0-4948-baf8-bd98027cd457", False)
1336+
finally:
1337+
# Clean up temporary file
1338+
if os.path.exists(temp_file_path):
1339+
os.unlink(temp_file_path)
1340+
1341+
1342+
def test_debugger_probe_malformed_file_configuration():
1343+
# Failing to parse the probe configuration will result in an error being logged.
1344+
malformed_probe_config = [
1345+
{
1346+
"version": 0,
1347+
"type": "LOG_PROBE",
1348+
"language": "python",
1349+
"where": {
1350+
"sourceFile": "tests/submod/stuff.py",
1351+
"lines": [36],
1352+
},
1353+
"tags": [],
1354+
"template": "Hello new monitoring API",
1355+
"captureSnapshot": True,
1356+
"capture": {"maxReferenceDepth": 3},
1357+
"evaluateAt": "EXIT",
1358+
}
1359+
]
1360+
1361+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as temp_file:
1362+
json.dump(malformed_probe_config, temp_file)
1363+
temp_file_path = temp_file.name
1364+
1365+
try:
1366+
with mock.patch("ddtrace.debugging._debugger.log") as mock_logger:
1367+
with debugger(probe_file=temp_file_path):
1368+
mock_logger.error.assert_called_once()
1369+
call_args = mock_logger.error.call_args[0]
1370+
assert "Failed to load probes from file" in call_args[0]
1371+
assert temp_file_path in call_args[1]
1372+
finally:
1373+
if os.path.exists(temp_file_path):
1374+
os.unlink(temp_file_path)

tests/telemetry/test_writer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ def test_app_started_event_configuration_override(test_agent_session, run_python
366366
{"name": "DD_DYNAMIC_INSTRUMENTATION_ENABLED", "origin": "default", "value": False},
367367
{"name": "DD_DYNAMIC_INSTRUMENTATION_MAX_PAYLOAD_SIZE", "origin": "default", "value": 1048576},
368368
{"name": "DD_DYNAMIC_INSTRUMENTATION_METRICS_ENABLED", "origin": "default", "value": True},
369+
{"name": "DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE", "origin": "default", "value": None},
369370
{"name": "DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS", "origin": "default", "value": "set()"},
370371
{"name": "DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES", "origin": "default", "value": "set()"},
371372
{"name": "DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS", "origin": "default", "value": "set()"},

0 commit comments

Comments
 (0)