Skip to content

Commit b2c5590

Browse files
committed
feat(di): add capture expressions
We add support for the new capture expression feature that allows capturing specific values instead of the whole scope snapshot.
1 parent f225902 commit b2c5590

File tree

8 files changed

+84
-5
lines changed

8 files changed

+84
-5
lines changed

ddtrace/debugging/_exception/replay.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ def build(cls, exc_id: uuid.UUID, frame: FrameType) -> "SpanExceptionProbe":
159159
template=message,
160160
segments=[LiteralTemplateSegment(message)],
161161
take_snapshot=True,
162+
capture_expressions=[],
162163
limits=DEFAULT_CAPTURE_LIMITS,
163164
condition=None,
164165
condition_error_rate=0.0,

ddtrace/debugging/_origin/span.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def build(cls, name: str, module: str, function: str) -> "EntrySpanProbe":
6565
template=message,
6666
segments=[LiteralTemplateSegment(message)],
6767
take_snapshot=True,
68+
capture_expressions=[],
6869
limits=DEFAULT_CAPTURE_LIMITS,
6970
condition=None,
7071
condition_error_rate=0.0,
@@ -89,6 +90,7 @@ def build(cls, name: str, filename: str, line: int) -> "ExitSpanProbe":
8990
template=message,
9091
segments=[LiteralTemplateSegment(message)],
9192
take_snapshot=True,
93+
capture_expressions=[],
9294
limits=DEFAULT_CAPTURE_LIMITS,
9395
condition=None,
9496
condition_error_rate=0.0,

ddtrace/debugging/_probe/model.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,28 +245,35 @@ def _to_str(value):
245245
return "".join([_to_str(s.eval(scope)) for s in self.segments])
246246

247247

248+
@dataclass
249+
class CaptureExpression:
250+
name: str
251+
expr: DDExpression
252+
253+
248254
@dataclass
249255
class LogProbeMixin(AbstractProbeMixIn):
250256
template: str
251257
segments: List[TemplateSegment]
252258
take_snapshot: bool
259+
capture_expressions: List[CaptureExpression]
253260
limits: CaptureLimits = field(compare=False)
254261

255262
@property
256263
def __budget__(self) -> int:
257-
return 10 if self.take_snapshot else 100
264+
return 10 if self.take_snapshot or self.capture_expressions else 100
258265

259266

260267
@dataclass(eq=False)
261268
class LogLineProbe(Probe, LineLocationMixin, LogProbeMixin, ProbeConditionMixin, RateLimitMixin):
262269
def is_global_rate_limited(self) -> bool:
263-
return self.take_snapshot
270+
return self.take_snapshot or bool(self.capture_expressions)
264271

265272

266273
@dataclass(eq=False)
267274
class LogFunctionProbe(Probe, FunctionLocationMixin, TimingMixin, LogProbeMixin, ProbeConditionMixin, RateLimitMixin):
268275
def is_global_rate_limited(self) -> bool:
269-
return self.take_snapshot
276+
return self.take_snapshot or bool(self.capture_expressions)
270277

271278

272279
@dataclass

ddtrace/debugging/_probe/remoteconfig.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ddtrace.debugging._probe.model import DEFAULT_PROBE_RATE
1818
from ddtrace.debugging._probe.model import DEFAULT_SNAPSHOT_PROBE_RATE
1919
from ddtrace.debugging._probe.model import DEFAULT_TRIGGER_PROBE_RATE
20+
from ddtrace.debugging._probe.model import CaptureExpression
2021
from ddtrace.debugging._probe.model import CaptureLimits
2122
from ddtrace.debugging._probe.model import ExpressionTemplateSegment
2223
from ddtrace.debugging._probe.model import FunctionProbe
@@ -146,6 +147,7 @@ def update_args(cls, args, attribs):
146147
else DEFAULT_CAPTURE_LIMITS,
147148
condition_error_rate=DEFAULT_PROBE_CONDITION_ERROR_RATE, # TODO: should we take rate limit out of Probe?
148149
take_snapshot=take_snapshot,
150+
capture_expressions=[CaptureExpression(**_) for _ in attribs.get("captureExpressions", [])],
149151
template=attribs.get("template"),
150152
segments=[_compile_segment(segment) for segment in attribs.get("segments", [])],
151153
)

ddtrace/debugging/_signal/snapshot.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
from types import ModuleType
88
from typing import Any
99
from typing import Dict
10+
from typing import List
1011
from typing import Mapping
1112
from typing import Optional
1213
from typing import cast
1314

1415
from ddtrace.debugging._expressions import DDExpressionEvaluationError
1516
from ddtrace.debugging._probe.model import DEFAULT_CAPTURE_LIMITS
17+
from ddtrace.debugging._probe.model import CaptureExpression
1618
from ddtrace.debugging._probe.model import CaptureLimits
1719
from ddtrace.debugging._probe.model import FunctionLocationMixin
1820
from ddtrace.debugging._probe.model import LineLocationMixin
@@ -85,6 +87,26 @@ def timeout(_):
8587
}
8688

8789

