Skip to content

Commit 2f586cc

Browse files
shatzibrettlangdonP403n1x87
authored
chore(debugger): support for all metric kinds with value expression (#4699)
## Description Effort to make feature parity with dd-trace-java metric implementation. To gain parity python debugger need to support: 1. GAUGE and HISTOGRAM metrics. 2. Be able to evaluate the metric value expression 3. Support Method Metric Probe with duration We also decided to support DISTRIBUTION metric support as well. This PR address the two main features. future PR would add support for function level Metric Probe ## Reviewer Checklist - [ ] Title is accurate. - [ ] Description motivates each change. - [ ] No unnecessary changes were introduced in this PR. - [ ] Avoid breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [ ] Tests provided or description of manual testing performed is included in the code or PR. - [ ] Release note has been added and follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines), or else `changelog/no-changelog` label added. - [ ] All relevant GitHub issues are correctly linked. - [ ] Backports are identified and tagged with Mergifyio. Co-authored-by: Brett Langdon <[email protected]> Co-authored-by: Gabriele N. Tornetta <[email protected]>
1 parent 2e07a34 commit 2f586cc

File tree

5 files changed

+86
-24
lines changed

5 files changed

+86
-24
lines changed

ddtrace/debugging/_debugger.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,19 @@ def _dd_debugger_hook(self, probe):
254254
if isinstance(probe, MetricProbe):
255255
# TODO: Handle value expressions
256256
assert probe.kind is not None and probe.name is not None
257+
258+
value = float(probe.value(sys._getframe(1).f_locals)) if probe.value is not None else 1
259+
260+
# TODO[perf]: We know the tags in advance so we can avoid the
261+
# list comprehension.
257262
if probe.kind == MetricProbeKind.COUNTER:
258-
# TODO[perf]: We know the tags in advance so we can avoid the
259-
# list comprehension.
260-
self._probe_meter.increment(probe.name, tags=probe.tags)
263+
self._probe_meter.increment(probe.name, value, probe.tags)
264+
elif probe.kind == MetricProbeKind.GAUGE:
265+
self._probe_meter.gauge(probe.name, value, probe.tags)
266+
elif probe.kind == MetricProbeKind.HISTOGRAM:
267+
self._probe_meter.histogram(probe.name, value, probe.tags)
268+
elif probe.kind == MetricProbeKind.DISTRIBUTION:
269+
self._probe_meter.distribution(probe.name, value, probe.tags)
261270

262271
return
263272

ddtrace/debugging/_probe/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,4 @@ class MetricProbeKind(object):
107107
class MetricProbe(LineProbe):
108108
kind = attr.ib(type=Optional[str], default=None)
109109
name = attr.ib(type=Optional[str], default=None)
110+
value = attr.ib(type=Optional[Callable[[Dict[str, Any]], Any]], default=None)

ddtrace/debugging/_probe/remoteconfig.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,21 @@
2525
_EXPRESSION_CACHE = LFUCache()
2626

2727

28-
def _invalid_condition(_):
29-
"""Forces probes with invalid conditions to never trigger.
28+
def _invalid_expression(_):
29+
"""Forces probes with invalid expression/conditions to never trigger.
3030
3131
Any signs of invalid conditions in logs is an indication of a problem with
3232
the expression compiler.
3333
"""
3434
return False
3535

3636

37-
INVALID_CONDITION = _invalid_condition
37+
INVALID_EXPRESSION = _invalid_expression
3838

3939

40-
def _compile_condition(when):
40+
def _compile_expression(when):
4141
# type: (Optional[Dict[str, Any]]) -> Optional[Callable[[Dict[str, Any]], Any]]
42-
global _EXPRESSION_CACHE, INVALID_CONDITION
42+
global _EXPRESSION_CACHE, INVALID_EXPRESSION
4343

4444
if when is None:
4545
return None
@@ -52,13 +52,13 @@ def compile_or_invalid(expr):
5252
return dd_compile(ast)
5353
except Exception:
5454
log.error("Cannot compile expression: %s", expr, exc_info=True)
55-
return INVALID_CONDITION
55+
return INVALID_EXPRESSION
5656

5757
expr = when["dsl"]
5858

5959
compiled = _EXPRESSION_CACHE.get(expr, compile_or_invalid) # type: Callable[[Dict[str, Any]], Any]
6060

61-
if compiled is INVALID_CONDITION:
61+
if compiled is INVALID_EXPRESSION:
6262
log.error("Cannot compile expression: %s", expr, exc_info=True)
6363

6464
return compiled
@@ -91,7 +91,7 @@ def probe(_id, _type, attribs):
9191
if _type == "snapshotProbes":
9292
args = dict(
9393
probe_id=_id,
94-
condition=_compile_condition(attribs.get("when")),
94+
condition=_compile_expression(attribs.get("when")),
9595
active=attribs["active"],
9696
tags=dict(_.split(":", 1) for _ in attribs.get("tags", [])),
9797
)
@@ -116,6 +116,7 @@ def probe(_id, _type, attribs):
116116
line=int(attribs["where"]["lines"][0]),
117117
name=attribs["metricName"],
118118
kind=attribs["kind"],
119+
value=_compile_expression(attribs.get("value")),
119120
)
120121

121122
raise ValueError("Unknown probe type: %s" % _type)

ddtrace/internal/metrics.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ def increment(self, name, value=1.0, tags=None):
4747
".".join((self.name, name)), value, [":".join(_) for _ in tags.items()] if tags else None
4848
)
4949

50+
def gauge(self, name, value=1.0, tags=None):
51+
# type: (str, float, Optional[Dict[str, str]]) -> None
52+
if not self.metrics.enabled:
53+
return None
54+
55+
self.metrics._client.gauge(
56+
".".join((self.name, name)), value, [":".join(_) for _ in tags.items()] if tags else None
57+
)
58+
59+
def histogram(self, name, value=1.0, tags=None):
60+
# type: (str, float, Optional[Dict[str, str]]) -> None
61+
if not self.metrics.enabled:
62+
return None
63+
64+
self.metrics._client.histogram(
65+
".".join((self.name, name)), value, [":".join(_) for _ in tags.items()] if tags else None
66+
)
67+
5068
def distribution(self, name, value=1.0, tags=None):
5169
# type: (str, float, Optional[Dict[str, str]]) -> None
5270
if not self.metrics.enabled:

tests/debugging/test_debugger.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -481,25 +481,58 @@ def mock_metrics():
481481
_probe_metrics._client = old_client
482482

483483

484-
def test_debugger_metric_probe(mock_metrics):
484+
def create_line_metric_probe(kind, value=None):
485+
return MetricProbe(
486+
probe_id="metric-probe-test",
487+
source_file="tests/submod/stuff.py",
488+
line=36,
489+
kind=kind,
490+
name="test.counter",
491+
tags={"foo": "bar"},
492+
value=value,
493+
)
494+
495+
496+
def test_debugger_metric_probe_simple_count(mock_metrics):
485497
with debugger() as d:
486-
d.add_probes(
487-
MetricProbe(
488-
probe_id="metric-probe-test",
489-
source_file="tests/submod/stuff.py",
490-
line=36,
491-
kind=MetricProbeKind.COUNTER,
492-
name="test.counter",
493-
tags={"foo": "bar"},
494-
),
495-
)
498+
d.add_probes(create_line_metric_probe(MetricProbeKind.COUNTER))
496499
sleep(0.5)
497-
498500
Stuff().instancestuff()
499-
500501
assert call("probe.test.counter", 1.0, ["foo:bar"]) in mock_metrics.increment.mock_calls
501502

502503

504+
def test_debugger_metric_probe_count_value(mock_metrics):
505+
with debugger() as d:
506+
d.add_probes(create_line_metric_probe(MetricProbeKind.COUNTER, dd_compile("#bar")))
507+
sleep(0.5)
508+
Stuff().instancestuff(40)
509+
assert call("probe.test.counter", 40.0, ["foo:bar"]) in mock_metrics.increment.mock_calls
510+
511+
512+
def test_debugger_metric_probe_guage_value(mock_metrics):
513+
with debugger() as d:
514+
d.add_probes(create_line_metric_probe(MetricProbeKind.GAUGE, dd_compile("#bar")))
515+
sleep(0.5)
516+
Stuff().instancestuff(41)
517+
assert call("probe.test.counter", 41.0, ["foo:bar"]) in mock_metrics.gauge.mock_calls
518+
519+
520+
def test_debugger_metric_probe_histogram_value(mock_metrics):
521+
with debugger() as d:
522+
d.add_probes(create_line_metric_probe(MetricProbeKind.HISTOGRAM, dd_compile("#bar")))
523+
sleep(0.5)
524+
Stuff().instancestuff(42)
525+
assert call("probe.test.counter", 42.0, ["foo:bar"]) in mock_metrics.histogram.mock_calls
526+
527+
528+
def test_debugger_metric_probe_distribution_value(mock_metrics):
529+
with debugger() as d:
530+
d.add_probes(create_line_metric_probe(MetricProbeKind.DISTRIBUTION, dd_compile("#bar")))
531+
sleep(0.5)
532+
Stuff().instancestuff(43)
533+
assert call("probe.test.counter", 43.0, ["foo:bar"]) in mock_metrics.distribution.mock_calls
534+
535+
503536
def test_debugger_multiple_function_probes_on_same_function():
504537
global Stuff
505538

0 commit comments

Comments
 (0)