Repository: DataDog/dd-trace-py

Issue:
pytest with doctest is not working
### Which version of dd-trace-py are you using?
ddtrace-0.51.2
### Which version of pip are you using?
20.0.2
### Which version of the libraries are you using?
```
appdirs==1.4.4
attrs==21.2.0
azure-core==1.17.0
azure-storage-blob==12.8.1
azure-storage-queue==12.1.6
backports.entry-points-selectable==1.1.0
black==21.6b0
certifi==2021.5.30
cffi==1.14.6
cfgv==3.3.0
charset-normalizer==2.0.4
click==8.0.1
cryptography==3.4.7
ddtrace==0.51.2
distlib==0.3.2
elementpath==2.2.3
filelock==3.0.12
flake8==3.9.2
identify==2.2.13
idna==3.2
iniconfig==1.1.1
isodate==0.6.0
jsonschema==3.2.0
logzero==1.7.0
mccabe==0.6.1
msrest==0.6.21
mypy==0.910
mypy-extensions==0.4.3
nodeenv==1.6.0
oauthlib==3.1.1
packaging==21.0
pathspec==0.9.0
pipenv==2021.5.29
platformdirs==2.2.0
pluggy==0.13.1
pre-commit==2.14.0
protobuf==3.17.3
py==1.10.0
pycodestyle==2.7.0
pycparser==2.20
pyflakes==2.3.1
pyinotify==0.9.6
pyparsing==2.4.7
pyrsistent==0.18.0
pytest==6.2.4
PyYAML==5.4.1
regex==2021.8.3
requests==2.26.0
requests-oauthlib==1.3.0
rope==0.19.0
six==1.16.0
tenacity==8.0.1
toml==0.10.2
typing-extensions==3.10.0.0
urllib3==1.26.6
virtualenv==20.7.1
virtualenv-clone==0.5.6
xmlschema==1.7.0
xmltodict==0.12.0
```

### How can we reproduce your problem?
Create a python file with doctest in it. Run it with `pytest --ddtrace --doctest-modules`
### What is the result that you get?
`pytest --ddtrace ` fails with doctest enabled: `pytest --ddtrace --doctest-modules`
Without doctest pytest is working.

Result with doctest:
```
Run pytest --ddtrace
  pytest --ddtrace
  shell: /usr/bin/bash -e {0}
  env:
    pythonLocation: /opt/hostedtoolcache/Python/3.8.11/x64
    LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.8.11/x64/lib
    DD_ENV: ci
    DD_SERVICE: service
============================= test session starts ==============================
platform linux -- Python 3.8.11, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/runner/work/service/service, configfile: pyproject.toml
plugins: ddtrace-0.51.2
collected 20 items
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/_pytest/main.py", line 269, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/_pytest/main.py", line 323, in _main
INTERNALERROR>     config.hook.pytest_runtestloop(session=session)
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/hooks.py", line 286, in __call__
INTERNALERROR>     return self._hookexec(self, self.get_hookimpls(), kwargs)
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/manager.py", line 93, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/manager.py", line 84, in <lambda>
INTERNALERROR>     self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/callers.py", line 208, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/callers.py", line 80, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/callers.py", line 187, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/_pytest/main.py", line 348, in pytest_runtestloop
INTERNALERROR>     item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/hooks.py", line 286, in __call__
INTERNALERROR>     return self._hookexec(self, self.get_hookimpls(), kwargs)
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/manager.py", line 93, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/manager.py", line 84, in <lambda>
INTERNALERROR>     self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/callers.py", line 208, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/callers.py", line 80, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/pluggy/callers.py", line 182, in _multicall
INTERNALERROR>     next(gen)  # first yield
INTERNALERROR>   File "/opt/hostedtoolcache/Python/3.8.11/x64/lib/python3.8/site-packages/ddtrace/contrib/pytest/plugin.py", line 121, in pytest_runtest_protocol
INTERNALERROR>     span.set_tag(test.SUITE, item.module.__name__)
INTERNALERROR> AttributeError: 'DoctestItem' object has no attribute 'module'

============================ no tests ran in 0.84s =============================
Error: Process completed with exit code 3.
```
### What is the result that you expected?
Be able to run `pytest --ddtrace --doctest-modules`


Discussion:
Hi @lukasz-mitka, thanks for reporting! We were able to reproduce your error and will be working on putting out a fix.

Resolving patch:
diff --git a/ddtrace/contrib/pytest/plugin.py b/ddtrace/contrib/pytest/plugin.py
--- a/ddtrace/contrib/pytest/plugin.py
+++ b/ddtrace/contrib/pytest/plugin.py
@@ -1,3 +1,4 @@
+from doctest import DocTest
 import json
 from typing import Dict
 
@@ -118,7 +119,10 @@ def pytest_runtest_protocol(item, nextitem):
         span.set_tag(SPAN_KIND, KIND)
         span.set_tag(test.FRAMEWORK, FRAMEWORK)
         span.set_tag(test.NAME, item.name)
-        span.set_tag(test.SUITE, item.module.__name__)
+        if hasattr(item, "module"):
+            span.set_tag(test.SUITE, item.module.__name__)
+        elif hasattr(item, "dtest") and isinstance(item.dtest, DocTest):
+            span.set_tag(test.SUITE, item.dtest.globs["__name__"])
         span.set_tag(test.TYPE, SpanTypes.TEST.value)
 
         # Parameterized test cases will have a `callspec` attribute attached to the pytest Item object.


Test patch:
diff --git a/tests/contrib/pytest/test_pytest.py b/tests/contrib/pytest/test_pytest.py
--- a/tests/contrib/pytest/test_pytest.py
+++ b/tests/contrib/pytest/test_pytest.py
@@ -498,6 +498,36 @@ def test_service(ddspan, pytestconfig):
         for span in decoded_trace:
             assert span[b"meta"][b"_dd.origin"] == b"ciapp-test"
 
+    def test_pytest_doctest_module(self):
+        """Test that pytest with doctest works as expected."""
+        py_file = self.testdir.makepyfile(
+            """
+        '''
+        This module supplies one function foo(). For example,
+        >>> foo()
+        42
+        '''
+
+        def foo():
+            '''Returns the answer to life, the universe, and everything.
+            >>> foo()
+            42
+            '''
+            return 42
+
+        def test_foo():
+            assert foo() == 42
+        """
+        )
+        file_name = os.path.basename(py_file.strpath)
+        rec = self.inline_run("--ddtrace", "--doctest-modules", file_name)
+        rec.assertoutcome(passed=3)
+        spans = self.pop_spans()
+
+        assert len(spans) == 3
+        for span in spans:
+            assert span.get_tag(test.SUITE) == file_name.partition(".py")[0]
+
 
 @pytest.mark.parametrize(
     "repository_url,repository_name",