90+
def _capture_expressions(
91+
exprs: List[CaptureExpression],
92+
scope: Mapping[str, Any],
93+
limits: CaptureLimits = DEFAULT_CAPTURE_LIMITS,
94+
) -> Dict[str, Any]:
95+
with HourGlass(duration=CAPTURE_TIME_BUDGET) as hg:
96+
97+
def timeout(_):
98+
return not hg.trickling()
99+
100+
return {
101+
"captureExpressions": {
102+
e.name: utils.capture_value(
103+
e.expr.eval(scope), limits.max_level, limits.max_len, limits.max_size, limits.max_fields, timeout
104+
)
105+
for e in exprs
106+
}
107+
}
108+
109+
88110
_EMPTY_CAPTURED_CONTEXT: Dict[str, Any] = {"arguments": {}, "locals": {}, "staticFields": {}, "throwable": None}
89111

90112

@@ -131,7 +153,13 @@ def _do(self, retval, exc_info, scope):
131153

132154
self._stack = utils.capture_stack(self.frame)
133155

134-
return _capture_context(frame, exc_info, retval=retval, limits=probe.limits) if probe.take_snapshot else None
156+
if probe.take_snapshot:
157+
return _capture_context(frame, exc_info, retval=retval, limits=probe.limits)
158+
159+
if probe.capture_expressions:
160+
return _capture_expressions(probe.capture_expressions, scope, probe.limits)
161+
162+
return None
135163

136164
def enter(self, scope: Mapping[str, Any]) -> None:
137165
self.entry_capture = self._do(_NOTSET, (None, None, None), scope)
@@ -166,7 +194,7 @@ def data(self):
166194
probe = self.probe
167195

168196
captures = {}
169-
if isinstance(probe, LogProbeMixin) and probe.take_snapshot:
197+
if isinstance(probe, LogProbeMixin) and (probe.take_snapshot or probe.capture_expressions):
170198
if isinstance(probe, LineLocationMixin):
171199
captures = {"lines": {str(probe.line): self.line_capture or _EMPTY_CAPTURED_CONTEXT}}
172200
elif isinstance(probe, FunctionLocationMixin):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
features:
3+
- |
4+
dynamic instrumentation: added support for capture expressions in log
5+
probes.

tests/debugging/test_debugger.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from ddtrace.internal.utils.formats import format_trace_id
3333
from ddtrace.internal.utils.inspection import linenos
3434
from tests.debugging.mocking import debugger
35+
from tests.debugging.utils import compile_capture_expressions
3536
from tests.debugging.utils import compile_template
3637
from tests.debugging.utils import create_log_function_probe
3738
from tests.debugging.utils import create_log_line_probe
@@ -916,6 +917,27 @@ def test_debugger_log_line_probe_generate_messages(stuff):
916917
assert not msg1["debugger"]["snapshot"]["captures"]
917918

918919

920+
def test_debugger_snapshot_line_probe_capture_expressions(stuff):
921+
with debugger(upload_flush_interval=float("inf")) as d:
922+
d.add_probes(
923+
create_snapshot_line_probe(
924+
probe_id="foo",
925+
source_file="tests/submod/stuff.py",
926+
line=36,
927+
rate=float("inf"),
928+
**compile_capture_expressions([{"name": "foo", "expr": {"dsl": "bar", "json": {"ref": "bar"}}}]),
929+
),
930+
)
931+
932+
stuff.Stuff().instancestuff(123)
933+
934+
(msg,) = d.uploader.wait_for_payloads(1)
935+
936+
assert msg["debugger"]["snapshot"]["captures"] == {
937+
"lines": {"36": {"captureExpressions": {"foo": {"type": "int", "value": "123"}}}}
938+
}
939+
940+
919941
class SpanProbeTestCase(TracerTestCase):
920942
def setUp(self):
921943
super(SpanProbeTestCase, self).setUp()

tests/debugging/utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ddtrace.debugging._probe.model import DEFAULT_PROBE_CONDITION_ERROR_RATE
77
from ddtrace.debugging._probe.model import DEFAULT_PROBE_RATE
88
from ddtrace.debugging._probe.model import DEFAULT_SNAPSHOT_PROBE_RATE
9+
from ddtrace.debugging._probe.model import CaptureExpression
910
from ddtrace.debugging._probe.model import ExpressionTemplateSegment
1011
from ddtrace.debugging._probe.model import LiteralTemplateSegment
1112
from ddtrace.debugging._probe.model import LogFunctionProbe
@@ -37,6 +38,15 @@ def compile_template(*args):
3738
return {"template": template, "segments": segments}
3839

3940

41+
def compile_capture_expressions(exprs):
42+
return {
43+
"take_snapshot": False,
44+
"capture_expressions": [
45+
CaptureExpression(name=expr["name"], expr=DDRedactedExpression.compile(expr["expr"])) for expr in exprs
46+
],
47+
}
48+
49+
4050
def ddexpr(json, dsl="test"):
4151
return DDExpression(dsl=dsl, callable=dd_compile(json))
4252

@@ -76,6 +86,7 @@ def _wrapper(*args, **kwargs):
7686
kwargs.setdefault("take_snapshot", False)
7787
kwargs.setdefault("rate", DEFAULT_PROBE_RATE)
7888
kwargs.setdefault("limits", DEFAULT_CAPTURE_LIMITS)
89+
kwargs.setdefault("capture_expressions", [])
7990
return f(*args, **kwargs)
8091

8192
return _wrapper
@@ -88,6 +99,7 @@ def _wrapper(*args, **kwargs):
8899
kwargs.setdefault("limits", DEFAULT_CAPTURE_LIMITS)
89100
kwargs.setdefault("template", "")
90101
kwargs.setdefault("segments", [])
102+
kwargs.setdefault("capture_expressions", [])
91103
return f(*args, **kwargs)
92104

93105
return _wrapper

0 commit comments

Comments
 (0)