Skip to content

Commit a04ffb0

Browse files
fix(tracer): dd_trace_methods wrapping async functions (#12337)
Resolves issue #10754 Updated` trace_wrapper` to check if the target function is async (using `inspect.iscoroutinefunction`). If it is, the wrapper uses async/await to keep the tracing span open until the coroutine truly finishes instead of closing immediately when the coroutine is created. ## 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 - [x] 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: Munir Abdinur <[email protected]>
1 parent 89689b8 commit a04ffb0

File tree

3 files changed

+57
-0
lines changed

3 files changed

+57
-0
lines changed

ddtrace/internal/tracemethods.py

+11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import inspect
12
from typing import List
23
from typing import Tuple
34

@@ -84,6 +85,16 @@ def trace_wrapper(wrapped, instance, args, kwargs):
8485
if hasattr(instance, "__class__") and instance.__class__ is not type(None): # noqa: E721
8586
resource = "%s.%s" % (instance.__class__.__name__, resource)
8687

88+
# Check for async
89+
if inspect.iscoroutinefunction(wrapped):
90+
91+
async def async_wrapper(*a, **kw):
92+
with tracer.trace("trace.annotation", resource=resource) as span:
93+
span.set_tag_str("component", "trace")
94+
return await wrapped(*a, **kw)
95+
96+
return async_wrapper(*args, **kwargs)
97+
8798
with tracer.trace("trace.annotation", resource=resource) as span:
8899
span.set_tag_str("component", "trace")
89100
return wrapped(*args, **kwargs)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
fixes:
3+
- |
4+
tracing: Captures the execution of async functions traced via DD_TRACE_METHODS, not just coroutine creation.
5+
This change increases span durations, which may affect latency-based metrics.

tests/integration/test_tracemethods.py

+41
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ async def async_test_method(self):
6060
async def async_test_method2(self):
6161
await self.async_test_method()
6262

63+
async def _nested_async_test_method(self):
64+
await asyncio.sleep(0.01)
65+
# Call other async method to confirm nested spans work
66+
await _async_test_method()
67+
await _async_test_method2()
68+
6369
class NestedClass:
6470
def test_method(self):
6571
pass
@@ -130,3 +136,38 @@ async def main():
130136
)
131137
assert status == 0, err
132138
assert out == b""
139+
140+
141+
@pytest.mark.snapshot()
142+
def test_ddtrace_run_trace_methods_async_nested(ddtrace_run_python_code_in_subprocess):
143+
"""
144+
This test ensures that async spans remain open for the duration of nested awaits
145+
in _Class._nested_async_test_method, which calls additional async functions.
146+
"""
147+
import os
148+
149+
env = os.environ.copy()
150+
env["DD_TRACE_METHODS"] = (
151+
"tests.integration.test_tracemethods:_Class._nested_async_test_method,"
152+
"tests.integration.test_tracemethods:_async_test_method,"
153+
"tests.integration.test_tracemethods:_async_test_method2"
154+
)
155+
156+
tests_dir = os.path.dirname(os.path.dirname(__file__))
157+
env["PYTHONPATH"] = os.pathsep.join([tests_dir, env.get("PYTHONPATH", "")])
158+
159+
code = r"""
160+
import asyncio
161+
from tests.integration.test_tracemethods import _Class
162+
163+
async def main():
164+
c = _Class()
165+
await c._nested_async_test_method()
166+
167+
asyncio.run(main())
168+
"""
169+
170+
out, err, status, _ = ddtrace_run_python_code_in_subprocess(code, env=env)
171+
172+
assert status == 0, err
173+
assert out == b""

0 commit comments

Comments
 (0)